/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/document/document_embedder_controls.rs
587 строк
22 KB
Gae24
script: Don't store rooted values inside `ControlElement` (#46460)
18 июл 2026, 08:39
Не верифицирован
18 июл 2026, 08:39
b72f104
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::Cell; use embedder_traits::{ ContextMenuAction, ContextMenuElementInformation, ContextMenuElementInformationFlags, ContextMenuItem, ContextMenuRequest, EditingActionEvent, EmbedderControlId, EmbedderControlRequest, EmbedderControlResponse, EmbedderMsg, }; use euclid::{Point2D, Rect, Size2D}; use js::context::JSContext; use net_traits::CoreResourceMsg; use net_traits::filemanager_thread::FileManagerThreadMsg; use rustc_hash::FxHashMap; use script_bindings::cell::DomRefCell; use script_bindings::codegen::GenericBindings::HTMLAnchorElementBinding::HTMLAnchorElementMethods; use script_bindings::codegen::GenericBindings::HTMLImageElementBinding::HTMLImageElementMethods; use script_bindings::codegen::GenericBindings::HistoryBinding::HistoryMethods; use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods; use script_bindings::inheritance::Castable; use script_bindings::root::{Dom, DomRoot}; use servo_base::Epoch; use servo_base::generic_channel::GenericSend; use servo_constellation_traits::{LoadData, NavigationHistoryBehavior}; use servo_url::ServoUrl; use webrender_api::units::{DeviceIntRect, DevicePoint}; use crate::dom::activation::Activatable; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::trace::NoTrace; use crate::dom::html::form_controls::text_control::TextControlElement; use crate::dom::inputevent::HitTestResult; use crate::dom::iterators::ShadowIncluding; use crate::dom::node::{Node, NodeTraits}; use crate::dom::types::{ Element, HTMLAnchorElement, HTMLElement, HTMLImageElement, HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window, }; use crate::messaging::MainThreadScriptMsg; use crate::navigation::navigate; #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] pub(crate) enum ControlElement { Select(Dom<HTMLSelectElement>), ColorInput(Dom<HTMLInputElement>), FileInput(Dom<HTMLInputElement>), Ime(Dom<HTMLElement>), ContextMenu(ContextMenuNodes), } impl ControlElement { fn node(&self) -> &Node { match self { ControlElement::Select(element) => element.upcast::<Node>(), ControlElement::ColorInput(element) => element.upcast::<Node>(), ControlElement::FileInput(element) => element.upcast::<Node>(), ControlElement::Ime(element) => element.upcast::<Node>(), ControlElement::ContextMenu(context_menu_nodes) => &context_menu_nodes.node, } } } impl js::gc::Rootable for ControlElement {} #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] pub(crate) struct DocumentEmbedderControls { /// The [`Window`] element for this [`DocumentUserInterfaceElements`]. window: Dom<Window>, /// The id of the next user interface element that the `Document` requests that the /// embedder show. This is used to track user interface elements in the API. #[no_trace] user_interface_element_index: Cell<Epoch>, /// A map of visible user interface elements. visible_elements: DomRefCell<FxHashMap<NoTrace<Epoch>, ControlElement>>, } impl DocumentEmbedderControls { pub fn new(window: &Window) -> Self { Self { window: Dom::from_ref(window), user_interface_element_index: Default::default(), visible_elements: Default::default(), } } /// Generate the next unused [`EmbedderControlId`]. This method is only needed for some older /// types of controls that are still being migrated, and it will eventually be removed. pub(crate) fn next_control_id(&self) -> EmbedderControlId { let index = self.user_interface_element_index.get(); self.user_interface_element_index.set(index.next()); EmbedderControlId { webview_id: self.window.webview_id(), pipeline_id: self.window.pipeline_id(), index, } } #[cfg_attr(crown, expect(crown::unrooted_must_root))] pub(crate) fn show_embedder_control( &self, element: ControlElement, request: EmbedderControlRequest, point: Option<DevicePoint>, ) -> EmbedderControlId { let id = self.next_control_id(); let rect = point .map(|point| DeviceIntRect::from_origin_and_size(point.to_i32(), Size2D::zero())) .unwrap_or_else(|| { let rect = element .node() .upcast::<Node>() .border_box() .unwrap_or_default(); let rect = Rect::new( Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()), Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()), ); // FIXME: This is a CSS pixel rect relative to this frame, we need a DevicePixel rectangle // relative to the entire WebView! DeviceIntRect::from_untyped(&rect.to_box2d()) }); self.visible_elements .borrow_mut() .insert(id.index.into(), element); self.send_embedder_control_request(request, id, rect); id } fn send_embedder_control_request( &self, request: EmbedderControlRequest, id: EmbedderControlId, rect: DeviceIntRect, ) { match request { EmbedderControlRequest::SelectElement(..) | EmbedderControlRequest::ColorPicker(..) | EmbedderControlRequest::InputMethod(..) | EmbedderControlRequest::ContextMenu(..) => self .window .send_to_embedder(EmbedderMsg::ShowEmbedderControl(id, rect, request)), EmbedderControlRequest::FilePicker(file_picker_request) => { let main_thread_sender = self.window.main_thread_script_chan().clone(); let callback = profile_traits::generic_callback::GenericCallback::new( self.window.as_global_scope().time_profiler_chan().clone(), move |result| { let Ok(embedder_control_response) = result else { return; }; if let Err(error) = main_thread_sender.send( MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager( id, embedder_control_response, ), ) { warn!("Could not send FileManager response to main thread: {error}") } }, ) .expect("Could not create callback"); self.window .as_global_scope() .resource_threads() .sender() .send(CoreResourceMsg::ToFileManager( FileManagerThreadMsg::SelectFiles(id, file_picker_request, callback), )) .unwrap(); }, } } pub(crate) fn hide_embedder_control(&self, element: &Element) { self.visible_elements .borrow_mut() .retain(|index, control_element| { if control_element.node() != element.upcast() { return true; } let id = EmbedderControlId { webview_id: self.window.webview_id(), pipeline_id: self.window.pipeline_id(), index: index.0, }; self.window .send_to_embedder(EmbedderMsg::HideEmbedderControl(id)); false }); } pub(crate) fn handle_embedder_control_response( &self, cx: &mut JSContext, id: EmbedderControlId, response: EmbedderControlResponse, ) { assert_eq!(self.window.pipeline_id(), id.pipeline_id); assert_eq!(self.window.webview_id(), id.webview_id); rooted!(&in(cx) let visible_element = self.visible_elements.borrow_mut().remove(&id.index.into())); let Some(element) = &*visible_element else { return; }; // Never process embedder responses on inactive `Document`s. if !element.node().owner_doc().is_active() { return; } match (element, response) { ( ControlElement::Select(select_element), EmbedderControlResponse::SelectElement(response), ) => { select_element.handle_embedder_response(cx, response); }, ( ControlElement::ColorInput(input_element), EmbedderControlResponse::ColorPicker(response), ) => { input_element.handle_color_picker_response(cx, response); }, ( ControlElement::FileInput(input_element), EmbedderControlResponse::FilePicker(response), ) => { input_element.handle_file_picker_response(cx, response); }, ( ControlElement::ContextMenu(context_menu_nodes), EmbedderControlResponse::ContextMenu(action), ) => { context_menu_nodes.handle_context_menu_action(action, cx); }, (_, _) => unreachable!( "The response to a form control should always match it's originating type." ), } } pub(crate) fn show_context_menu(&self, hit_test_result: &HitTestResult) { { let mut visible_elements = self.visible_elements.borrow_mut(); visible_elements.retain(|index, control_element| { if matches!(control_element, ControlElement::ContextMenu(..)) { let id = EmbedderControlId { webview_id: self.window.webview_id(), pipeline_id: self.window.pipeline_id(), index: index.0, }; self.window .send_to_embedder(EmbedderMsg::HideEmbedderControl(id)); false } else { true } }); } let mut anchor_element = None; let mut image_element = None; let mut text_input_element = None; for node in hit_test_result .node .inclusive_ancestors(ShadowIncluding::Yes) { if anchor_element.is_none() && let Some(candidate_anchor_element) = node.downcast::<HTMLAnchorElement>() && candidate_anchor_element.is_instance_activatable() { anchor_element = Some(DomRoot::from_ref(candidate_anchor_element)); } if image_element.is_none() && let Some(candidate_image_element) = node.downcast::<HTMLImageElement>() { image_element = Some(DomRoot::from_ref(candidate_image_element)) } if text_input_element.is_none() && let Some(candidate_text_input_element) = node.as_text_input() { text_input_element = Some(candidate_text_input_element); } } let mut info = ContextMenuElementInformation::default(); let mut items = Vec::new(); if let Some(anchor_element) = anchor_element.as_ref() { info.flags.insert(ContextMenuElementInformationFlags::Link); info.link_url = anchor_element .full_href_url_for_user_interface() .map(ServoUrl::into_url); items.extend(vec![ ContextMenuItem::Item { label: "Open Link in New View".into(), action: ContextMenuAction::OpenLinkInNewWebView, enabled: true, }, ContextMenuItem::Item { label: "Copy Link".into(), action: ContextMenuAction::CopyLink, enabled: true, }, ContextMenuItem::Separator, ]); } if let Some(image_element) = image_element.as_ref() { info.flags.insert(ContextMenuElementInformationFlags::Image); info.image_url = image_element .full_image_url_for_user_interface() .map(ServoUrl::into_url); items.extend(vec![ ContextMenuItem::Item { label: "Open Image in New View".into(), action: ContextMenuAction::OpenImageInNewView, enabled: true, }, ContextMenuItem::Item { label: "Copy Image Link".into(), action: ContextMenuAction::CopyImageLink, enabled: true, }, ContextMenuItem::Separator, ]); } if let Some(text_input_element) = &text_input_element { let has_selection = text_input_element.has_uncollapsed_selection(); info.flags .insert(ContextMenuElementInformationFlags::EditableText); if has_selection { info.flags .insert(ContextMenuElementInformationFlags::Selection); } items.extend(vec![ ContextMenuItem::Item { label: "Cut".into(), action: ContextMenuAction::Cut, enabled: has_selection, }, ContextMenuItem::Item { label: "Copy".into(), action: ContextMenuAction::Copy, enabled: has_selection, }, ContextMenuItem::Item { label: "Paste".into(), action: ContextMenuAction::Paste, enabled: true, }, ContextMenuItem::Item { label: "Select All".into(), action: ContextMenuAction::SelectAll, enabled: text_input_element.has_selectable_text(), }, ContextMenuItem::Separator, ]); } items.extend(vec![ ContextMenuItem::Item { label: "Back".into(), action: ContextMenuAction::GoBack, enabled: true, }, ContextMenuItem::Item { label: "Forward".into(), action: ContextMenuAction::GoForward, enabled: true, }, ContextMenuItem::Item { label: "Reload".into(), action: ContextMenuAction::Reload, enabled: true, }, ]); self.show_embedder_control( ControlElement::ContextMenu(ContextMenuNodes { node: hit_test_result.node.as_traced(), anchor_element: anchor_element.map(|element| element.as_traced()), image_element: image_element.map(|element| element.as_traced()), text_input_element: text_input_element.map(|element| element.as_traced()), }), EmbedderControlRequest::ContextMenu(ContextMenuRequest { element_info: info, items, }), Some(hit_test_result.point_in_frame.cast_unit()), ); } } #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] pub(crate) struct ContextMenuNodes { /// The node that this menu was triggered on. node: Dom<Node>, /// The first inclusive ancestor of this node that is an `<a>` if one exists. anchor_element: Option<Dom<HTMLAnchorElement>>, /// The first inclusive ancestor of this node that is an `<img>` if one exists. image_element: Option<Dom<HTMLImageElement>>, /// The first inclusive ancestor of this node which is a text entry field. text_input_element: Option<Dom<Element>>, } impl ContextMenuNodes { fn handle_context_menu_action(&self, action: Option<ContextMenuAction>, cx: &mut JSContext) { let Some(action) = action else { return; }; let window = self.node.owner_window(); let document = window.Document(); let set_clipboard_text = |string: String| { if string.is_empty() { return; } window.send_to_embedder(EmbedderMsg::SetClipboardText(window.webview_id(), string)); }; let open_url_in_new_webview = |cx: &mut JSContext, url: ServoUrl| { let Some(browsing_context) = document.browsing_context() else { return; }; let (browsing_context, new) = browsing_context.choose_browsing_context( cx, "_blank".into(), true, /* nooopener */ ); let Some(browsing_context) = browsing_context else { return; }; assert!(new); let Some(target_document) = browsing_context.document() else { return; }; let target_window = target_document.window(); let target = Trusted::new(target_window); let load_data = LoadData::new_for_new_unrelated_webview(url); let task = task!(open_link_in_new_webview: move |cx| { navigate(cx, &target.root(), NavigationHistoryBehavior::Replace, false, load_data); }); target_document .owner_global() .task_manager() .dom_manipulation_task_source() .queue(task); }; match action { ContextMenuAction::GoBack => { let _ = window.History(cx).Back(); }, ContextMenuAction::GoForward => { let _ = window.History(cx).Forward(); }, ContextMenuAction::Reload => { window.Location(cx).reload_without_origin_check(cx); }, ContextMenuAction::CopyLink => { let Some(anchor_element) = &self.anchor_element else { return; }; let url_string = anchor_element .full_href_url_for_user_interface() .as_ref() .map(ServoUrl::to_string) .unwrap_or_else(|| String::from(anchor_element.Href())); set_clipboard_text(url_string); }, ContextMenuAction::OpenLinkInNewWebView => { let Some(anchor_element) = &self.anchor_element else { return; }; if let Some(url) = anchor_element.full_href_url_for_user_interface() { open_url_in_new_webview(cx, url); }; }, ContextMenuAction::CopyImageLink => { let Some(image_element) = &self.image_element else { return; }; let url_string = image_element .full_image_url_for_user_interface() .as_ref() .map(ServoUrl::to_string) .unwrap_or_else(|| String::from(image_element.CurrentSrc())); set_clipboard_text(url_string); }, ContextMenuAction::OpenImageInNewView => { let Some(image_element) = &self.image_element else { return; }; if let Some(url) = image_element.full_image_url_for_user_interface() { open_url_in_new_webview(cx, url); } }, ContextMenuAction::Cut => { window.Document().event_handler().handle_editing_action( cx, self.text_input_element.as_deref().map(DomRoot::from_ref), EditingActionEvent::Cut, ); }, ContextMenuAction::Copy => { window.Document().event_handler().handle_editing_action( cx, self.text_input_element.as_deref().map(DomRoot::from_ref), EditingActionEvent::Copy, ); }, ContextMenuAction::Paste => { window.Document().event_handler().handle_editing_action( cx, self.text_input_element.as_deref().map(DomRoot::from_ref), EditingActionEvent::Paste, ); }, ContextMenuAction::SelectAll => { if let Some(text_input_element) = &self.text_input_element { text_input_element.select_all(); } }, } } } impl Node { fn as_text_input(&self) -> Option<DomRoot<Element>> { if let Some(input_element) = self .downcast::<HTMLInputElement>() .filter(|input_element| input_element.is_textual_or_password()) { return Some(DomRoot::from_ref(input_element.upcast::<Element>())); } self.downcast::<HTMLTextAreaElement>() .map(Castable::upcast) .map(DomRoot::from_ref) } } impl Element { fn has_uncollapsed_selection(&self) -> bool { self.downcast::<HTMLTextAreaElement>() .map(TextControlElement::has_uncollapsed_selection) .or(self .downcast::<HTMLInputElement>() .map(TextControlElement::has_uncollapsed_selection)) .unwrap_or_default() } fn has_selectable_text(&self) -> bool { self.downcast::<HTMLTextAreaElement>() .map(TextControlElement::has_selectable_text) .or(self .downcast::<HTMLInputElement>() .map(TextControlElement::has_selectable_text)) .unwrap_or_default() } fn select_all(&self) { self.downcast::<HTMLTextAreaElement>() .map(TextControlElement::select_all) .or(self .downcast::<HTMLInputElement>() .map(TextControlElement::select_all)) .unwrap_or_default() } }