diff --git a/components/devtools/actors/inspector.rs b/components/devtools/actors/inspector.rs index d41777f56c6..28a4d7ccf94 100644 --- a/components/devtools/actors/inspector.rs +++ b/components/devtools/actors/inspector.rs @@ -166,6 +166,8 @@ impl Actor for InspectorActor { if self.highlighter.borrow().is_none() { let highlighter_actor = HighlighterActor { name: registry.new_name("highlighter"), + pipeline, + script_sender: self.script_chan.clone(), }; let mut highlighter = self.highlighter.borrow_mut(); *highlighter = Some(highlighter_actor.name()); diff --git a/components/devtools/actors/inspector/highlighter.rs b/components/devtools/actors/inspector/highlighter.rs index f75d25f2175..0dbf2b1e52f 100644 --- a/components/devtools/actors/inspector/highlighter.rs +++ b/components/devtools/actors/inspector/highlighter.rs @@ -7,6 +7,9 @@ use std::net::TcpStream; +use base::id::PipelineId; +use devtools_traits::DevtoolScriptControlMsg; +use ipc_channel::ipc::IpcSender; use serde::Serialize; use serde_json::{self, Map, Value}; @@ -21,6 +24,8 @@ pub struct HighlighterMsg { pub struct HighlighterActor { pub name: String, + pub script_sender: IpcSender, + pub pipeline: PipelineId, } #[derive(Serialize)] @@ -41,14 +46,39 @@ impl Actor for HighlighterActor { /// - `hide`: Disables highlighting for the selected node fn handle_message( &self, - _registry: &ActorRegistry, + registry: &ActorRegistry, msg_type: &str, - _msg: &Map, + msg: &Map, stream: &mut TcpStream, _id: StreamId, ) -> Result { Ok(match msg_type { "show" => { + let Some(node_actor) = msg.get("node") else { + // TODO: send missing parameter error + return Ok(ActorMessageStatus::Ignored); + }; + + let Some(node_actor_name) = node_actor.as_str() else { + // TODO: send invalid parameter error + return Ok(ActorMessageStatus::Ignored); + }; + + if node_actor_name.starts_with("inspector") { + // TODO: For some reason, the client initially asks us to highlight + // the inspector? Investigate what this is supposed to mean. + let msg = ShowReply { + from: self.name(), + value: false, + }; + let _ = stream.write_json_packet(&msg); + return Ok(ActorMessageStatus::Processed); + } + + self.instruct_script_thread_to_highlight_node( + Some(node_actor_name.to_owned()), + registry, + ); let msg = ShowReply { from: self.name(), value: true, @@ -58,6 +88,8 @@ impl Actor for HighlighterActor { }, "hide" => { + self.instruct_script_thread_to_highlight_node(None, registry); + let msg = EmptyReplyMsg { from: self.name() }; let _ = stream.write_json_packet(&msg); ActorMessageStatus::Processed @@ -67,3 +99,19 @@ impl Actor for HighlighterActor { }) } } + +impl HighlighterActor { + fn instruct_script_thread_to_highlight_node( + &self, + node_actor: Option, + registry: &ActorRegistry, + ) { + let node_id = node_actor.map(|node_actor| registry.actor_to_script(node_actor)); + self.script_sender + .send(DevtoolScriptControlMsg::HighlightDomNode( + self.pipeline, + node_id, + )) + .unwrap(); + } +} diff --git a/components/layout/context.rs b/components/layout/context.rs index 62f8a8cdae9..3411eed486c 100644 --- a/components/layout/context.rs +++ b/components/layout/context.rs @@ -46,6 +46,9 @@ pub struct LayoutContext<'a> { Arc>>, pub node_image_animation_map: Arc>>, + + /// The DOM node that is highlighted by the devtools inspector, if any + pub highlighted_dom_node: Option, } pub enum ResolvedImage<'a> { diff --git a/components/layout/display_list/mod.rs b/components/layout/display_list/mod.rs index 3908da69ce1..8799dd2da0c 100644 --- a/components/layout/display_list/mod.rs +++ b/components/layout/display_list/mod.rs @@ -31,13 +31,15 @@ use style::values::generics::NonNegative; use style::values::generics::rect::Rect; use style::values::specified::text::TextDecorationLine; use style::values::specified::ui::CursorKind; +use style_traits::CSSPixel; use webrender_api::units::{DevicePixel, LayoutPixel, LayoutRect, LayoutSize}; use webrender_api::{ self as wr, BorderDetails, BoxShadowClipMode, ClipChainId, CommonItemProperties, - ImageRendering, NinePatchBorder, NinePatchBorderSource, units, + ImageRendering, NinePatchBorder, NinePatchBorderSource, SpatialId, units, }; use wr::units::LayoutVector2D; +use crate::cell::ArcRefCell; use crate::context::{LayoutContext, ResolvedImage}; pub use crate::display_list::conversions::ToWebRender; use crate::display_list::stacking_context::StackingContextSection; @@ -161,10 +163,49 @@ pub(crate) struct DisplayListBuilder<'a> { /// The [DisplayList] used to collect display list items and metadata. pub display_list: &'a mut DisplayList, + + /// Data about the fragments that are highlighted by the inspector, if any. + /// + /// This data is collected during the traversal of the fragment tree and used + /// to paint the highlight at the very end. + inspector_highlight: Option, +} + +struct InspectorHighlight { + /// The node that should be highlighted + tag: Tag, + + /// Accumulates information about the fragments that belong to the highlighted node. + /// + /// This information is collected as the fragment tree is traversed to build the + /// display list. + state: Option, +} + +struct HighlightTraversalState { + /// The smallest rectangle that fully encloses all fragments created by the highlighted + /// dom node, if any. + content_box: euclid::Rect, + + spatial_id: SpatialId, + + clip_chain_id: ClipChainId, + + /// When the highlighted fragment is a box fragment we remember the information + /// needed to paint padding, border and margin areas. + maybe_box_fragment: Option>, +} + +impl InspectorHighlight { + fn for_node(node: OpaqueNode) -> Self { + Self { + tag: Tag::new(node), + state: None, + } + } } impl DisplayList { - /// Build the display list, returning true if it was contentful. pub fn build( &mut self, context: &LayoutContext, @@ -180,8 +221,19 @@ impl DisplayList { element_for_canvas_background: fragment_tree.canvas_background.from_element, context, display_list: self, + inspector_highlight: context + .highlighted_dom_node + .map(InspectorHighlight::for_node), }; fragment_tree.build_display_list(&mut builder, root_stacking_context); + + if let Some(highlight) = builder + .inspector_highlight + .take() + .and_then(|highlight| highlight.state) + { + builder.paint_dom_inspector_highlight(highlight); + } } } @@ -233,6 +285,150 @@ impl DisplayListBuilder<'_> { self.display_list.compositor_info.epoch.as_u16(), )) } + + /// Draw highlights around the node that is currently hovered in the devtools. + fn paint_dom_inspector_highlight(&mut self, highlight: HighlightTraversalState) { + const CONTENT_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { + r: 0.23, + g: 0.7, + b: 0.87, + a: 0.5, + }; + + const PADDING_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { + r: 0.49, + g: 0.3, + b: 0.7, + a: 0.5, + }; + + const BORDER_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { + r: 0.2, + g: 0.2, + b: 0.2, + a: 0.5, + }; + + const MARGIN_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { + r: 1., + g: 0.93, + b: 0., + a: 0.5, + }; + + // Highlight content box + let content_box = highlight.content_box.to_webrender(); + let properties = wr::CommonItemProperties { + clip_rect: content_box, + spatial_id: highlight.spatial_id, + clip_chain_id: highlight.clip_chain_id, + flags: wr::PrimitiveFlags::default(), + }; + + self.display_list + .wr + .push_rect(&properties, content_box, CONTENT_BOX_HIGHLIGHT_COLOR); + + // Highlight margin, border and padding + if let Some(box_fragment) = highlight.maybe_box_fragment { + let mut paint_highlight = + |color: webrender_api::ColorF, + fragment_relative_bounds: PhysicalRect, + widths: webrender_api::units::LayoutSideOffsets| { + if widths.is_zero() { + return; + } + + let bounds = box_fragment + .borrow() + .offset_by_containing_block(&fragment_relative_bounds) + .to_webrender(); + + // We paint each highlighted area as if it was a border for simplicity + let border_style = wr::BorderSide { + color, + style: webrender_api::BorderStyle::Solid, + }; + + let details = wr::BorderDetails::Normal(wr::NormalBorder { + top: border_style, + right: border_style, + bottom: border_style, + left: border_style, + radius: webrender_api::BorderRadius::default(), + do_aa: true, + }); + + let common = wr::CommonItemProperties { + clip_rect: bounds, + spatial_id: highlight.spatial_id, + clip_chain_id: highlight.clip_chain_id, + flags: wr::PrimitiveFlags::default(), + }; + self.wr().push_border(&common, bounds, widths, details) + }; + + let box_fragment = box_fragment.borrow(); + paint_highlight( + PADDING_BOX_HIGHLIGHT_COLOR, + box_fragment.padding_rect(), + box_fragment.padding.to_webrender(), + ); + paint_highlight( + BORDER_BOX_HIGHLIGHT_COLOR, + box_fragment.border_rect(), + box_fragment.border.to_webrender(), + ); + paint_highlight( + MARGIN_BOX_HIGHLIGHT_COLOR, + box_fragment.margin_rect(), + box_fragment.margin.to_webrender(), + ); + } + } +} + +impl InspectorHighlight { + fn register_fragment_of_highlighted_dom_node( + &mut self, + fragment: &Fragment, + spatial_id: SpatialId, + clip_chain_id: ClipChainId, + containing_block: &PhysicalRect, + ) { + let state = self.state.get_or_insert(HighlightTraversalState { + content_box: euclid::Rect::zero(), + spatial_id, + clip_chain_id, + maybe_box_fragment: None, + }); + + // We expect all fragments generated by one node to be in the same scroll tree node and clip node + debug_assert_eq!(spatial_id, state.spatial_id); + if clip_chain_id != ClipChainId::INVALID && state.clip_chain_id != ClipChainId::INVALID { + debug_assert_eq!( + clip_chain_id, state.clip_chain_id, + "Fragments of the same node must either have no clip chain or the same one" + ); + } + + let fragment_relative_rect = match fragment { + Fragment::Box(fragment) | Fragment::Float(fragment) => { + state.maybe_box_fragment = Some(fragment.clone()); + + fragment.borrow().content_rect + }, + Fragment::Positioning(fragment) => fragment.borrow().rect, + Fragment::Text(fragment) => fragment.borrow().rect, + Fragment::Image(image_fragment) => image_fragment.borrow().rect, + Fragment::AbsoluteOrFixedPositioned(_) => return, + Fragment::IFrame(iframe_fragment) => iframe_fragment.borrow().rect, + }; + + state.content_box = state + .content_box + .union(&fragment_relative_rect.translate(containing_block.origin.to_vector())); + } } impl Fragment { @@ -244,6 +440,17 @@ impl Fragment { is_hit_test_for_scrollable_overflow: bool, is_collapsed_table_borders: bool, ) { + if let Some(inspector_highlight) = &mut builder.inspector_highlight { + if self.tag() == Some(inspector_highlight.tag) { + inspector_highlight.register_fragment_of_highlighted_dom_node( + self, + builder.current_scroll_node_id.spatial_id, + builder.current_clip_chain_id, + containing_block, + ); + } + } + match self { Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => { let box_fragment = &*box_fragment.borrow(); @@ -739,6 +946,7 @@ impl<'a> BuilderForBoxFragment<'a> { { return; } + self.build_background(builder); self.build_box_shadow(builder); self.build_border(builder); diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index bb84806ebdb..efd31057127 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -651,6 +651,7 @@ impl LayoutThread { ))), iframe_sizes: Mutex::default(), use_rayon: rayon_pool.is_some(), + highlighted_dom_node: reflow_request.highlighted_dom_node, }; self.restyle_and_build_trees( diff --git a/components/script/devtools.rs b/components/script/devtools.rs index c9d7c3094db..93212887dc8 100644 --- a/components/script/devtools.rs +++ b/components/script/devtools.rs @@ -538,3 +538,21 @@ pub(crate) fn handle_get_css_database(reply: IpcSender, +) { + let node = node_id.and_then(|node_id| { + let node = find_node_by_unique_id(documents, id, &node_id); + if node.is_none() { + log::warn!("Node id {node_id} for pipeline id {id} is not found",); + } + node + }); + + if let Some(window) = documents.find_window(id) { + window.Document().highlight_dom_node(node.as_deref()); + } +} diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 9ce24038259..bb30a84172c 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -565,6 +565,8 @@ pub(crate) struct Document { /// The active keyboard modifiers for the WebView. This is updated when receiving any input event. #[no_trace] active_keyboard_modifiers: Cell, + /// The node that is currently highlighted by the devtools + highlighted_dom_node: MutNullableDom, } #[allow(non_snake_case)] @@ -4222,6 +4224,7 @@ impl Document { intersection_observer_task_queued: Cell::new(false), intersection_observers: Default::default(), active_keyboard_modifiers: Cell::new(Modifiers::empty()), + highlighted_dom_node: Default::default(), } } @@ -5213,6 +5216,14 @@ impl Document { self.has_trustworthy_ancestor_origin.get() || self.origin().immutable().is_potentially_trustworthy() } + pub(crate) fn highlight_dom_node(&self, node: Option<&Node>) { + self.highlighted_dom_node.set(node); + self.set_needs_paint(true); + } + + pub(crate) fn highlighted_dom_node(&self) -> Option> { + self.highlighted_dom_node.get() + } } #[allow(non_snake_case)] diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index b115add8611..d888cc8d917 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -2142,6 +2142,8 @@ impl Window { .or_else(|| document.GetDocumentElement()) .map(|root| root.upcast::().to_trusted_node_address()); + let highlighted_dom_node = document.highlighted_dom_node().map(|node| node.to_opaque()); + // Send new document and relevant styles to layout. let reflow = ReflowRequest { reflow_info: Reflow { @@ -2161,6 +2163,7 @@ impl Window { .image_animation_manager_mut() .take_image_animate_set(), theme: self.theme.get(), + highlighted_dom_node, }; let Some(results) = self.layout.borrow_mut().reflow(reflow) else { diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 9f6e1bc1dd1..54cf89a213f 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -2069,6 +2069,9 @@ impl ScriptThread { None => warn!("Message sent to closed pipeline {}.", id), } }, + DevtoolScriptControlMsg::HighlightDomNode(id, node_id) => { + devtools::handle_highlight_dom_node(&documents, id, node_id) + }, } } diff --git a/components/shared/devtools/lib.rs b/components/shared/devtools/lib.rs index c07f4529073..628d76bd25d 100644 --- a/components/shared/devtools/lib.rs +++ b/components/shared/devtools/lib.rs @@ -273,6 +273,8 @@ pub enum DevtoolScriptControlMsg { GetCssDatabase(IpcSender>), /// Simulates a light or dark color scheme for the given pipeline SimulateColorScheme(PipelineId, Theme), + /// Highlight the given DOM node + HighlightDomNode(PipelineId, Option), } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/components/shared/script_layout/lib.rs b/components/shared/script_layout/lib.rs index 66baccd5147..8c5d4edc4e0 100644 --- a/components/shared/script_layout/lib.rs +++ b/components/shared/script_layout/lib.rs @@ -428,6 +428,8 @@ pub struct ReflowRequest { pub node_to_image_animation_map: FxHashMap, /// The theme for the window pub theme: PrefersColorScheme, + /// The node highlighted by the devtools, if any + pub highlighted_dom_node: Option, } /// A pending restyle.