Skip to main content

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::cell::DomRefCell;
18use script_bindings::codegen::GenericBindings::HTMLAnchorElementBinding::HTMLAnchorElementMethods;
19use script_bindings::codegen::GenericBindings::HTMLImageElementBinding::HTMLImageElementMethods;
20use script_bindings::codegen::GenericBindings::HistoryBinding::HistoryMethods;
21use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
22use script_bindings::inheritance::Castable;
23use script_bindings::root::{Dom, DomRoot};
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::refcounted::Trusted;
32use crate::dom::bindings::trace::NoTrace;
33use crate::dom::inputevent::HitTestResult;
34use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
35use crate::dom::textcontrol::TextControlElement;
36use crate::dom::types::{
37    Element, HTMLAnchorElement, HTMLElement, HTMLImageElement, HTMLInputElement, HTMLSelectElement,
38    HTMLTextAreaElement, Window,
39};
40use crate::messaging::MainThreadScriptMsg;
41use crate::navigation::navigate;
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 main_thread_sender = self.window.main_thread_script_chan().clone();
137                let callback = profile_traits::generic_callback::GenericCallback::new(
138                    self.window.as_global_scope().time_profiler_chan().clone(),
139                    move |result| {
140                        let Ok(embedder_control_response) = result else {
141                            return;
142                        };
143                        if let Err(error) = main_thread_sender.send(
144                            MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager(
145                                id,
146                                embedder_control_response,
147                            ),
148                        ) {
149                            warn!("Could not send FileManager response to main thread: {error}")
150                        }
151                    },
152                )
153                .expect("Could not create callback");
154                self.window
155                    .as_global_scope()
156                    .resource_threads()
157                    .sender()
158                    .send(CoreResourceMsg::ToFileManager(
159                        FileManagerThreadMsg::SelectFiles(id, file_picker_request, callback),
160                    ))
161                    .unwrap();
162            },
163        }
164
165        id
166    }
167
168    pub(crate) fn hide_embedder_control(&self, element: &Element) {
169        self.visible_elements
170            .borrow_mut()
171            .retain(|index, control_element| {
172                if control_element.node() != element.upcast() {
173                    return true;
174                }
175                let id = EmbedderControlId {
176                    webview_id: self.window.webview_id(),
177                    pipeline_id: self.window.pipeline_id(),
178                    index: index.0,
179                };
180                self.window
181                    .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
182                false
183            });
184    }
185
186    pub(crate) fn handle_embedder_control_response(
187        &self,
188        cx: &mut JSContext,
189        id: EmbedderControlId,
190        response: EmbedderControlResponse,
191    ) {
192        assert_eq!(self.window.pipeline_id(), id.pipeline_id);
193        assert_eq!(self.window.webview_id(), id.webview_id);
194
195        let Some(element) = self.visible_elements.borrow_mut().remove(&id.index.into()) else {
196            return;
197        };
198
199        // Never process embedder responses on inactive `Document`s.
200        if !element.node().owner_doc().is_active() {
201            return;
202        }
203
204        match (element, response) {
205            (
206                ControlElement::Select(select_element),
207                EmbedderControlResponse::SelectElement(response),
208            ) => {
209                select_element.handle_embedder_response(cx, response);
210            },
211            (
212                ControlElement::ColorInput(input_element),
213                EmbedderControlResponse::ColorPicker(response),
214            ) => {
215                input_element.handle_color_picker_response(cx, response);
216            },
217            (
218                ControlElement::FileInput(input_element),
219                EmbedderControlResponse::FilePicker(response),
220            ) => {
221                input_element.handle_file_picker_response(cx, response);
222            },
223            (
224                ControlElement::ContextMenu(context_menu_nodes),
225                EmbedderControlResponse::ContextMenu(action),
226            ) => {
227                context_menu_nodes.handle_context_menu_action(action, cx);
228            },
229            (_, _) => unreachable!(
230                "The response to a form control should always match it's originating type."
231            ),
232        }
233    }
234
235    pub(crate) fn show_context_menu(&self, hit_test_result: &HitTestResult) {
236        {
237            let mut visible_elements = self.visible_elements.borrow_mut();
238            visible_elements.retain(|index, control_element| {
239                if matches!(control_element, ControlElement::ContextMenu(..)) {
240                    let id = EmbedderControlId {
241                        webview_id: self.window.webview_id(),
242                        pipeline_id: self.window.pipeline_id(),
243                        index: index.0,
244                    };
245                    self.window
246                        .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
247                    false
248                } else {
249                    true
250                }
251            });
252        }
253
254        let mut anchor_element = None;
255        let mut image_element = None;
256        let mut text_input_element = None;
257        for node in hit_test_result
258            .node
259            .inclusive_ancestors(ShadowIncluding::Yes)
260        {
261            if anchor_element.is_none() &&
262                let Some(candidate_anchor_element) = node.downcast::<HTMLAnchorElement>() &&
263                candidate_anchor_element.is_instance_activatable()
264            {
265                anchor_element = Some(DomRoot::from_ref(candidate_anchor_element));
266            }
267
268            if image_element.is_none() &&
269                let Some(candidate_image_element) = node.downcast::<HTMLImageElement>()
270            {
271                image_element = Some(DomRoot::from_ref(candidate_image_element))
272            }
273
274            if text_input_element.is_none() &&
275                let Some(candidate_text_input_element) = node.as_text_input()
276            {
277                text_input_element = Some(candidate_text_input_element);
278            }
279        }
280
281        let mut info = ContextMenuElementInformation::default();
282        let mut items = Vec::new();
283        if let Some(anchor_element) = anchor_element.as_ref() {
284            info.flags.insert(ContextMenuElementInformationFlags::Link);
285            info.link_url = anchor_element
286                .full_href_url_for_user_interface()
287                .map(ServoUrl::into_url);
288
289            items.extend(vec![
290                ContextMenuItem::Item {
291                    label: "Open Link in New View".into(),
292                    action: ContextMenuAction::OpenLinkInNewWebView,
293                    enabled: true,
294                },
295                ContextMenuItem::Item {
296                    label: "Copy Link".into(),
297                    action: ContextMenuAction::CopyLink,
298                    enabled: true,
299                },
300                ContextMenuItem::Separator,
301            ]);
302        }
303
304        if let Some(image_element) = image_element.as_ref() {
305            info.flags.insert(ContextMenuElementInformationFlags::Image);
306            info.image_url = image_element
307                .full_image_url_for_user_interface()
308                .map(ServoUrl::into_url);
309
310            items.extend(vec![
311                ContextMenuItem::Item {
312                    label: "Open Image in New View".into(),
313                    action: ContextMenuAction::OpenImageInNewView,
314                    enabled: true,
315                },
316                ContextMenuItem::Item {
317                    label: "Copy Image Link".into(),
318                    action: ContextMenuAction::CopyImageLink,
319                    enabled: true,
320                },
321                ContextMenuItem::Separator,
322            ]);
323        }
324
325        if let Some(text_input_element) = &text_input_element {
326            let has_selection = text_input_element.has_uncollapsed_selection();
327
328            info.flags
329                .insert(ContextMenuElementInformationFlags::EditableText);
330            if has_selection {
331                info.flags
332                    .insert(ContextMenuElementInformationFlags::Selection);
333            }
334
335            items.extend(vec![
336                ContextMenuItem::Item {
337                    label: "Cut".into(),
338                    action: ContextMenuAction::Cut,
339                    enabled: has_selection,
340                },
341                ContextMenuItem::Item {
342                    label: "Copy".into(),
343                    action: ContextMenuAction::Copy,
344                    enabled: has_selection,
345                },
346                ContextMenuItem::Item {
347                    label: "Paste".into(),
348                    action: ContextMenuAction::Paste,
349                    enabled: true,
350                },
351                ContextMenuItem::Item {
352                    label: "Select All".into(),
353                    action: ContextMenuAction::SelectAll,
354                    enabled: text_input_element.has_selectable_text(),
355                },
356                ContextMenuItem::Separator,
357            ]);
358        }
359
360        items.extend(vec![
361            ContextMenuItem::Item {
362                label: "Back".into(),
363                action: ContextMenuAction::GoBack,
364                enabled: true,
365            },
366            ContextMenuItem::Item {
367                label: "Forward".into(),
368                action: ContextMenuAction::GoForward,
369                enabled: true,
370            },
371            ContextMenuItem::Item {
372                label: "Reload".into(),
373                action: ContextMenuAction::Reload,
374                enabled: true,
375            },
376        ]);
377
378        let context_menu_nodes = ContextMenuNodes {
379            node: hit_test_result.node.clone(),
380            anchor_element,
381            image_element,
382            text_input_element,
383        };
384
385        self.show_embedder_control(
386            ControlElement::ContextMenu(context_menu_nodes),
387            EmbedderControlRequest::ContextMenu(ContextMenuRequest {
388                element_info: info,
389                items,
390            }),
391            Some(hit_test_result.point_in_frame.cast_unit()),
392        );
393    }
394}
395
396#[derive(JSTraceable, MallocSizeOf)]
397pub(crate) struct ContextMenuNodes {
398    /// The node that this menu was triggered on.
399    node: DomRoot<Node>,
400    /// The first inclusive ancestor of this node that is an `<a>` if one exists.
401    anchor_element: Option<DomRoot<HTMLAnchorElement>>,
402    /// The first inclusive ancestor of this node that is an `<img>` if one exists.
403    image_element: Option<DomRoot<HTMLImageElement>>,
404    /// The first inclusive ancestor of this node which is a text entry field.
405    text_input_element: Option<DomRoot<Element>>,
406}
407
408impl ContextMenuNodes {
409    fn handle_context_menu_action(&self, action: Option<ContextMenuAction>, cx: &mut JSContext) {
410        let Some(action) = action else {
411            return;
412        };
413
414        let window = self.node.owner_window();
415        let document = window.Document();
416        let set_clipboard_text = |string: String| {
417            if string.is_empty() {
418                return;
419            }
420            window.send_to_embedder(EmbedderMsg::SetClipboardText(window.webview_id(), string));
421        };
422
423        let open_url_in_new_webview = |url: ServoUrl| {
424            let Some(browsing_context) = document.browsing_context() else {
425                return;
426            };
427            let (browsing_context, new) = browsing_context
428                .choose_browsing_context("_blank".into(), true /* nooopener */);
429            let Some(browsing_context) = browsing_context else {
430                return;
431            };
432            assert!(new);
433            let Some(target_document) = browsing_context.document() else {
434                return;
435            };
436
437            let target_window = target_document.window();
438            let target = Trusted::new(target_window);
439            let load_data = LoadData::new_for_new_unrelated_webview(url);
440            let task = task!(open_link_in_new_webview: move |cx| {
441                navigate(cx, &target.root(), NavigationHistoryBehavior::Replace, false, load_data);
442            });
443            target_document
444                .owner_global()
445                .task_manager()
446                .dom_manipulation_task_source()
447                .queue(task);
448        };
449
450        match action {
451            ContextMenuAction::GoBack => {
452                let _ = window.History().Back();
453            },
454            ContextMenuAction::GoForward => {
455                let _ = window.History().Forward();
456            },
457            ContextMenuAction::Reload => {
458                window.Location(cx).reload_without_origin_check(cx);
459            },
460            ContextMenuAction::CopyLink => {
461                let Some(anchor_element) = &self.anchor_element else {
462                    return;
463                };
464
465                let url_string = anchor_element
466                    .full_href_url_for_user_interface()
467                    .as_ref()
468                    .map(ServoUrl::to_string)
469                    .unwrap_or_else(|| anchor_element.Href().to_string());
470                set_clipboard_text(url_string);
471            },
472            ContextMenuAction::OpenLinkInNewWebView => {
473                let Some(anchor_element) = &self.anchor_element else {
474                    return;
475                };
476                if let Some(url) = anchor_element.full_href_url_for_user_interface() {
477                    open_url_in_new_webview(url);
478                };
479            },
480            ContextMenuAction::CopyImageLink => {
481                let Some(image_element) = &self.image_element else {
482                    return;
483                };
484                let url_string = image_element
485                    .full_image_url_for_user_interface()
486                    .as_ref()
487                    .map(ServoUrl::to_string)
488                    .unwrap_or_else(|| image_element.CurrentSrc().to_string());
489                set_clipboard_text(url_string);
490            },
491            ContextMenuAction::OpenImageInNewView => {
492                let Some(image_element) = &self.image_element else {
493                    return;
494                };
495                if let Some(url) = image_element.full_image_url_for_user_interface() {
496                    open_url_in_new_webview(url);
497                }
498            },
499            ContextMenuAction::Cut => {
500                window.Document().event_handler().handle_editing_action(
501                    cx,
502                    self.text_input_element.clone(),
503                    EditingActionEvent::Cut,
504                );
505            },
506            ContextMenuAction::Copy => {
507                window.Document().event_handler().handle_editing_action(
508                    cx,
509                    self.text_input_element.clone(),
510                    EditingActionEvent::Copy,
511                );
512            },
513            ContextMenuAction::Paste => {
514                window.Document().event_handler().handle_editing_action(
515                    cx,
516                    self.text_input_element.clone(),
517                    EditingActionEvent::Paste,
518                );
519            },
520            ContextMenuAction::SelectAll => {
521                if let Some(text_input_element) = &self.text_input_element {
522                    text_input_element.select_all();
523                }
524            },
525        }
526    }
527}
528
529impl Node {
530    fn as_text_input(&self) -> Option<DomRoot<Element>> {
531        if let Some(input_element) = self
532            .downcast::<HTMLInputElement>()
533            .filter(|input_element| input_element.is_textual_or_password())
534        {
535            return Some(DomRoot::from_ref(input_element.upcast::<Element>()));
536        }
537        self.downcast::<HTMLTextAreaElement>()
538            .map(Castable::upcast)
539            .map(DomRoot::from_ref)
540    }
541}
542
543impl Element {
544    fn has_uncollapsed_selection(&self) -> bool {
545        self.downcast::<HTMLTextAreaElement>()
546            .map(TextControlElement::has_uncollapsed_selection)
547            .or(self
548                .downcast::<HTMLInputElement>()
549                .map(TextControlElement::has_uncollapsed_selection))
550            .unwrap_or_default()
551    }
552
553    fn has_selectable_text(&self) -> bool {
554        self.downcast::<HTMLTextAreaElement>()
555            .map(TextControlElement::has_selectable_text)
556            .or(self
557                .downcast::<HTMLInputElement>()
558                .map(TextControlElement::has_selectable_text))
559            .unwrap_or_default()
560    }
561
562    fn select_all(&self) {
563        self.downcast::<HTMLTextAreaElement>()
564            .map(TextControlElement::select_all)
565            .or(self
566                .downcast::<HTMLInputElement>()
567                .map(TextControlElement::select_all))
568            .unwrap_or_default()
569    }
570}