diff options
-rw-r--r-- | components/layout/construct.rs | 3 | ||||
-rw-r--r-- | components/layout/fragment.rs | 72 | ||||
-rw-r--r-- | components/layout/inline.rs | 113 | ||||
-rw-r--r-- | components/layout/text.rs | 12 | ||||
-rw-r--r-- | components/layout/wrapper.rs | 10 | ||||
-rw-r--r-- | components/script/dom/webidls/CSSStyleDeclaration.webidl | 1 | ||||
-rw-r--r-- | components/style/properties/mod.rs.mako | 2 | ||||
-rw-r--r-- | tests/ref/basic.list | 1 | ||||
-rw-r--r-- | tests/ref/text_overflow_a.html | 20 | ||||
-rw-r--r-- | tests/ref/text_overflow_ref.html | 17 |
10 files changed, 195 insertions, 56 deletions
diff --git a/components/layout/construct.rs b/components/layout/construct.rs index 9e14e2244ab..421f4e25f3f 100644 --- a/components/layout/construct.rs +++ b/components/layout/construct.rs @@ -991,7 +991,8 @@ impl<'a> FlowConstructor<'a> { let mut unscanned_marker_fragments = DList::new(); unscanned_marker_fragments.push_back(Fragment::new_from_specific_info( node, - SpecificFragmentInfo::UnscannedText(UnscannedTextFragmentInfo::from_text(text)))); + SpecificFragmentInfo::UnscannedText( + UnscannedTextFragmentInfo::from_text(text)))); let marker_fragments = TextRunScanner::new().scan_for_runs( self.layout_context.font_context(), unscanned_marker_fragments); diff --git a/components/layout/fragment.rs b/components/layout/fragment.rs index 21396838f0c..8dcf24b8bee 100644 --- a/components/layout/fragment.rs +++ b/components/layout/fragment.rs @@ -37,7 +37,9 @@ use servo_util::logical_geometry::{LogicalRect, LogicalSize, LogicalMargin}; use servo_util::range::*; use servo_util::smallvec::SmallVec; use servo_util::str::is_whitespace; +use std::borrow::ToOwned; use std::cmp::{max, min}; +use std::collections::DList; use std::fmt; use std::num::ToPrimitive; use std::str::FromStr; @@ -49,6 +51,7 @@ use style::computed_values::{LengthOrPercentage, LengthOrPercentageOrAuto}; use style::computed_values::{LengthOrPercentageOrNone, clear, mix_blend_mode, overflow_wrap}; use style::computed_values::{position, text_align, text_decoration, vertical_align, white_space}; use style::computed_values::{word_break}; +use text::TextRunScanner; use url::Url; /// Fragments (`struct Fragment`) are the leaves of the layout tree. They cannot position @@ -575,6 +578,14 @@ pub struct SplitResult { pub text_run: Arc<Box<TextRun>>, } +/// Describes how a fragment should be truncated. +pub struct TruncationResult { + /// The part of the fragment remaining after truncation. + pub split: SplitInfo, + /// The text run which is being truncated. + pub text_run: Arc<Box<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)] @@ -778,14 +789,12 @@ impl Fragment { /// Transforms this fragment into another fragment of the given type, with the given size, /// preserving all the other data. - pub fn transform(&self, size: LogicalSize<Au>, mut info: Box<ScannedTextFragmentInfo>) + pub fn transform(&self, size: LogicalSize<Au>, info: SpecificFragmentInfo) -> Fragment { let new_border_box = LogicalRect::from_point_size(self.style.writing_mode, self.border_box.start, size); - info.content_size = size.clone(); - Fragment { node: self.node, style: self.style.clone(), @@ -793,12 +802,37 @@ impl Fragment { border_box: new_border_box, border_padding: self.border_padding, margin: self.margin, - specific: SpecificFragmentInfo::ScannedText(info), + specific: info, inline_context: self.inline_context.clone(), debug_id: self.debug_id, } } + /// Transforms this fragment using the given `SplitInfo`, preserving all the other data. + pub fn transform_with_split_info(&self, + split: &SplitInfo, + text_run: Arc<Box<TextRun>>) + -> Fragment { + let size = LogicalSize::new(self.style.writing_mode, + split.inline_size, + self.border_box.size.block); + let info = box ScannedTextFragmentInfo::new(text_run, split.range, Vec::new(), size); + self.transform(size, SpecificFragmentInfo::ScannedText(info)) + } + + /// Transforms this fragment into an ellipsis fragment, preserving all the other data. + pub fn transform_into_ellipsis(&self, layout_context: &LayoutContext) -> Fragment { + let mut unscanned_ellipsis_fragments = DList::new(); + unscanned_ellipsis_fragments.push_back(self.transform( + self.border_box.size, + SpecificFragmentInfo::UnscannedText(UnscannedTextFragmentInfo::from_text( + "…".to_owned())))); + let ellipsis_fragments = TextRunScanner::new().scan_for_runs(layout_context.font_context(), + unscanned_ellipsis_fragments); + debug_assert!(ellipsis_fragments.len() == 1); + ellipsis_fragments.fragments.into_iter().next().unwrap() + } + pub fn restyle_damage(&self) -> RestyleDamage { self.restyle_damage | self.specific.restyle_damage() } @@ -1353,6 +1387,36 @@ impl Fragment { } } + /// 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> { + let text_fragment_info = + if let SpecificFragmentInfo::ScannedText(ref text_fragment_info) = self.specific { + text_fragment_info + } else { + return None + }; + + let character_breaking_strategy = + text_fragment_info.run.character_slices_in_range(&text_fragment_info.range); + match self.calculate_split_position_using_breaking_strategy(character_breaking_strategy, + max_inline_size, + SplitOptions::empty()) { + None => None, + Some(split_info) => { + match split_info.inline_start { + None => None, + Some(split) => { + Some(TruncationResult { + split: split, + text_run: split_info.text_run.clone(), + }) + } + } + } + } + } + /// A helper method that uses the breaking strategy described by `slice_iterator` (at present, /// either natural word breaking or character breaking) to split this fragment. fn calculate_split_position_using_breaking_strategy<'a,I>(&self, diff --git a/components/layout/inline.rs b/components/layout/inline.rs index aadcd1b310e..ac10e813e77 100644 --- a/components/layout/inline.rs +++ b/components/layout/inline.rs @@ -34,7 +34,7 @@ use std::mem; use std::num::ToPrimitive; use std::ops::{Add, Sub, Mul, Div, Rem, Neg, Shl, Shr, Not, BitOr, BitAnd, BitXor}; use std::u16; -use style::computed_values::{text_align, vertical_align, white_space}; +use style::computed_values::{overflow, text_align, text_overflow, vertical_align, white_space}; use style::ComputedValues; use std::sync::Arc; @@ -289,7 +289,7 @@ impl LineBreaker { // `append_fragment_to_line_if_possible` and // `try_append_to_line_by_new_line` by adding another bit in the reflow // flags. - if !self.try_append_to_line_by_new_line(fragment) { + if !self.try_append_to_line_by_new_line(layout_context, fragment) { self.flush_current_line() } } @@ -479,7 +479,10 @@ impl LineBreaker { /// Tries to append the given fragment to the line for `pre`-formatted text, splitting it if /// necessary. Returns true if we successfully pushed the fragment to the line or false if we /// couldn't. - fn try_append_to_line_by_new_line(&mut self, in_fragment: Fragment) -> bool { + fn try_append_to_line_by_new_line(&mut self, + layout_context: &LayoutContext, + in_fragment: Fragment) + -> bool { let should_push = match in_fragment.newline_positions() { None => true, Some(ref positions) => positions.is_empty(), @@ -487,7 +490,7 @@ impl LineBreaker { if should_push { debug!("LineBreaker: did not find a newline character; pushing the fragment to \ the line without splitting"); - self.push_fragment_to_line(in_fragment); + self.push_fragment_to_line(layout_context, in_fragment); return true } @@ -498,16 +501,16 @@ impl LineBreaker { .expect("LineBreaker: this split case makes no sense!"); let writing_mode = self.floats.writing_mode; - let split_fragment = |&:split: SplitInfo| { + let split_fragment = |&: split: SplitInfo| { + let size = LogicalSize::new(writing_mode, + split.inline_size, + in_fragment.border_box.size.block); let info = box ScannedTextFragmentInfo::new(run.clone(), split.range, (*in_fragment.newline_positions() .unwrap()).clone(), - in_fragment.border_box.size); - let size = LogicalSize::new(writing_mode, - split.inline_size, - in_fragment.border_box.size.block); - in_fragment.transform(size, info) + size); + in_fragment.transform(size, SpecificFragmentInfo::ScannedText(info)) }; debug!("LineBreaker: Pushing the fragment to the inline_start of the new-line character \ @@ -515,7 +518,7 @@ impl LineBreaker { let mut inline_start = split_fragment(inline_start); inline_start.save_new_line_pos(); *inline_start.newline_positions_mut().unwrap() = vec![]; - self.push_fragment_to_line(inline_start); + self.push_fragment_to_line(layout_context, inline_start); for inline_end in inline_end.into_iter() { debug!("LineBreaker: Deferring the fragment to the inline_end of the new-line \ @@ -567,7 +570,7 @@ impl LineBreaker { fragment.border_box.size.inline + indentation; if new_inline_size <= green_zone.inline { debug!("LineBreaker: fragment fits without splitting"); - self.push_fragment_to_line(fragment); + self.push_fragment_to_line(layout_context, fragment); return true } @@ -577,47 +580,41 @@ impl LineBreaker { flags.contains(NO_WRAP_INLINE_REFLOW_FLAG) { debug!("LineBreaker: fragment can't split and line {} is empty, so overflowing", self.lines.len()); - self.push_fragment_to_line(fragment); + self.push_fragment_to_line(layout_context, fragment); return true } // Split it up! let available_inline_size = green_zone.inline - self.pending_line.bounds.size.inline - indentation; - let (inline_start_fragment, inline_end_fragment) = - match fragment.calculate_split_position(available_inline_size, - self.pending_line_is_empty()) { - None => { - debug!("LineBreaker: fragment was unsplittable; deferring to next line: {:?}", - fragment); - self.work_list.push_front(fragment); - return false - } - Some(split_result) => { - let split_fragment = |&:split: SplitInfo| { - let info = box ScannedTextFragmentInfo::new(split_result.text_run.clone(), - split.range, - Vec::new(), - fragment.border_box.size); - let size = LogicalSize::new(self.floats.writing_mode, - split.inline_size, - fragment.border_box.size.block); - fragment.transform(size, info) - }; - (split_result.inline_start.as_ref().map(|x| split_fragment(x.clone())), - split_result.inline_end.as_ref().map(|x| split_fragment(x.clone()))) - } - }; + let inline_start_fragment; + let inline_end_fragment; + let split_result = match fragment.calculate_split_position(available_inline_size, + self.pending_line_is_empty()) { + None => { + debug!("LineBreaker: fragment was unsplittable; deferring to next line"); + self.work_list.push_front(fragment); + return false + } + Some(split_result) => split_result, + }; + + inline_start_fragment = split_result.inline_start.as_ref().map(|x| { + fragment.transform_with_split_info(x, split_result.text_run.clone()) + }); + inline_end_fragment = split_result.inline_end.as_ref().map(|x| { + fragment.transform_with_split_info(x, split_result.text_run.clone()) + }); // Push the first fragment onto the line we're working on and start off the next line with // the second fragment. If there's no second fragment, the next line will start off empty. match (inline_start_fragment, inline_end_fragment) { (Some(inline_start_fragment), Some(inline_end_fragment)) => { - self.push_fragment_to_line(inline_start_fragment); + self.push_fragment_to_line(layout_context, inline_start_fragment); self.work_list.push_front(inline_end_fragment) }, (Some(fragment), None) | (None, Some(fragment)) => { - self.push_fragment_to_line(fragment) + self.push_fragment_to_line(layout_context, fragment) } (None, None) => {} } @@ -625,8 +622,9 @@ impl LineBreaker { true } - /// Pushes a fragment to the current line unconditionally. - fn push_fragment_to_line(&mut self, fragment: Fragment) { + /// Pushes a fragment to the current line unconditionally, possibly truncating it and placing + /// an ellipsis based on the value of `text-overflow`. + fn push_fragment_to_line(&mut self, layout_context: &LayoutContext, fragment: Fragment) { let indentation = self.indentation_for_pending_fragment(); if self.pending_line_is_empty() { assert!(self.new_fragments.len() <= (u16::MAX as uint)); @@ -634,6 +632,39 @@ impl LineBreaker { FragmentIndex(0)); } + // Determine if an ellipsis will be necessary to account for `text-overflow`. + let mut need_ellipsis = false; + let available_inline_size = self.pending_line.green_zone.inline - + self.pending_line.bounds.size.inline - indentation; + match (fragment.style().get_inheritedtext().text_overflow, + fragment.style().get_box().overflow) { + (text_overflow::T::clip, _) | (_, overflow::T::visible) => {} + (text_overflow::T::ellipsis, _) => { + need_ellipsis = fragment.border_box.size.inline > available_inline_size; + } + } + + if !need_ellipsis { + self.push_fragment_to_line_ignoring_text_overflow(fragment); + return + } + + let ellipsis = fragment.transform_into_ellipsis(layout_context); + if let Some(truncation_info) = + fragment.truncate_to_inline_size(available_inline_size - + ellipsis.border_box.size.inline) { + let fragment = fragment.transform_with_split_info(&truncation_info.split, + truncation_info.text_run); + self.push_fragment_to_line_ignoring_text_overflow(fragment); + } + self.push_fragment_to_line_ignoring_text_overflow(ellipsis); + } + + /// Pushes a fragment to the current line unconditionally, without placing an ellipsis in the + /// case of `text-overflow: ellipsis`. + fn push_fragment_to_line_ignoring_text_overflow(&mut self, fragment: Fragment) { + let indentation = self.indentation_for_pending_fragment(); + self.pending_line.range.extend_by(FragmentIndex(1)); self.pending_line.bounds.size.inline = self.pending_line.bounds.size.inline + fragment.border_box.size.inline + diff --git a/components/layout/text.rs b/components/layout/text.rs index ca23fdc12de..7727c59a7c6 100644 --- a/components/layout/text.rs +++ b/components/layout/text.rs @@ -194,16 +194,15 @@ impl TextRunScanner { mem::replace(&mut self.clump, DList::new()).into_iter().enumerate() { let range = *new_ranges.get(logical_offset); if range.is_empty() { - debug!("Elided an `SpecificFragmentInfo::UnscannedText` because it was zero-length after \ - compression; {:?}", - old_fragment); + debug!("Elided an `SpecificFragmentInfo::UnscannedText` because it was \ + zero-length after compression"); continue } let text_size = old_fragment.border_box.size; let &mut NewLinePositions(ref mut new_line_positions) = new_line_positions.get_mut(logical_offset); - let new_text_fragment_info = + let mut new_text_fragment_info = box ScannedTextFragmentInfo::new(run.clone(), range, mem::replace(new_line_positions, Vec::new()), @@ -211,7 +210,10 @@ impl TextRunScanner { let new_metrics = new_text_fragment_info.run.metrics_for_range(&range); let bounding_box_size = bounding_box_for_run_metrics(&new_metrics, old_fragment.style.writing_mode); - let new_fragment = old_fragment.transform(bounding_box_size, new_text_fragment_info); + new_text_fragment_info.content_size = bounding_box_size; + let new_fragment = + old_fragment.transform(bounding_box_size, + SpecificFragmentInfo::ScannedText(new_text_fragment_info)); out_fragments.push(new_fragment) } diff --git a/components/layout/wrapper.rs b/components/layout/wrapper.rs index d54af90014a..da89ab15e4a 100644 --- a/components/layout/wrapper.rs +++ b/components/layout/wrapper.rs @@ -40,8 +40,9 @@ use util::{PrivateLayoutData}; use cssparser::RGBA; use gfx::display_list::OpaqueNode; use script::dom::bindings::codegen::InheritTypes::{ElementCast, HTMLIFrameElementCast}; -use script::dom::bindings::codegen::InheritTypes::{HTMLCanvasElementCast, HTMLImageElementCast, HTMLInputElementCast}; -use script::dom::bindings::codegen::InheritTypes::{HTMLTextAreaElementCast, NodeCast, TextCast}; +use script::dom::bindings::codegen::InheritTypes::{HTMLCanvasElementCast, HTMLImageElementCast}; +use script::dom::bindings::codegen::InheritTypes::{HTMLInputElementCast, HTMLTextAreaElementCast}; +use script::dom::bindings::codegen::InheritTypes::{NodeCast, TextCast}; use script::dom::bindings::js::JS; use script::dom::element::{Element, ElementTypeId}; use script::dom::element::{LayoutElementHelpers, RawLayoutElementHelpers}; @@ -58,6 +59,8 @@ use script::dom::text::Text; use script::layout_interface::LayoutChan; use servo_msg::constellation_msg::{PipelineId, SubpageId}; use servo_util::str::{LengthOrPercentageOrAuto, is_whitespace}; +use std::borrow::ToOwned; +use std::cell::{Ref, RefMut}; use std::marker::ContravariantLifetime; use std::mem; use std::sync::mpsc::Sender; @@ -68,9 +71,6 @@ use style::{LengthAttribute, PropertyDeclarationBlock, SimpleColorAttribute}; use style::{TElement, TElementAttributes, TNode, UnsignedIntegerAttribute}; use url::Url; -use std::borrow::ToOwned; -use std::cell::{Ref, RefMut}; - /// Allows some convenience methods on generic layout nodes. pub trait TLayoutNode { /// Creates a new layout node with the same lifetime as this layout node. diff --git a/components/script/dom/webidls/CSSStyleDeclaration.webidl b/components/script/dom/webidls/CSSStyleDeclaration.webidl index afe511b7e1f..88c78b72775 100644 --- a/components/script/dom/webidls/CSSStyleDeclaration.webidl +++ b/components/script/dom/webidls/CSSStyleDeclaration.webidl @@ -116,6 +116,7 @@ partial interface CSSStyleDeclaration { [TreatNullAs=EmptyString] attribute DOMString wordBreak; [TreatNullAs=EmptyString] attribute DOMString wordSpacing; [TreatNullAs=EmptyString] attribute DOMString wordWrap; + [TreatNullAs=EmptyString] attribute DOMString textOverflow; [TreatNullAs=EmptyString] attribute DOMString textAlign; [TreatNullAs=EmptyString] attribute DOMString textDecoration; diff --git a/components/style/properties/mod.rs.mako b/components/style/properties/mod.rs.mako index c4833972501..b3283b894d8 100644 --- a/components/style/properties/mod.rs.mako +++ b/components/style/properties/mod.rs.mako @@ -1213,6 +1213,8 @@ pub mod longhands { // TODO(pcwalton): Support `word-break: keep-all` once we have better CJK support. ${single_keyword("word-break", "normal break-all")} + ${single_keyword("text-overflow", "clip ellipsis")} + ${new_style_struct("Text", is_inherited=False)} <%self:longhand name="text-decoration"> diff --git a/tests/ref/basic.list b/tests/ref/basic.list index f4af79d356f..f4097c69c36 100644 --- a/tests/ref/basic.list +++ b/tests/ref/basic.list @@ -232,3 +232,4 @@ fragment=top != ../html/acid2.html acid2_ref.html == filter_opacity_a.html filter_opacity_ref.html == filter_sepia_a.html filter_sepia_ref.html == mix_blend_mode_a.html mix_blend_mode_ref.html +!= text_overflow_a.html text_overflow_ref.html diff --git a/tests/ref/text_overflow_a.html b/tests/ref/text_overflow_a.html new file mode 100644 index 00000000000..70818c2b914 --- /dev/null +++ b/tests/ref/text_overflow_a.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<style> +p { + width: 128px; + background: gold; + text-overflow: ellipsis; +} +#hideme { + overflow: hidden; +} +</style> +</head> +<body> +<p id=hideme>mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm</p> +<p>mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm</p> +</body> +</html> + diff --git a/tests/ref/text_overflow_ref.html b/tests/ref/text_overflow_ref.html new file mode 100644 index 00000000000..19adf5d9104 --- /dev/null +++ b/tests/ref/text_overflow_ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<style> +p { + width: 128px; + background: gold; +} +</style> +</head> +<body> +<p>mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm</p> +<p>mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm</p> +</body> +</html> + + |