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