script/dom/
document_embedder_controls.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6
7use base::Epoch;
8use base::generic_channel::GenericSend;
9use constellation_traits::{LoadData, NavigationHistoryBehavior};
10use embedder_traits::{
11    ContextMenuAction, ContextMenuElementInformation, ContextMenuElementInformationFlags,
12    ContextMenuItem, ContextMenuRequest, EditingActionEvent, EmbedderControlId,
13    EmbedderControlRequest, EmbedderControlResponse, EmbedderMsg,
14};
15use euclid::{Point2D, Rect, Size2D};
16use ipc_channel::router::ROUTER;
17use net_traits::CoreResourceMsg;
18use net_traits::filemanager_thread::FileManagerThreadMsg;
19use rustc_hash::FxHashMap;
20use script_bindings::codegen::GenericBindings::HTMLAnchorElementBinding::HTMLAnchorElementMethods;
21use script_bindings::codegen::GenericBindings::HTMLImageElementBinding::HTMLImageElementMethods;
22use script_bindings::codegen::GenericBindings::HistoryBinding::HistoryMethods;
23use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
24use script_bindings::inheritance::Castable;
25use script_bindings::root::{Dom, DomRoot};
26use script_bindings::script_runtime::CanGc;
27use servo_url::ServoUrl;
28use webrender_api::units::{DeviceIntRect, DevicePoint};
29
30use crate::dom::activation::Activatable;
31use crate::dom::bindings::cell::DomRefCell;
32use crate::dom::bindings::refcounted::Trusted;
33use crate::dom::bindings::trace::NoTrace;
34use crate::dom::inputevent::HitTestResult;
35use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
36use crate::dom::textcontrol::TextControlElement;
37use crate::dom::types::{
38    Element, HTMLAnchorElement, HTMLElement, HTMLImageElement, HTMLInputElement, HTMLSelectElement,
39    HTMLTextAreaElement, Window,
40};
41use crate::messaging::MainThreadScriptMsg;
42
43#[derive(JSTraceable, MallocSizeOf)]
44pub(crate) enum ControlElement {
45    Select(DomRoot<HTMLSelectElement>),
46    ColorInput(DomRoot<HTMLInputElement>),
47    FileInput(DomRoot<HTMLInputElement>),
48    Ime(DomRoot<HTMLElement>),
49    ContextMenu(ContextMenuNodes),
50}
51
52impl ControlElement {
53    fn node(&self) -> &Node {
54        match self {
55            ControlElement::Select(element) => element.upcast::<Node>(),
56            ControlElement::ColorInput(element) => element.upcast::<Node>(),
57            ControlElement::FileInput(element) => element.upcast::<Node>(),
58            ControlElement::Ime(element) => element.upcast::<Node>(),
59            ControlElement::ContextMenu(context_menu_nodes) => &context_menu_nodes.node,
60        }
61    }
62}
63
64#[derive(JSTraceable, MallocSizeOf)]
65#[cfg_attr(crown, expect(crown::unrooted_must_root))]
66pub(crate) struct DocumentEmbedderControls {
67    /// The [`Window`] element for this [`DocumentUserInterfaceElements`].
68    window: Dom<Window>,
69    /// The id of the next user interface element that the `Document` requests that the
70    /// embedder show. This is used to track user interface elements in the API.
71    #[no_trace]
72    user_interface_element_index: Cell<Epoch>,
73    /// A map of visible user interface elements.
74    visible_elements: DomRefCell<FxHashMap<NoTrace<Epoch>, ControlElement>>,
75}
76
77impl DocumentEmbedderControls {
78    pub fn new(window: &Window) -> Self {
79        Self {
80            window: Dom::from_ref(window),
81            user_interface_element_index: Default::default(),
82            visible_elements: Default::default(),
83        }
84    }
85
86    /// Generate the next unused [`EmbedderControlId`]. This method is only needed for some older
87    /// types of controls that are still being migrated, and it will eventually be removed.
88    pub(crate) fn next_control_id(&self) -> EmbedderControlId {
89        let index = self.user_interface_element_index.get();
90        self.user_interface_element_index.set(index.next());
91        EmbedderControlId {
92            webview_id: self.window.webview_id(),
93            pipeline_id: self.window.pipeline_id(),
94            index,
95        }
96    }
97
98    pub(crate) fn show_embedder_control(
99        &self,
100        element: ControlElement,
101        request: EmbedderControlRequest,
102        point: Option<DevicePoint>,
103    ) -> EmbedderControlId {
104        let id = self.next_control_id();
105        let rect = point
106            .map(|point| DeviceIntRect::from_origin_and_size(point.to_i32(), Size2D::zero()))
107            .unwrap_or_else(|| {
108                let rect = element
109                    .node()
110                    .upcast::<Node>()
111                    .border_box()
112                    .unwrap_or_default();
113
114                let rect = Rect::new(
115                    Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
116                    Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
117                );
118
119                // FIXME: This is a CSS pixel rect relative to this frame, we need a DevicePixel rectangle
120                // relative to the entire WebView!
121                DeviceIntRect::from_untyped(&rect.to_box2d())
122            });
123
124        self.visible_elements
125            .borrow_mut()
126            .insert(id.index.into(), element);
127
128        match request {
129            EmbedderControlRequest::SelectElement(..) |
130            EmbedderControlRequest::ColorPicker(..) |
131            EmbedderControlRequest::InputMethod(..) |
132            EmbedderControlRequest::ContextMenu(..) => self
133                .window
134                .send_to_embedder(EmbedderMsg::ShowEmbedderControl(id, rect, request)),
135            EmbedderControlRequest::FilePicker(file_picker_request) => {
136                let (sender, receiver) = profile_traits::ipc::channel(
137                    self.window.as_global_scope().time_profiler_chan().clone(),
138                )
139                .expect("Error initializing channel");
140                let main_thread_sender = self.window.main_thread_script_chan().clone();
141                ROUTER.add_typed_route(
142                    receiver.to_ipc_receiver(),
143                    Box::new(move |result| {
144                        let Ok(embedder_control_response) = result else {
145                            return;
146                        };
147                        if let Err(error) = main_thread_sender.send(
148                            MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager(
149                                id,
150                                embedder_control_response,
151                            ),
152                        ) {
153                            warn!("Could not send FileManager response to main thread: {error}")
154                        }
155                    }),
156                );
157                self.window
158                    .as_global_scope()
159                    .resource_threads()
160                    .sender()
161                    .send(CoreResourceMsg::ToFileManager(
162                        FileManagerThreadMsg::SelectFiles(id, file_picker_request, sender),
163                    ))
164                    .unwrap();
165            },
166        }
167
168        id
169    }
170
171    pub(crate) fn hide_embedder_control(&self, element: &Element) {
172        self.visible_elements
173            .borrow_mut()
174            .retain(|index, control_element| {
175                if control_element.node() != element.upcast() {
176                    return true;
177                }
178                let id = EmbedderControlId {
179                    webview_id: self.window.webview_id(),
180                    pipeline_id: self.window.pipeline_id(),
181                    index: index.0,
182                };
183                self.window
184                    .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
185                false
186            });
187    }
188
189    pub(crate) fn handle_embedder_control_response(
190        &self,
191        id: EmbedderControlId,
192        response: EmbedderControlResponse,
193        can_gc: CanGc,
194    ) {
195        assert_eq!(self.window.pipeline_id(), id.pipeline_id);
196        assert_eq!(self.window.webview_id(), id.webview_id);
197
198        let Some(element) = self.visible_elements.borrow_mut().remove(&id.index.into()) else {
199            return;
200        };
201
202        // Never process embedder responses on inactive `Document`s.
203        if !element.node().owner_doc().is_active() {
204            return;
205        }
206
207        match (element, response) {
208            (
209                ControlElement::Select(select_element),
210                EmbedderControlResponse::SelectElement(response),
211            ) => {
212                select_element.handle_menu_response(response, can_gc);
213            },
214            (
215                ControlElement::ColorInput(input_element),
216                EmbedderControlResponse::ColorPicker(response),
217            ) => {
218                input_element.handle_color_picker_response(response, can_gc);
219            },
220            (
221                ControlElement::FileInput(input_element),
222                EmbedderControlResponse::FilePicker(response),
223            ) => {
224                input_element.handle_file_picker_response(response, can_gc);
225            },
226            (
227                ControlElement::ContextMenu(context_menu_nodes),
228                EmbedderControlResponse::ContextMenu(action),
229            ) => {
230                context_menu_nodes.handle_context_menu_action(action, can_gc);
231            },
232            (_, _) => unreachable!(
233                "The response to a form control should always match it's originating type."
234            ),
235        }
236    }
237
238    pub(crate) fn show_context_menu(&self, hit_test_result: &HitTestResult) {
239        {
240            let mut visible_elements = self.visible_elements.borrow_mut();
241            visible_elements.retain(|index, control_element| {
242                if matches!(control_element, ControlElement::ContextMenu(..)) {
243                    let id = EmbedderControlId {
244                        webview_id: self.window.webview_id(),
245                        pipeline_id: self.window.pipeline_id(),
246                        index: index.0,
247                    };
248                    self.window
249                        .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
250                    false
251                } else {
252                    true
253                }
254            });
255        }
256
257        let mut anchor_element = None;
258        let mut image_element = None;
259        let mut text_input_element = None;
260        for node in hit_test_result
261            .node
262            .inclusive_ancestors(ShadowIncluding::Yes)
263        {
264            if anchor_element.is_none() {
265                if let Some(candidate_anchor_element) = node.downcast::<HTMLAnchorElement>() {
266                    if candidate_anchor_element.is_instance_activatable() {
267                        anchor_element = Some(DomRoot::from_ref(candidate_anchor_element));
268                    }
269                }
270            }
271
272            if image_element.is_none() {
273                if let Some(candidate_image_element) = node.downcast::<HTMLImageElement>() {
274                    image_element = Some(DomRoot::from_ref(candidate_image_element))
275                }
276            }
277
278            if text_input_element.is_none() {
279                if let Some(candidate_text_input_element) = node.as_text_input() {
280                    text_input_element = Some(candidate_text_input_element);
281                }
282            }
283        }
284
285        let mut info = ContextMenuElementInformation::default();
286        let mut items = Vec::new();
287        if let Some(anchor_element) = anchor_element.as_ref() {
288            info.flags.insert(ContextMenuElementInformationFlags::Link);
289            info.link_url = anchor_element
290                .full_href_url_for_user_interface()
291                .map(ServoUrl::into_url);
292
293            items.extend(vec![
294                ContextMenuItem::Item {
295                    label: "Open Link in New View".into(),
296                    action: ContextMenuAction::OpenLinkInNewWebView,
297                    enabled: true,
298                },
299                ContextMenuItem::Item {
300                    label: "Copy Link".into(),
301                    action: ContextMenuAction::CopyLink,
302                    enabled: true,
303                },
304                ContextMenuItem::Separator,
305            ]);
306        }
307
308        if let Some(image_element) = image_element.as_ref() {
309            info.flags.insert(ContextMenuElementInformationFlags::Image);
310            info.image_url = image_element
311                .full_image_url_for_user_interface()
312                .map(ServoUrl::into_url);
313
314            items.extend(vec![
315                ContextMenuItem::Item {
316                    label: "Open Image in New View".into(),
317                    action: ContextMenuAction::OpenImageInNewView,
318                    enabled: true,
319                },
320                ContextMenuItem::Item {
321                    label: "Copy Image Link".into(),
322                    action: ContextMenuAction::CopyImageLink,
323                    enabled: true,
324                },
325                ContextMenuItem::Separator,
326            ]);
327        }
328
329        if let Some(text_input_element) = &text_input_element {
330            let has_selection = text_input_element.has_uncollapsed_selection();
331
332            info.flags
333                .insert(ContextMenuElementInformationFlags::EditableText);
334            if has_selection {
335                info.flags
336                    .insert(ContextMenuElementInformationFlags::Selection);
337            }
338
339            items.extend(vec![
340                ContextMenuItem::Item {
341                    label: "Cut".into(),
342                    action: ContextMenuAction::Cut,
343                    enabled: has_selection,
344                },
345                ContextMenuItem::Item {
346                    label: "Copy".into(),
347                    action: ContextMenuAction::Copy,
348                    enabled: has_selection,
349                },
350                ContextMenuItem::Item {
351                    label: "Paste".into(),
352                    action: ContextMenuAction::Paste,
353                    enabled: true,
354                },
355                ContextMenuItem::Item {
356                    label: "Select All".into(),
357                    action: ContextMenuAction::SelectAll,
358                    enabled: text_input_element.has_selectable_text(),
359                },
360                ContextMenuItem::Separator,
361            ]);
362        }
363
364        items.extend(vec![
365            ContextMenuItem::Item {
366                label: "Back".into(),
367                action: ContextMenuAction::GoBack,
368                enabled: true,
369            },
370            ContextMenuItem::Item {
371                label: "Forward".into(),
372                action: ContextMenuAction::GoForward,
373                enabled: true,
374            },
375            ContextMenuItem::Item {
376                label: "Reload".into(),
377                action: ContextMenuAction::Reload,
378                enabled: true,
379            },
380        ]);
381
382        let context_menu_nodes = ContextMenuNodes {
383            node: hit_test_result.node.clone(),
384            anchor_element,
385            image_element,
386            text_input_element,
387        };
388
389        self.show_embedder_control(
390            ControlElement::ContextMenu(context_menu_nodes),
391            EmbedderControlRequest::ContextMenu(ContextMenuRequest {
392                element_info: info,
393                items,
394            }),
395            Some(hit_test_result.point_in_frame.cast_unit()),
396        );
397    }
398}
399
400#[derive(JSTraceable, MallocSizeOf)]
401pub(crate) struct ContextMenuNodes {
402    /// The node that this menu was triggered on.
403    node: DomRoot<Node>,
404    /// The first inclusive ancestor of this node that is an `<a>` if one exists.
405    anchor_element: Option<DomRoot<HTMLAnchorElement>>,
406    /// The first inclusive ancestor of this node that is an `<img>` if one exists.
407    image_element: Option<DomRoot<HTMLImageElement>>,
408    /// The first inclusive ancestor of this node which is a text entry field.
409    text_input_element: Option<DomRoot<Element>>,
410}
411
412impl ContextMenuNodes {
413    fn handle_context_menu_action(&self, action: Option<ContextMenuAction>, can_gc: CanGc) {
414        let Some(action) = action else {
415            return;
416        };
417
418        let window = self.node.owner_window();
419        let document = window.Document();
420        let set_clipboard_text = |string: String| {
421            if string.is_empty() {
422                return;
423            }
424            window.send_to_embedder(EmbedderMsg::SetClipboardText(window.webview_id(), string));
425        };
426
427        let open_url_in_new_webview = |url: ServoUrl| {
428            let Some(browsing_context) = document.browsing_context() else {
429                return;
430            };
431            let (browsing_context, new) = browsing_context
432                .choose_browsing_context("_blank".into(), true /* nooopener */);
433            let Some(browsing_context) = browsing_context else {
434                return;
435            };
436            assert!(new);
437            let Some(target_document) = browsing_context.document() else {
438                return;
439            };
440
441            let target_window = target_document.window();
442            let target = Trusted::new(target_window);
443            let load_data = LoadData::new_for_new_unrelated_webview(url);
444            let task = task!(open_link_in_new_webview: move || {
445                target.root().load_url(NavigationHistoryBehavior::Replace, false, load_data, CanGc::note());
446            });
447            target_document
448                .owner_global()
449                .task_manager()
450                .dom_manipulation_task_source()
451                .queue(task);
452        };
453
454        match action {
455            ContextMenuAction::GoBack => {
456                let _ = window.History().Back();
457            },
458            ContextMenuAction::GoForward => {
459                let _ = window.History().Forward();
460            },
461            ContextMenuAction::Reload => {
462                window.Location().reload_without_origin_check(can_gc);
463            },
464            ContextMenuAction::CopyLink => {
465                let Some(anchor_element) = &self.anchor_element else {
466                    return;
467                };
468
469                let url_string = anchor_element
470                    .full_href_url_for_user_interface()
471                    .as_ref()
472                    .map(ServoUrl::to_string)
473                    .unwrap_or_else(|| anchor_element.Href().to_string());
474                set_clipboard_text(url_string);
475            },
476            ContextMenuAction::OpenLinkInNewWebView => {
477                let Some(anchor_element) = &self.anchor_element else {
478                    return;
479                };
480                if let Some(url) = anchor_element.full_href_url_for_user_interface() {
481                    open_url_in_new_webview(url);
482                };
483            },
484            ContextMenuAction::CopyImageLink => {
485                let Some(image_element) = &self.image_element else {
486                    return;
487                };
488                let url_string = image_element
489                    .full_image_url_for_user_interface()
490                    .as_ref()
491                    .map(ServoUrl::to_string)
492                    .unwrap_or_else(|| image_element.CurrentSrc().to_string());
493                set_clipboard_text(url_string.to_string());
494            },
495            ContextMenuAction::OpenImageInNewView => {
496                let Some(image_element) = &self.image_element else {
497                    return;
498                };
499                if let Some(url) = image_element.full_image_url_for_user_interface() {
500                    open_url_in_new_webview(url);
501                }
502            },
503            ContextMenuAction::Cut => {
504                window.Document().event_handler().handle_editing_action(
505                    self.text_input_element.clone(),
506                    EditingActionEvent::Cut,
507                    can_gc,
508                );
509            },
510            ContextMenuAction::Copy => {
511                window.Document().event_handler().handle_editing_action(
512                    self.text_input_element.clone(),
513                    EditingActionEvent::Copy,
514                    can_gc,
515                );
516            },
517            ContextMenuAction::Paste => {
518                window.Document().event_handler().handle_editing_action(
519                    self.text_input_element.clone(),
520                    EditingActionEvent::Paste,
521                    can_gc,
522                );
523            },
524            ContextMenuAction::SelectAll => {
525                if let Some(text_input_element) = &self.text_input_element {
526                    text_input_element.select_all();
527                }
528            },
529        }
530    }
531}
532
533impl Node {
534    fn as_text_input(&self) -> Option<DomRoot<Element>> {
535        if let Some(input_element) = self
536            .downcast::<HTMLInputElement>()
537            .filter(|input_element| input_element.renders_as_text_input_widget())
538        {
539            return Some(DomRoot::from_ref(input_element.upcast::<Element>()));
540        }
541        self.downcast::<HTMLTextAreaElement>()
542            .map(Castable::upcast)
543            .map(DomRoot::from_ref)
544    }
545}
546
547impl Element {
548    fn has_uncollapsed_selection(&self) -> bool {
549        self.downcast::<HTMLTextAreaElement>()
550            .map(TextControlElement::has_uncollapsed_selection)
551            .or(self
552                .downcast::<HTMLInputElement>()
553                .map(TextControlElement::has_uncollapsed_selection))
554            .unwrap_or_default()
555    }
556
557    fn has_selectable_text(&self) -> bool {
558        self.downcast::<HTMLTextAreaElement>()
559            .map(TextControlElement::has_selectable_text)
560            .or(self
561                .downcast::<HTMLInputElement>()
562                .map(TextControlElement::has_selectable_text))
563            .unwrap_or_default()
564    }
565
566    fn select_all(&self) {
567        self.downcast::<HTMLTextAreaElement>()
568            .map(TextControlElement::select_all)
569            .or(self
570                .downcast::<HTMLInputElement>()
571                .map(TextControlElement::select_all))
572            .unwrap_or_default()
573    }
574}