use atomic_refcell::AtomicRefCell;
use devtools_traits::DevtoolScriptControlMsg::{self, GetCssDatabase, SimulateColorScheme};
use devtools_traits::{DevtoolsPageInfo, NavigationState};
use embedder_traits::Theme;
use malloc_size_of_derive::MallocSizeOf;
use rustc_hash::FxHashMap;
use serde::Serialize;
use serde_json::{Map, Value};
use servo_base::generic_channel::{self, GenericSender, SendError};
use servo_base::id::PipelineId;
use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
use crate::actors::inspector::InspectorActor;
use crate::actors::inspector::accessibility::AccessibilityActor;
use crate::actors::inspector::css_properties::CssPropertiesActor;
use crate::actors::reflow::ReflowActor;
use crate::actors::stylesheets::StyleSheetsActor;
use crate::actors::tab::TabDescriptorActor;
use crate::actors::thread::ThreadActor;
use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
use crate::id::{DevtoolsBrowserId, DevtoolsBrowsingContextId, DevtoolsOuterWindowId, IdMap};
use crate::protocol::{ClientRequest, DevtoolsConnection, JsonPacketStream};
use crate::resource::ResourceAvailable;
use crate::{EmptyReplyMsg, StreamId};
#[derive(Serialize)]
struct ListWorkersReply {
from: String,
workers: Vec<()>,
}
#[derive(Serialize)]
struct FrameUpdateReply {
from: String,
#[serde(rename = "type")]
type_: String,
frames: Vec<FrameUpdateMsg>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FrameUpdateMsg {
id: u32,
is_top_level: bool,
url: String,
title: String,
}
#[derive(Serialize)]
struct TabNavigated {
from: String,
#[serde(rename = "type")]
type_: String,
url: String,
title: Option<String>,
#[serde(rename = "nativeConsoleAPI")]
native_console_api: bool,
state: String,
is_frame_switching: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct BrowsingContextTraits {
frames: bool,
is_browsing_context: bool,
log_in_page: bool,
navigation: bool,
supports_top_level_target_flag: bool,
watchpoints: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
enum TargetType {
Frame,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct BrowsingContextActorMsg {
actor: String,
title: String,
url: String,
#[serde(rename = "browserId")]
browser_id: u32,
#[serde(rename = "outerWindowID")]
outer_window_id: u32,
#[serde(rename = "browsingContextID")]
browsing_context_id: u32,
is_top_level_target: bool,
traits: BrowsingContextTraits,
accessibility_actor: String,
console_actor: String,
css_properties_actor: String,
inspector_actor: String,
reflow_actor: String,
style_sheets_actor: String,
thread_actor: String,
target_type: TargetType,
}
#[derive(MallocSizeOf)]
pub(crate) struct BrowsingContextActor {
name: String,
pub title: AtomicRefCell<String>,
pub url: AtomicRefCell<String>,
pub browser_id: DevtoolsBrowserId,
active_pipeline_id: AtomicRefCell<PipelineId>,
active_outer_window_id: AtomicRefCell<DevtoolsOuterWindowId>,
pub browsing_context_id: DevtoolsBrowsingContextId,
accessibility: String,
pub console_name: String,
css_properties_name: String,
pub(crate) inspector_name: String,
reflow_name: String,
style_sheets_name: String,
pub thread_name: String,
_tab: String,
script_chans: AtomicRefCell<FxHashMap<PipelineId, GenericSender<DevtoolScriptControlMsg>>>,
pub watcher_name: String,
}
impl ResourceAvailable for BrowsingContextActor {
fn actor_name(&self) -> String {
self.name.clone()
}
}
impl Actor for BrowsingContextActor {
fn name(&self) -> String {
self.name.clone()
}
fn handle_message(
&self,
request: ClientRequest,
_registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
match msg_type {
"listFrames" => {
let msg = EmptyReplyMsg { from: self.name() };
request.reply_final(&msg)?
},
"listWorkers" => {
request.reply_final(&ListWorkersReply {
from: self.name(),
workers: vec![],
})?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}
impl BrowsingContextActor {
#[expect(clippy::too_many_arguments)]
pub(crate) fn new(
console_name: String,
browser_id: DevtoolsBrowserId,
browsing_context_id: DevtoolsBrowsingContextId,
page_info: DevtoolsPageInfo,
pipeline_id: PipelineId,
outer_window_id: DevtoolsOuterWindowId,
script_sender: GenericSender<DevtoolScriptControlMsg>,
registry: &ActorRegistry,
) -> BrowsingContextActor {
let name = registry.new_name::<BrowsingContextActor>();
let DevtoolsPageInfo {
title,
url,
is_top_level_global,
..
} = page_info;
let accessibility_actor =
AccessibilityActor::new(registry.new_name::<AccessibilityActor>());
let properties = (|| {
let (properties_sender, properties_receiver) = generic_channel::channel()?;
script_sender.send(GetCssDatabase(properties_sender)).ok()?;
properties_receiver.recv().ok()
})()
.unwrap_or_default();
let css_properties_actor =
CssPropertiesActor::new(registry.new_name::<CssPropertiesActor>(), properties);
let inspector_name = InspectorActor::register(registry, name.clone());
let reflow_actor = ReflowActor::new(registry.new_name::<ReflowActor>());
let style_sheets_actor = StyleSheetsActor::new(registry.new_name::<StyleSheetsActor>());
let tab_descriptor_actor =
TabDescriptorActor::new(registry, name.clone(), is_top_level_global);
let thread_actor = ThreadActor::new(
registry.new_name::<ThreadActor>(),
script_sender.clone(),
Some(name.clone()),
);
let watcher_actor = WatcherActor::new(
registry,
name.clone(),
SessionContext::new(SessionContextType::BrowserElement),
);
let mut script_chans = FxHashMap::default();
script_chans.insert(pipeline_id, script_sender);
let target = BrowsingContextActor {
name,
script_chans: AtomicRefCell::new(script_chans),
title: AtomicRefCell::new(title),
url: AtomicRefCell::new(url.into_string()),
active_pipeline_id: AtomicRefCell::new(pipeline_id),
active_outer_window_id: AtomicRefCell::new(outer_window_id),
browser_id,
browsing_context_id,
accessibility: accessibility_actor.name(),
console_name,
css_properties_name: css_properties_actor.name(),
inspector_name,
reflow_name: reflow_actor.name(),
style_sheets_name: style_sheets_actor.name(),
_tab: tab_descriptor_actor.name(),
thread_name: thread_actor.name(),
watcher_name: watcher_actor.name(),
};
registry.register(accessibility_actor);
registry.register(css_properties_actor);
registry.register(reflow_actor);
registry.register(style_sheets_actor);
registry.register(tab_descriptor_actor);
registry.register(thread_actor);
registry.register(watcher_actor);
target
}
pub(crate) fn handle_new_global(
&self,
pipeline: PipelineId,
script_sender: GenericSender<DevtoolScriptControlMsg>,
) {
self.script_chans
.borrow_mut()
.insert(pipeline, script_sender);
}
pub(crate) fn handle_navigate<'a>(
&self,
state: NavigationState,
id_map: &mut IdMap,
connections: impl Iterator<Item = &'a mut DevtoolsConnection>,
) {
let (pipeline_id, title, url, state) = match state {
NavigationState::Start(url) => (None, None, url, "start"),
NavigationState::Stop(pipeline, info) => {
(Some(pipeline), Some(info.title), info.url, "stop")
},
};
if let Some(pipeline_id) = pipeline_id {
let outer_window_id = id_map.outer_window_id(pipeline_id);
*self.active_outer_window_id.borrow_mut() = outer_window_id;
*self.active_pipeline_id.borrow_mut() = pipeline_id;
}
url.as_str().clone_into(&mut self.url.borrow_mut());
if let Some(ref t) = title {
self.title.borrow_mut().clone_from(t);
}
let msg = TabNavigated {
from: self.name(),
type_: "tabNavigated".to_owned(),
url: url.as_str().to_owned(),
title,
native_console_api: true,
state: state.to_owned(),
is_frame_switching: false,
};
for stream in connections {
let _ = stream.write_json_packet(&msg);
}
}
pub(crate) fn title_changed(&self, pipeline_id: PipelineId, title: String) {
if pipeline_id != self.pipeline_id() {
return;
}
*self.title.borrow_mut() = title;
}
pub(crate) fn frame_update(&self, request: &mut ClientRequest) {
let _ = request.write_json_packet(&FrameUpdateReply {
from: self.name(),
type_: "frameUpdate".into(),
frames: vec![FrameUpdateMsg {
id: self.browsing_context_id.value(),
is_top_level: true,
title: self.title.borrow().clone(),
url: self.url.borrow().clone(),
}],
});
}
pub fn simulate_color_scheme(&self, theme: Theme) -> Result<(), ()> {
self.script_chan()
.send(SimulateColorScheme(self.pipeline_id(), theme))
.map_err(|_| ())
}
pub(crate) fn pipeline_id(&self) -> PipelineId {
*self.active_pipeline_id.borrow()
}
pub(crate) fn outer_window_id(&self) -> DevtoolsOuterWindowId {
*self.active_outer_window_id.borrow()
}
pub(crate) fn script_chan(&self) -> GenericSender<DevtoolScriptControlMsg> {
self.script_chans
.borrow()
.get(&self.pipeline_id())
.unwrap()
.clone()
}
pub(crate) fn instruct_script_to_send_live_updates(&self, should_send_updates: bool) {
let result = self
.script_chan()
.send(DevtoolScriptControlMsg::WantsLiveNotifications(
self.pipeline_id(),
should_send_updates,
));
debug_assert!(matches!(result, Ok(_) | Err(SendError::Disconnected)));
}
}
impl ActorEncode<BrowsingContextActorMsg> for BrowsingContextActor {
fn encode(&self, _: &ActorRegistry) -> BrowsingContextActorMsg {
BrowsingContextActorMsg {
actor: self.name(),
traits: BrowsingContextTraits {
is_browsing_context: true,
frames: true,
log_in_page: false,
navigation: true,
supports_top_level_target_flag: true,
watchpoints: true,
},
title: self.title.borrow().clone(),
url: self.url.borrow().clone(),
browser_id: self.browser_id.value(),
browsing_context_id: self.browsing_context_id.value(),
outer_window_id: self.outer_window_id().value(),
is_top_level_target: true,
accessibility_actor: self.accessibility.clone(),
console_actor: self.console_name.clone(),
css_properties_actor: self.css_properties_name.clone(),
inspector_actor: self.inspector_name.clone(),
reflow_actor: self.reflow_name.clone(),
style_sheets_actor: self.style_sheets_name.clone(),
thread_actor: self.thread_name.clone(),
target_type: TargetType::Frame,
}
}
}