mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
devtools: Allow highlighting elements from the inspector (#35822)
This change connects the `HighlighterActor` from the devtools with the document, which will draw a blue rectangle over any highlighted dom node. https://github.com/user-attachments/assets/571b2dab-497f-4102-9e55-517cdcc040ba --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: --> - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes do not require tests because we don't have devtools tests Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
parent
20f20a07f2
commit
8608e328a1
11 changed files with 305 additions and 4 deletions
|
@ -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());
|
||||
|
|
|
@ -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<DevtoolScriptControlMsg>,
|
||||
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<String, Value>,
|
||||
msg: &Map<String, Value>,
|
||||
stream: &mut TcpStream,
|
||||
_id: StreamId,
|
||||
) -> Result<ActorMessageStatus, ()> {
|
||||
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<String>,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ pub struct LayoutContext<'a> {
|
|||
Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder), WebRenderImageInfo>>>,
|
||||
|
||||
pub node_image_animation_map: Arc<RwLock<FxHashMap<OpaqueNode, ImageAnimationState>>>,
|
||||
|
||||
/// The DOM node that is highlighted by the devtools inspector, if any
|
||||
pub highlighted_dom_node: Option<OpaqueNode>,
|
||||
}
|
||||
|
||||
pub enum ResolvedImage<'a> {
|
||||
|
|
|
@ -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<InspectorHighlight>,
|
||||
}
|
||||
|
||||
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<HighlightTraversalState>,
|
||||
}
|
||||
|
||||
struct HighlightTraversalState {
|
||||
/// The smallest rectangle that fully encloses all fragments created by the highlighted
|
||||
/// dom node, if any.
|
||||
content_box: euclid::Rect<Au, CSSPixel>,
|
||||
|
||||
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<ArcRefCell<BoxFragment>>,
|
||||
}
|
||||
|
||||
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<Au>,
|
||||
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<Au>,
|
||||
) {
|
||||
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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -538,3 +538,21 @@ pub(crate) fn handle_get_css_database(reply: IpcSender<HashMap<String, CssDataba
|
|||
.collect();
|
||||
let _ = reply.send(database);
|
||||
}
|
||||
|
||||
pub(crate) fn handle_highlight_dom_node(
|
||||
documents: &DocumentCollection,
|
||||
id: PipelineId,
|
||||
node_id: Option<String>,
|
||||
) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Modifiers>,
|
||||
/// The node that is currently highlighted by the devtools
|
||||
highlighted_dom_node: MutNullableDom<Node>,
|
||||
}
|
||||
|
||||
#[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<DomRoot<Node>> {
|
||||
self.highlighted_dom_node.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
|
|
|
@ -2142,6 +2142,8 @@ impl Window {
|
|||
.or_else(|| document.GetDocumentElement())
|
||||
.map(|root| root.upcast::<Node>().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 {
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -273,6 +273,8 @@ pub enum DevtoolScriptControlMsg {
|
|||
GetCssDatabase(IpcSender<HashMap<String, CssDatabaseProperty>>),
|
||||
/// Simulates a light or dark color scheme for the given pipeline
|
||||
SimulateColorScheme(PipelineId, Theme),
|
||||
/// Highlight the given DOM node
|
||||
HighlightDomNode(PipelineId, Option<String>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
|
|
@ -428,6 +428,8 @@ pub struct ReflowRequest {
|
|||
pub node_to_image_animation_map: FxHashMap<OpaqueNode, ImageAnimationState>,
|
||||
/// The theme for the window
|
||||
pub theme: PrefersColorScheme,
|
||||
/// The node highlighted by the devtools, if any
|
||||
pub highlighted_dom_node: Option<OpaqueNode>,
|
||||
}
|
||||
|
||||
/// A pending restyle.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue