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