From b40db5b55d5035f3cae3078d0c92f75b6b49f9fd Mon Sep 17 00:00:00 2001 From: Florian Merz Date: Wed, 11 Jan 2017 09:04:03 +0100 Subject: [PATCH] Position insertion point in input field with mouse --- components/gfx/display_list/mod.rs | 144 ++++++++++++------ components/gfx/text/glyph.rs | 21 +++ components/gfx/text/text_run.rs | 15 ++ components/layout/query.rs | 16 +- components/layout_thread/lib.rs | 24 ++- components/script/dom/htmlinputelement.rs | 31 ++++ components/script/dom/window.rs | 12 +- components/script_layout_interface/message.rs | 1 + components/script_layout_interface/rpc.rs | 5 + 9 files changed, 219 insertions(+), 50 deletions(-) diff --git a/components/gfx/display_list/mod.rs b/components/gfx/display_list/mod.rs index a2cb83c5cf2..fb6aa952440 100644 --- a/components/gfx/display_list/mod.rs +++ b/components/gfx/display_list/mod.rs @@ -48,10 +48,74 @@ pub struct DisplayList { } impl DisplayList { + // Returns the text index within a node for the point of interest. + pub fn text_index(&self, + node: OpaqueNode, + client_point: &Point2D, + scroll_offsets: &ScrollOffsetMap) + -> Option { + let mut result = Vec::new(); + let mut translated_point = client_point.clone(); + let mut traversal = DisplayListTraversal::new(self); + self.text_index_contents(node, + &mut traversal, + &mut translated_point, + client_point, + scroll_offsets, + &mut result); + result.pop() + } + + pub fn text_index_contents<'a>(&self, + node: OpaqueNode, + traversal: &mut DisplayListTraversal<'a>, + translated_point: &mut Point2D, + client_point: &Point2D, + scroll_offsets: &ScrollOffsetMap, + result: &mut Vec) { + while let Some(item) = traversal.next() { + match item { + &DisplayItem::PushStackingContext(ref stacking_context_item) => { + DisplayList::translate_point(&stacking_context_item.stacking_context, + translated_point, + client_point); + self.text_index_contents(node, + traversal, + translated_point, + client_point, + scroll_offsets, + result); + } + &DisplayItem::PushScrollRoot(ref item) => { + DisplayList::scroll_root(&item.scroll_root, + translated_point, + scroll_offsets); + self.text_index_contents(node, + traversal, + translated_point, + client_point, + scroll_offsets, + result); + + }, + &DisplayItem::PopStackingContext(_) => return, + &DisplayItem::Text(ref text) => { + let base = item.base(); + if base.metadata.node == node { + let offset = *translated_point - text.baseline_origin; + let index = text.text_run.range_index_of_advance(&text.range, offset.x); + result.push(index); + } + }, + _ => {}, + } + } + } + // Return all nodes containing the point of interest, bottommost first, and // respecting the `pointer-events` CSS property. pub fn hit_test(&self, - translated_point: &Point2D, + translated_point: &mut Point2D, client_point: &Point2D, scroll_offsets: &ScrollOffsetMap) -> Vec { @@ -67,27 +131,31 @@ impl DisplayList { pub fn hit_test_contents<'a>(&self, traversal: &mut DisplayListTraversal<'a>, - translated_point: &Point2D, + translated_point: &mut Point2D, client_point: &Point2D, scroll_offsets: &ScrollOffsetMap, result: &mut Vec) { while let Some(item) = traversal.next() { match item { &DisplayItem::PushStackingContext(ref stacking_context_item) => { - self.hit_test_stacking_context(traversal, - &stacking_context_item.stacking_context, - translated_point, - client_point, - scroll_offsets, - result); + DisplayList::translate_point(&stacking_context_item.stacking_context, + translated_point, + client_point); + self.hit_test_contents(traversal, + translated_point, + client_point, + scroll_offsets, + result); } &DisplayItem::PushScrollRoot(ref item) => { - self.hit_test_scroll_root(traversal, - &item.scroll_root, - *translated_point, - client_point, - scroll_offsets, - result); + DisplayList::scroll_root(&item.scroll_root, + translated_point, + scroll_offsets); + self.hit_test_contents(traversal, + translated_point, + client_point, + scroll_offsets, + result); } &DisplayItem::PopStackingContext(_) | &DisplayItem::PopScrollRoot(_) => return, _ => { @@ -99,38 +167,15 @@ impl DisplayList { } } - fn hit_test_scroll_root<'a>(&self, - traversal: &mut DisplayListTraversal<'a>, - scroll_root: &ScrollRoot, - mut translated_point: Point2D, - client_point: &Point2D, - scroll_offsets: &ScrollOffsetMap, - result: &mut Vec) { - // Adjust the translated point to account for the scroll offset if - // necessary. This can only happen when WebRender is in use. - // - // We don't perform this adjustment on the root stacking context because - // the DOM-side code has already translated the point for us (e.g. in - // `Window::hit_test_query()`) by now. - if let Some(scroll_offset) = scroll_offsets.get(&scroll_root.id) { - translated_point.x -= Au::from_f32_px(scroll_offset.x); - translated_point.y -= Au::from_f32_px(scroll_offset.y); - } - self.hit_test_contents(traversal, &translated_point, client_point, scroll_offsets, result); - } - - fn hit_test_stacking_context<'a>(&self, - traversal: &mut DisplayListTraversal<'a>, - stacking_context: &StackingContext, - translated_point: &Point2D, - client_point: &Point2D, - scroll_offsets: &ScrollOffsetMap, - result: &mut Vec) { + #[inline] + fn translate_point<'a>(stacking_context: &StackingContext, + translated_point: &mut Point2D, + client_point: &Point2D) { // Convert the parent translated point into stacking context local transform space if the // stacking context isn't fixed. If it's fixed, we need to use the client point anyway. debug_assert!(stacking_context.context_type == StackingContextType::Real); let is_fixed = stacking_context.scroll_policy == ScrollPolicy::Fixed; - let translated_point = if is_fixed { + *translated_point = if is_fixed { *client_point } else { let point = *translated_point - stacking_context.bounds.origin; @@ -139,8 +184,21 @@ impl DisplayList { point.y.to_f32_px())); Point2D::new(Au::from_f32_px(frac_point.x), Au::from_f32_px(frac_point.y)) }; + } - self.hit_test_contents(traversal, &translated_point, client_point, scroll_offsets, result); + #[inline] + fn scroll_root<'a>(scroll_root: &ScrollRoot, + translated_point: &mut Point2D, + scroll_offsets: &ScrollOffsetMap) { + // Adjust the translated point to account for the scroll offset if necessary. + // + // We don't perform this adjustment on the root stacking context because + // the DOM-side code has already translated the point for us (e.g. in + // `Window::hit_test_query()`) by now. + if let Some(scroll_offset) = scroll_offsets.get(&scroll_root.id) { + translated_point.x -= Au::from_f32_px(scroll_offset.x); + translated_point.y -= Au::from_f32_px(scroll_offset.y); + } } pub fn print(&self) { diff --git a/components/gfx/text/glyph.rs b/components/gfx/text/glyph.rs index 5628547a417..73a66ff19bf 100644 --- a/components/gfx/text/glyph.rs +++ b/components/gfx/text/glyph.rs @@ -546,6 +546,27 @@ impl<'a> GlyphStore { } } + // Scan the glyphs for a given range until we reach a given advance. Returns the index + // and advance of the glyph in the range at the given advance, if reached. Otherwise, returns the + // the number of glyphs and the advance for the given range. + #[inline] + pub fn range_index_of_advance(&self, range: &Range, advance: Au, extra_word_spacing: Au) -> (usize, Au) { + let mut index = 0; + let mut current_advance = Au(0); + for glyph in self.iter_glyphs_for_byte_range(range) { + if glyph.char_is_space() { + current_advance += glyph.advance() + extra_word_spacing + } else { + current_advance += glyph.advance() + } + if current_advance > advance { + break; + } + index += 1; + } + (index, current_advance) + } + #[inline] pub fn advance_for_byte_range(&self, range: &Range, extra_word_spacing: Au) -> Au { if range.begin() == ByteIndex(0) && range.end() == self.len() { diff --git a/components/gfx/text/text_run.rs b/components/gfx/text/text_run.rs index db65dc49142..0f487a4b70e 100644 --- a/components/gfx/text/text_run.rs +++ b/components/gfx/text/text_run.rs @@ -304,6 +304,21 @@ impl<'a> TextRun { }) } + /// Returns the index in the range of the first glyph advancing over given advance + pub fn range_index_of_advance(&self, range: &Range, advance: Au) -> usize { + // TODO(Issue #199): alter advance direction for RTL + // TODO(Issue #98): using inter-char and inter-word spacing settings when measuring text + let mut remaining = advance; + self.natural_word_slices_in_range(range) + .map(|slice| { + let (slice_index, slice_advance) = + slice.glyphs.range_index_of_advance(&slice.range, remaining, self.extra_word_spacing); + remaining -= slice_advance; + slice_index + }) + .sum() + } + /// Returns an iterator that will iterate over all slices of glyphs that represent natural /// words in the given range. pub fn natural_word_slices_in_range(&'a self, range: &Range) diff --git a/components/layout/query.rs b/components/layout/query.rs index 4b076942a54..4068c3230bf 100644 --- a/components/layout/query.rs +++ b/components/layout/query.rs @@ -20,7 +20,7 @@ use script_layout_interface::rpc::{ContentBoxResponse, ContentBoxesResponse}; use script_layout_interface::rpc::{HitTestResponse, LayoutRPC}; use script_layout_interface::rpc::{MarginStyleResponse, NodeGeometryResponse}; use script_layout_interface::rpc::{NodeOverflowResponse, OffsetParentResponse}; -use script_layout_interface::rpc::{NodeScrollRootIdResponse, ResolvedStyleResponse}; +use script_layout_interface::rpc::{NodeScrollRootIdResponse, ResolvedStyleResponse, TextIndexResponse}; use script_layout_interface::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; use script_traits::LayoutMsg as ConstellationMsg; use script_traits::UntrustedNodeAddress; @@ -85,6 +85,9 @@ pub struct LayoutThreadData { /// Scroll offsets of stacking contexts. This will only be populated if WebRender is in use. pub stacking_context_scroll_offsets: ScrollOffsetMap, + + /// Index in a text fragment. We need this do determine the insertion point. + pub text_index_response: TextIndexResponse, } pub struct LayoutRPCImpl(pub Arc>); @@ -138,7 +141,7 @@ impl LayoutRPC for LayoutRPCImpl { fn nodes_from_point(&self, page_point: Point2D, client_point: Point2D) -> Vec { - let page_point = Point2D::new(Au::from_f32_px(page_point.x), + let mut page_point = Point2D::new(Au::from_f32_px(page_point.x), Au::from_f32_px(page_point.y)); let client_point = Point2D::new(Au::from_f32_px(client_point.x), Au::from_f32_px(client_point.y)); @@ -149,7 +152,7 @@ impl LayoutRPC for LayoutRPCImpl { let result = match rw_data.display_list { None => panic!("Tried to hit test without a DisplayList"), Some(ref display_list) => { - display_list.hit_test(&page_point, + display_list.hit_test(&mut page_point, &client_point, &rw_data.stacking_context_scroll_offsets) } @@ -206,6 +209,12 @@ impl LayoutRPC for LayoutRPCImpl { let rw_data = rw_data.lock().unwrap(); rw_data.margin_style_response.clone() } + + fn text_index(&self) -> TextIndexResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + rw_data.text_index_response.clone() + } } struct UnioningFragmentBorderBoxIterator { @@ -581,6 +590,7 @@ impl FragmentBorderBoxIterator for ParentOffsetBorderBoxIterator { } } + pub fn process_node_geometry_request(requested_node: N, layout_root: &mut Flow) -> Rect { let mut iterator = FragmentLocatingFragmentIterator::new(requested_node.opaque()); diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs index 133fcbf9093..6dc72bc86c3 100644 --- a/components/layout_thread/lib.rs +++ b/components/layout_thread/lib.rs @@ -91,6 +91,7 @@ use script::layout_wrapper::{ServoLayoutElement, ServoLayoutDocument, ServoLayou use script_layout_interface::message::{Msg, NewLayoutThreadInfo, Reflow, ReflowQueryType, ScriptReflow}; use script_layout_interface::reporter::CSSErrorReporter; use script_layout_interface::rpc::{LayoutRPC, MarginStyleResponse, NodeOverflowResponse, OffsetParentResponse}; +use script_layout_interface::rpc::TextIndexResponse; use script_layout_interface::wrapper_traits::LayoutNode; use script_traits::{ConstellationControlMsg, LayoutControlMsg, LayoutMsg as ConstellationMsg}; use script_traits::{StackingContextScrollState, UntrustedNodeAddress}; @@ -474,6 +475,7 @@ impl LayoutThread { offset_parent_response: OffsetParentResponse::empty(), margin_style_response: MarginStyleResponse::empty(), stacking_context_scroll_offsets: HashMap::new(), + text_index_response: TextIndexResponse(None), })), error_reporter: CSSErrorReporter { pipelineid: id, @@ -1039,6 +1041,9 @@ impl LayoutThread { ReflowQueryType::MarginStyleQuery(_) => { rw_data.margin_style_response = MarginStyleResponse::empty(); }, + ReflowQueryType::TextIndexQuery(..) => { + rw_data.text_index_response = TextIndexResponse(None); + } ReflowQueryType::NoQuery => {} } return; @@ -1243,7 +1248,7 @@ impl LayoutThread { rw_data.content_boxes_response = process_content_boxes_request(node, root_flow); }, ReflowQueryType::HitTestQuery(translated_point, client_point, update_cursor) => { - let translated_point = Point2D::new(Au::from_f32_px(translated_point.x), + let mut translated_point = Point2D::new(Au::from_f32_px(translated_point.x), Au::from_f32_px(translated_point.y)); let client_point = Point2D::new(Au::from_f32_px(client_point.x), @@ -1252,11 +1257,24 @@ impl LayoutThread { let result = rw_data.display_list .as_ref() .expect("Tried to hit test with no display list") - .hit_test(&translated_point, + .hit_test(&mut translated_point, &client_point, &rw_data.stacking_context_scroll_offsets); rw_data.hit_test_response = (result.last().cloned(), update_cursor); }, + ReflowQueryType::TextIndexQuery(node, mouse_x, mouse_y) => { + let node = unsafe { ServoLayoutNode::new(&node) }; + let opaque_node = node.opaque(); + let client_point = Point2D::new(Au::from_px(mouse_x), + Au::from_px(mouse_y)); + rw_data.text_index_response = + TextIndexResponse(rw_data.display_list + .as_ref() + .expect("Tried to hit test with no display list") + .text_index(opaque_node, + &client_point, + &rw_data.stacking_context_scroll_offsets)); + }, ReflowQueryType::NodeGeometryQuery(node) => { let node = unsafe { ServoLayoutNode::new(&node) }; rw_data.client_rect_response = process_node_geometry_request(node, root_flow); @@ -1593,7 +1611,7 @@ fn get_ua_stylesheets() -> Result { /// or false if it only needs stacking-relative positions. fn reflow_query_type_needs_display_list(query_type: &ReflowQueryType) -> bool { match *query_type { - ReflowQueryType::HitTestQuery(..) => true, + ReflowQueryType::HitTestQuery(..) | ReflowQueryType::TextIndexQuery(..) => true, ReflowQueryType::ContentBoxQuery(_) | ReflowQueryType::ContentBoxesQuery(_) | ReflowQueryType::NodeGeometryQuery(_) | ReflowQueryType::NodeScrollGeometryQuery(_) | ReflowQueryType::NodeOverflowQuery(_) | ReflowQueryType::NodeScrollRootIdQuery(_) | diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index d133efd5e14..7e0eddae26d 100755 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -11,6 +11,8 @@ use dom::bindings::codegen::Bindings::FileListBinding::FileListMethods; use dom::bindings::codegen::Bindings::HTMLInputElementBinding; use dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use dom::bindings::codegen::Bindings::KeyboardEventBinding::KeyboardEventMethods; +use dom::bindings::codegen::Bindings::MouseEventBinding::MouseEventMethods; +use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use dom::bindings::error::{Error, ErrorResult}; use dom::bindings::inheritance::Castable; use dom::bindings::js::{JS, LayoutJS, MutNullableJS, Root, RootedReference}; @@ -27,6 +29,7 @@ use dom::htmlfieldsetelement::HTMLFieldSetElement; use dom::htmlformelement::{FormControl, FormDatum, FormDatumValue, FormSubmitter, HTMLFormElement}; use dom::htmlformelement::{ResetFrom, SubmittedFrom}; use dom::keyboardevent::KeyboardEvent; +use dom::mouseevent::MouseEvent; use dom::node::{Node, NodeDamage, UnbindContext}; use dom::node::{document_from_node, window_from_node}; use dom::nodelist::NodeList; @@ -39,6 +42,7 @@ use mime_guess; use net_traits::{CoreResourceMsg, IpcSend}; use net_traits::blob_url_store::get_blob_origin; use net_traits::filemanager_thread::{FileManagerThreadMsg, FilterPattern}; +use script_layout_interface::rpc::TextIndexResponse; use script_traits::ScriptMsg as ConstellationMsg; use servo_atoms::Atom; use std::borrow::ToOwned; @@ -1088,6 +1092,33 @@ impl VirtualMethods for HTMLInputElement { //TODO: set the editing position for text inputs document_from_node(self).request_focus(self.upcast()); + if (self.input_type.get() == InputType::InputText || + self.input_type.get() == InputType::InputPassword) && + // Check if we display a placeholder. Layout doesn't know about this. + !self.textinput.borrow().is_empty() { + if let Some(mouse_event) = event.downcast::() { + // dispatch_key_event (document.rs) triggers a click event when releasing + // the space key. There's no nice way to catch this so let's use this for + // now. + if !(mouse_event.ScreenX() == 0 && mouse_event.ScreenY() == 0 && + mouse_event.GetRelatedTarget().is_none()) { + let window = window_from_node(self); + let translated_x = mouse_event.ClientX() + window.PageXOffset(); + let translated_y = mouse_event.ClientY() + window.PageYOffset(); + let TextIndexResponse(index) = window.text_index_query( + self.upcast::().to_trusted_node_address(), + translated_x, + translated_y + ); + if let Some(i) = index { + self.textinput.borrow_mut().edit_point.index = i as usize; + // trigger redraw + self.upcast::().dirty(NodeDamage::OtherNodeDamage); + event.PreventDefault(); + } + } + } + } } else if event.type_() == atom!("keydown") && !event.DefaultPrevented() && (self.input_type.get() == InputType::InputText || self.input_type.get() == InputType::InputPassword) { diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 4eab03eaeee..d0a87168ab7 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -68,7 +68,7 @@ use script_layout_interface::message::{Msg, Reflow, ReflowQueryType, ScriptReflo use script_layout_interface::reporter::CSSErrorReporter; use script_layout_interface::rpc::{ContentBoxResponse, ContentBoxesResponse, LayoutRPC}; use script_layout_interface::rpc::{MarginStyleResponse, NodeScrollRootIdResponse}; -use script_layout_interface::rpc::ResolvedStyleResponse; +use script_layout_interface::rpc::{ResolvedStyleResponse, TextIndexResponse}; use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort, ScriptThreadEventCategory}; use script_thread::{MainThreadScriptChan, MainThreadScriptMsg, Runnable, RunnableWrapper}; use script_thread::SendableMainThreadScriptChan; @@ -1362,6 +1362,15 @@ impl Window { self.layout_rpc.margin_style() } + pub fn text_index_query(&self, node: TrustedNodeAddress, mouse_x: i32, mouse_y: i32) -> TextIndexResponse { + if !self.reflow(ReflowGoal::ForScriptQuery, + ReflowQueryType::TextIndexQuery(node, mouse_x, mouse_y), + ReflowReason::Query) { + return TextIndexResponse(None); + } + self.layout_rpc.text_index() + } + #[allow(unsafe_code)] pub fn init_browsing_context(&self, browsing_context: &BrowsingContext) { assert!(self.browsing_context.get().is_none()); @@ -1710,6 +1719,7 @@ fn debug_reflow_events(id: PipelineId, goal: &ReflowGoal, query_type: &ReflowQue ReflowQueryType::ResolvedStyleQuery(_, _, _) => "\tResolvedStyleQuery", ReflowQueryType::OffsetParentQuery(_n) => "\tOffsetParentQuery", ReflowQueryType::MarginStyleQuery(_n) => "\tMarginStyleQuery", + ReflowQueryType::TextIndexQuery(..) => "\tTextIndexQuery", }); debug_msg.push_str(match *reason { diff --git a/components/script_layout_interface/message.rs b/components/script_layout_interface/message.rs index 3d0e00c46cc..80c354d0243 100644 --- a/components/script_layout_interface/message.rs +++ b/components/script_layout_interface/message.rs @@ -100,6 +100,7 @@ pub enum ReflowQueryType { ResolvedStyleQuery(TrustedNodeAddress, Option, PropertyId), OffsetParentQuery(TrustedNodeAddress), MarginStyleQuery(TrustedNodeAddress), + TextIndexQuery(TrustedNodeAddress, i32, i32), } /// Information needed for a reflow. diff --git a/components/script_layout_interface/rpc.rs b/components/script_layout_interface/rpc.rs index 7bc58dbf334..7b559078291 100644 --- a/components/script_layout_interface/rpc.rs +++ b/components/script_layout_interface/rpc.rs @@ -39,6 +39,8 @@ pub trait LayoutRPC { fn margin_style(&self) -> MarginStyleResponse; fn nodes_from_point(&self, page_point: Point2D, client_point: Point2D) -> Vec; + + fn text_index(&self) -> TextIndexResponse; } pub struct ContentBoxResponse(pub Rect); @@ -92,3 +94,6 @@ impl MarginStyleResponse { } } } + +#[derive(Clone)] +pub struct TextIndexResponse(pub Option);