diff --git a/components/layout_2020/display_list/mod.rs b/components/layout_2020/display_list/mod.rs index 8a746a01184..5879733ac45 100644 --- a/components/layout_2020/display_list/mod.rs +++ b/components/layout_2020/display_list/mod.rs @@ -5,7 +5,7 @@ use std::cell::{OnceCell, RefCell}; use std::sync::Arc; -use app_units::Au; +use app_units::{AU_PER_PX, Au}; use base::WebRenderEpochToU16; use base::id::ScrollTreeNodeId; use compositing_traits::display_list::{AxesScrollSensitivity, CompositorDisplayListInfo}; @@ -13,6 +13,7 @@ use embedder_traits::Cursor; use euclid::{Point2D, SideOffsets2D, Size2D, UnknownUnit}; use fonts::GlyphStore; use gradient::WebRenderGradient; +use range::Range as ServoRange; use servo_geometry::MaxRect; use style::Zero; use style::color::{AbsoluteColor, ColorSpace}; @@ -68,6 +69,7 @@ pub struct WebRenderImageInfo { // webrender's `ItemTag` is private. type ItemTag = (u64, u16); type HitInfo = Option; +const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(AU_PER_PX); /// Where the information that's used to build display lists is stored. This /// includes both a [wr::DisplayListBuilder] for building up WebRender-specific @@ -388,6 +390,7 @@ impl Fragment { &fragment.glyphs, baseline_origin, fragment.justification_adjustment, + !fragment.has_selection(), ); if glyphs.is_empty() { return; @@ -424,7 +427,6 @@ impl Fragment { ); } - // Underline. if fragment .text_decoration_line .contains(TextDecorationLine::UNDERLINE) @@ -435,7 +437,6 @@ impl Fragment { self.build_display_list_for_text_decoration(fragment, builder, &rect, &color); } - // Overline. if fragment .text_decoration_line .contains(TextDecorationLine::OVERLINE) @@ -445,7 +446,70 @@ impl Fragment { self.build_display_list_for_text_decoration(fragment, builder, &rect, &color); } - // Text. + // TODO: This caret/text selection implementation currently does not account for vertical text + // and RTL text properly. + if let Some(range) = fragment.selection_range { + let baseline_origin = rect.origin; + if !range.is_empty() { + let start = glyphs_advance_by_index( + &fragment.glyphs, + range.begin(), + baseline_origin, + fragment.justification_adjustment, + ); + + let end = glyphs_advance_by_index( + &fragment.glyphs, + range.end(), + baseline_origin, + fragment.justification_adjustment, + ); + + let selection_rect = LayoutRect::new( + Point2D::new(start.x.to_f32_px(), containing_block.min_y().to_f32_px()), + Point2D::new(end.x.to_f32_px(), containing_block.max_y().to_f32_px()), + ); + if let Some(selection_color) = fragment + .selected_style + .clone_background_color() + .as_absolute() + { + let selection_common = + builder.common_properties(selection_rect, &fragment.parent_style); + builder.wr().push_rect( + &selection_common, + selection_rect, + rgba(*selection_color), + ); + } + } else { + let insertion_point = glyphs_advance_by_index( + &fragment.glyphs, + range.begin(), + baseline_origin, + fragment.justification_adjustment, + ); + + let insertion_point_rect = LayoutRect::new( + Point2D::new( + insertion_point.x.to_f32_px(), + containing_block.min_y().to_f32_px(), + ), + Point2D::new( + insertion_point.x.to_f32_px() + INSERTION_POINT_LOGICAL_WIDTH.to_f32_px(), + containing_block.max_y().to_f32_px(), + ), + ); + let insertion_point_common = + builder.common_properties(insertion_point_rect, &fragment.parent_style); + // TODO: The color of the caret is currently hardcoded to the text color. + // We should be retrieving the caret color from the style properly. + builder + .wr() + .push_rect(&insertion_point_common, insertion_point_rect, rgba(color)); + } + } + builder.wr().push_text( &common, rect.to_webrender(), @@ -455,7 +519,6 @@ impl Fragment { None, ); - // Line-through. if fragment .text_decoration_line .contains(TextDecorationLine::LINE_THROUGH) @@ -1130,6 +1193,7 @@ fn glyphs( glyph_runs: &[Arc], mut baseline_origin: PhysicalPoint, justification_adjustment: Au, + ignore_whitespace: bool, ) -> Vec { use fonts_traits::ByteIndex; use range::Range; @@ -1137,7 +1201,7 @@ fn glyphs( let mut glyphs = vec![]; for run in glyph_runs { for glyph in run.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), run.len())) { - if !run.is_whitespace() { + if !run.is_whitespace() || !ignore_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(), @@ -1159,6 +1223,26 @@ fn glyphs( glyphs } +// TODO: This implementation has not been tested against `TextFragment` with mutiple runs. +// It is possible that the `glyphs` function above will need to be modified to +// handle multiple runs correctly. +fn glyphs_advance_by_index( + glyph_runs: &[Arc], + index: fonts_traits::ByteIndex, + baseline_origin: PhysicalPoint, + justification_adjustment: Au, +) -> PhysicalPoint { + let mut point = baseline_origin; + for run in glyph_runs { + let total_advance = run.advance_for_byte_range( + &ServoRange::new(fonts::ByteIndex(0), index), + justification_adjustment, + ); + point.x += total_advance; + } + point +} + fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor { match kind { CursorKind::Auto => auto_cursor, diff --git a/components/layout_2020/dom_traversal.rs b/components/layout_2020/dom_traversal.rs index 5800637c50f..1b9258fb824 100644 --- a/components/layout_2020/dom_traversal.rs +++ b/components/layout_2020/dom_traversal.rs @@ -5,7 +5,9 @@ use std::borrow::Cow; use std::iter::FusedIterator; +use fonts::ByteIndex; use html5ever::{LocalName, local_name}; +use range::Range; use script_layout_interface::wrapper_traits::{ThreadSafeLayoutElement, ThreadSafeLayoutNode}; use script_layout_interface::{LayoutElementType, LayoutNodeType}; use selectors::Element as SelectorsElement; @@ -74,6 +76,14 @@ impl<'dom, Node: NodeExt<'dom>> NodeAndStyleInfo { style, }) } + + pub(crate) fn get_selected_style(&self) -> ServoArc { + self.node.to_threadsafe().selected_style() + } + + pub(crate) fn get_selection_range(&self) -> Option> { + self.node.to_threadsafe().selection() + } } impl<'dom, Node> From<&NodeAndStyleInfo> for BaseFragmentInfo diff --git a/components/layout_2020/flow/inline/construct.rs b/components/layout_2020/flow/inline/construct.rs index 9452d3ffb33..38c0767b577 100644 --- a/components/layout_2020/flow/inline/construct.rs +++ b/components/layout_2020/flow/inline/construct.rs @@ -245,6 +245,9 @@ impl InlineFormattingContextBuilder { return; } + let selection_range = info.get_selection_range(); + let selected_style = info.get_selected_style(); + if let Some(last_character) = new_text.chars().next_back() { self.on_word_boundary = last_character.is_whitespace(); self.last_inline_box_ended_with_collapsible_white_space = @@ -264,7 +267,13 @@ impl InlineFormattingContextBuilder { self.inline_items .push(ArcRefCell::new(InlineItem::TextRun(ArcRefCell::new( - TextRun::new(info.into(), info.style.clone(), new_range), + TextRun::new( + info.into(), + info.style.clone(), + new_range, + selection_range, + selected_style, + ), )))); } diff --git a/components/layout_2020/flow/inline/line.rs b/components/layout_2020/flow/inline/line.rs index 420d79ead9f..eb1dd536466 100644 --- a/components/layout_2020/flow/inline/line.rs +++ b/components/layout_2020/flow/inline/line.rs @@ -4,8 +4,9 @@ use app_units::Au; use bitflags::bitflags; -use fonts::{FontMetrics, GlyphStore}; +use fonts::{ByteIndex, FontMetrics, GlyphStore}; use itertools::Either; +use range::Range; use servo_arc::Arc; use style::Zero; use style::computed_values::position::T as Position; @@ -576,6 +577,8 @@ impl LineItemLayout<'_, '_> { glyphs: text_item.text, text_decoration_line: text_item.text_decoration_line, justification_adjustment: self.justification_adjustment, + selection_range: text_item.selection_range, + selected_style: text_item.selected_style, })), content_rect, )); @@ -768,6 +771,8 @@ pub(super) struct TextRunLineItem { pub text_decoration_line: TextDecorationLine, /// The BiDi level of this [`TextRunLineItem`] to enable reordering. pub bidi_level: Level, + pub selection_range: Option>, + pub selected_style: Arc, } impl TextRunLineItem { diff --git a/components/layout_2020/flow/inline/mod.rs b/components/layout_2020/flow/inline/mod.rs index ee22830aa9e..834064cd3e2 100644 --- a/components/layout_2020/flow/inline/mod.rs +++ b/components/layout_2020/flow/inline/mod.rs @@ -81,13 +81,14 @@ use std::rc::Rc; use app_units::{Au, MAX_AU}; use bitflags::bitflags; use construct::InlineFormattingContextBuilder; -use fonts::{FontMetrics, GlyphStore}; +use fonts::{ByteIndex, FontMetrics, GlyphStore}; use inline_box::{InlineBox, InlineBoxContainerState, InlineBoxIdentifier, InlineBoxes}; use line::{ AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, LineItem, LineItemLayout, TextRunLineItem, }; use line_breaker::LineBreaker; +use range::Range; use servo_arc::Arc; use style::Zero; use style::computed_values::text_wrap_mode::T as TextWrapMode; @@ -1288,6 +1289,7 @@ impl InlineFormattingContextLayout<'_> { text_run: &TextRun, font_index: usize, bidi_level: Level, + range: range::Range, ) { let inline_advance = glyph_store.total_advance(); let flags = if glyph_store.is_whitespace() { @@ -1344,6 +1346,33 @@ impl InlineFormattingContextLayout<'_> { _ => {}, } + let selection_range = if let Some(selection) = &text_run.selection_range { + let intersection = selection.intersect(&range); + if intersection.is_empty() { + let insertion_point_index = selection.begin(); + // We only allow the caret to be shown in the start of the fragment if it is the first fragment. + // Otherwise this will cause duplicate caret, especially apparent when encountered line break. + if insertion_point_index >= range.begin() && + insertion_point_index <= range.end() && + (range.begin() != insertion_point_index || range.begin().0 == 0) + { + Some(Range::new( + insertion_point_index - range.begin(), + ByteIndex(0), + )) + } else { + None + } + } else { + Some(Range::new( + intersection.begin() - range.begin(), + intersection.length(), + )) + } + } else { + None + }; + self.push_line_item_to_unbreakable_segment(LineItem::TextRun( current_inline_box_identifier, TextRunLineItem { @@ -1354,6 +1383,8 @@ impl InlineFormattingContextLayout<'_> { font_key: ifc_font_info.key, text_decoration_line: self.current_inline_container_state().text_decoration_line, bidi_level, + selection_range, + selected_style: text_run.selected_style.clone(), }, )); } diff --git a/components/layout_2020/flow/inline/text_run.rs b/components/layout_2020/flow/inline/text_run.rs index ead6c394131..d36834bd80e 100644 --- a/components/layout_2020/flow/inline/text_run.rs +++ b/components/layout_2020/flow/inline/text_run.rs @@ -46,6 +46,8 @@ pub(crate) struct TextRun { /// The text of this [`TextRun`] with a font selected, broken into unbreakable /// segments, and shaped. pub shaped_text: Vec, + pub selection_range: Option>, + pub selected_style: Arc, } // There are two reasons why we might want to break at the start: @@ -140,6 +142,7 @@ impl TextRunSegment { soft_wrap_policy = SegmentStartSoftWrapPolicy::Force; } + let mut byte_processed = ByteIndex(0); for (run_index, run) in self.runs.iter().enumerate() { ifc.possibly_flush_deferred_forced_line_break(); @@ -147,6 +150,7 @@ impl TextRunSegment { // see any content. We don't line break immediately, because we'd like to finish processing // any ongoing inline boxes before ending the line. if run.is_single_preserved_newline() { + byte_processed = byte_processed + run.range.length(); ifc.defer_forced_line_break(); continue; } @@ -160,7 +164,12 @@ impl TextRunSegment { text_run, self.font_index, self.bidi_level, + ServoRange::::new( + byte_processed + ByteIndex(self.range.start as isize), + run.range.length(), + ), ); + byte_processed = byte_processed + run.range.length(); } } @@ -327,12 +336,16 @@ impl TextRun { base_fragment_info: BaseFragmentInfo, parent_style: Arc, text_range: Range, + selection_range: Option>, + selected_style: Arc, ) -> Self { Self { base_fragment_info, parent_style, text_range, shaped_text: Vec::new(), + selection_range, + selected_style, } } diff --git a/components/layout_2020/fragment_tree/fragment.rs b/components/layout_2020/fragment_tree/fragment.rs index fbc95ce3d5a..f8ba80369ed 100644 --- a/components/layout_2020/fragment_tree/fragment.rs +++ b/components/layout_2020/fragment_tree/fragment.rs @@ -7,7 +7,8 @@ use std::sync::Arc; use app_units::Au; use base::id::PipelineId; use base::print_tree::PrintTree; -use fonts::{FontMetrics, GlyphStore}; +use fonts::{ByteIndex, FontMetrics, GlyphStore}; +use range::Range as ServoRange; use servo_arc::Arc as ServoArc; use style::Zero; use style::properties::ComputedValues; @@ -71,6 +72,8 @@ pub(crate) struct TextFragment { /// Extra space to add for each justification opportunity. pub justification_adjustment: Au, + pub selection_range: Option>, + pub selected_style: ServoArc, } pub(crate) struct ImageFragment { @@ -221,6 +224,10 @@ impl TextFragment { self.rect, )); } + + pub fn has_selection(&self) -> bool { + self.selection_range.is_some() + } } impl ImageFragment { diff --git a/components/shared/script_layout/wrapper_traits.rs b/components/shared/script_layout/wrapper_traits.rs index 20818bda9f5..be27050a42f 100644 --- a/components/shared/script_layout/wrapper_traits.rs +++ b/components/shared/script_layout/wrapper_traits.rs @@ -212,7 +212,7 @@ pub trait ThreadSafeLayoutNode<'dom>: Clone + Copy + Debug + NodeInfo + PartialE fn node_text_content(self) -> Cow<'dom, str>; - /// If the insertion point is within this node, returns it. Otherwise, returns `None`. + /// If selection intersects this node, return it. Otherwise, returns `None`. fn selection(&self) -> Option>; /// If this is an image element, returns its URL. If this is not an image element, fails. diff --git a/tests/wpt/meta/css/css-overflow/text-overflow-ellipsis-editing-input.html.ini b/tests/wpt/meta/css/css-overflow/text-overflow-ellipsis-editing-input.html.ini new file mode 100644 index 00000000000..ecad87f774b --- /dev/null +++ b/tests/wpt/meta/css/css-overflow/text-overflow-ellipsis-editing-input.html.ini @@ -0,0 +1,2 @@ +[text-overflow-ellipsis-editing-input.html] + expected: FAIL