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::html::form_controls::text_control::TextControlElement;
34use crate::dom::inputevent::HitTestResult;
35use crate::dom::iterators::ShadowIncluding;
36use crate::dom::node::{Node, NodeTraits};
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)]
45#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
46pub(crate) enum ControlElement {
47    Select(Dom<HTMLSelectElement>),
48    ColorInput(Dom<HTMLInputElement>),
49    FileInput(Dom<HTMLInputElement>),
50    Ime(Dom<HTMLElement>),
51    ContextMenu(ContextMenuNodes),
52}
53
54impl ControlElement {
55    fn node(&self) -> &Node {
56        match self {
57            ControlElement::Select(element) => element.upcast::<Node>(),
58            ControlElement::ColorInput(element) => element.upcast::<Node>(),
59            ControlElement::FileInput(element) => element.upcast::<Node>(),
60            ControlElement::Ime(element) => element.upcast::<Node>(),
61            ControlElement::ContextMenu(context_menu_nodes) => &context_menu_nodes.node,
62        }
63    }
64}
65
66impl js::gc::Rootable for ControlElement {}
67
68#[derive(JSTraceable, MallocSizeOf)]
69#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
70pub(crate) struct DocumentEmbedderControls {
71    /// The [`Window`] element for this [`DocumentUserInterfaceElements`].
72    window: Dom<Window>,
73    /// The id of the next user interface element that the `Document` requests that the
74    /// embedder show. This is used to track user interface elements in the API.
75    #[no_trace]
76    user_interface_element_index: Cell<Epoch>,
77    /// A map of visible user interface elements.
78    visible_elements: DomRefCell<FxHashMap<NoTrace<Epoch>, ControlElement>>,
79}
80
81impl DocumentEmbedderControls {
82    pub fn new(window: &Window) -> Self {
83        Self {
84            window: Dom::from_ref(window),
85            user_interface_element_index: Default::default(),
86            visible_elements: Default::default(),
87        }
88    }
89
90    /// Generate the next unused [`EmbedderControlId`]. This method is only needed for some older
91    /// types of controls that are still being migrated, and it will eventually be removed.
92    pub(crate) fn next_control_id(&self) -> EmbedderControlId {
93        let index = self.user_interface_element_index.get();
94        self.user_interface_element_index.set(index.next());
95        EmbedderControlId {
96            webview_id: self.window.webview_id(),
97            pipeline_id: self.window.pipeline_id(),
98            index,
99        }
100    }
101
102    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
103    pub(crate) fn show_embedder_control(
104        &self,
105        element: ControlElement,
106        request: EmbedderControlRequest,
107        point: Option<DevicePoint>,
108    ) -> EmbedderControlId {
109        let id = self.next_control_id();
110        let rect = point
111            .map(|point| DeviceIntRect::from_origin_and_size(point.to_i32(), Size2D::zero()))
112            .unwrap_or_else(|| {
113                let rect = element
114                    .node()
115                    .upcast::<Node>()
116                    .border_box()
117                    .unwrap_or_default();
118
119                let rect = Rect::new(
120                    Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
121                    Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
122                );
123
124                // FIXME: This is a CSS pixel rect relative to this frame, we need a DevicePixel rectangle
125                // relative to the entire WebView!
126                DeviceIntRect::from_untyped(&rect.to_box2d())
127            });
128
129        self.visible_elements
130            .borrow_mut()
131            .insert(id.index.into(), element);
132
133        self.send_embedder_control_request(request, id, rect);
134        id
135    }
136
137    fn send_embedder_control_request(
138        &self,
139        request: EmbedderControlRequest,
140        id: EmbedderControlId,
141        rect: DeviceIntRect,
142    ) {
143        match request {
144            EmbedderControlRequest::SelectElement(..) |
145            EmbedderControlRequest::ColorPicker(..) |
146            EmbedderControlRequest::InputMethod(..) |
147            EmbedderControlRequest::ContextMenu(..) => self
148                .window
149                .send_to_embedder(EmbedderMsg::ShowEmbedderControl(id, rect, request)),
150            EmbedderControlRequest::FilePicker(file_picker_request) => {
151                let main_thread_sender = self.window.main_thread_script_chan().clone();
152                let callback = profile_traits::generic_callback::GenericCallback::new(
153                    self.window.as_global_scope().time_profiler_chan().clone(),
154                    move |result| {
155                        let Ok(embedder_control_response) = result else {
156                            return;
157                        };
158                        if let Err(error) = main_thread_sender.send(
159                            MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager(
160                                id,
161                                embedder_control_response,
162                            ),
163                        ) {
164                            warn!("Could not send FileManager response to main thread: {error}")
165                        }
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, 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        let mut text_input_element = None;
272        for node in hit_test_result
273            .node
274            .inclusive_ancestors(ShadowIncluding::Yes)
275        {
276            if anchor_element.is_none() &&
277                let Some(candidate_anchor_element) = node.downcast::<HTMLAnchorElement>() &&
278                candidate_anchor_element.is_instance_activatable()
279            {
280                anchor_element = Some(DomRoot::from_ref(candidate_anchor_element));
281            }
282
283            if image_element.is_none() &&
284                let Some(candidate_image_element) = node.downcast::<HTMLImageElement>()
285            {
286                image_element = Some(DomRoot::from_ref(candidate_image_element))
287            }
288
289            if text_input_element.is_none() &&
290                let Some(candidate_text_input_element) = node.as_text_input()
291            {
292                text_input_element = Some(candidate_text_input_element);
293            }
294        }
295
296        let mut info = ContextMenuElementInformation::default();
297        let mut items = Vec::new();
298        if let Some(anchor_element) = anchor_element.as_ref() {
299            info.flags.insert(ContextMenuElementInformationFlags::Link);
300            info.link_url = anchor_element
301                .full_href_url_for_user_interface()
302                .map(ServoUrl::into_url);
303
304            items.extend(vec![
305                ContextMenuItem::Item {
306                    label: "Open Link in New View".into(),
307                    action: ContextMenuAction::OpenLinkInNewWebView,
308                    enabled: true,
309                },
310                ContextMenuItem::Item {
311                    label: "Copy Link".into(),
312                    action: ContextMenuAction::CopyLink,
313                    enabled: true,
314                },
315                ContextMenuItem::Separator,
316            ]);
317        }
318
319        if let Some(image_element) = image_element.as_ref() {
320            info.flags.insert(ContextMenuElementInformationFlags::Image);
321            info.image_url = image_element
322                .full_image_url_for_user_interface()
323                .map(ServoUrl::into_url);
324
325            items.extend(vec![
326                ContextMenuItem::Item {
327                    label: "Open Image in New View".into(),
328                    action: ContextMenuAction::OpenImageInNewView,
329                    enabled: true,
330                },
331                ContextMenuItem::Item {
332                    label: "Copy Image Link".into(),
333                    action: ContextMenuAction::CopyImageLink,
334                    enabled: true,
335                },
336                ContextMenuItem::Separator,
337            ]);
338        }
339
340        if let Some(text_input_element) = &text_input_element {
341            let has_selection = text_input_element.has_uncollapsed_selection();
342
343            info.flags
344                .insert(ContextMenuElementInformationFlags::EditableText);
345            if has_selection {
346                info.flags
347                    .insert(ContextMenuElementInformationFlags::Selection);
348            }
349
350            items.extend(vec![
351                ContextMenuItem::Item {
352                    label: "Cut".into(),
353                    action: ContextMenuAction::Cut,
354                    enabled: has_selection,
355                },
356                ContextMenuItem::Item {
357                    label: "Copy".into(),
358                    action: ContextMenuAction::Copy,
359                    enabled: has_selection,
360                },
361                ContextMenuItem::Item {
362                    label: "Paste".into(),
363                    action: ContextMenuAction::Paste,
364                    enabled: true,
365                },
366                ContextMenuItem::Item {
367                    label: "Select All".into(),
368                    action: ContextMenuAction::SelectAll,
369                    enabled: text_input_element.has_selectable_text(),
370                },
371                ContextMenuItem::Separator,
372            ]);
373        }
374
375        items.extend(vec![
376            ContextMenuItem::Item {
377                label: "Back".into(),
378                action: ContextMenuAction::GoBack,
379                enabled: true,
380            },
381            ContextMenuItem::Item {
382                label: "Forward".into(),
383                action: ContextMenuAction::GoForward,
384                enabled: true,
385            },
386            ContextMenuItem::Item {
387                label: "Reload".into(),
388                action: ContextMenuAction::Reload,
389                enabled: true,
390            },
391        ]);
392
393        self.show_embedder_control(
394            ControlElement::ContextMenu(ContextMenuNodes {
395                node: hit_test_result.node.as_traced(),
396                anchor_element: anchor_element.map(|element| element.as_traced()),
397                image_element: image_element.map(|element| element.as_traced()),
398                text_input_element: text_input_element.map(|element| element.as_traced()),
399            }),
400            EmbedderControlRequest::ContextMenu(ContextMenuRequest {
401                element_info: info,
402                items,
403            }),
404            Some(hit_test_result.point_in_frame.cast_unit()),
405        );
406    }
407}
408
409#[derive(JSTraceable, MallocSizeOf)]
410#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
411pub(crate) struct ContextMenuNodes {
412    /// The node that this menu was triggered on.
413    node: Dom<Node>,
414    /// The first inclusive ancestor of this node that is an `<a>` if one exists.
415    anchor_element: Option<Dom<HTMLAnchorElement>>,
416    /// The first inclusive ancestor of this node that is an `<img>` if one exists.
417    image_element: Option<Dom<HTMLImageElement>>,
418    /// The first inclusive ancestor of this node which is a text entry field.
419    text_input_element: Option<Dom<Element>>,
420}
421
422impl ContextMenuNodes {
423    fn handle_context_menu_action(&self, action: Option<ContextMenuAction>, cx: &mut JSContext) {
424        let Some(action) = action else {
425            return;
426        };
427
428        let window = self.node.owner_window();
429        let document = window.Document();
430        let set_clipboard_text = |string: String| {
431            if string.is_empty() {
432                return;
433            }
434            window.send_to_embedder(EmbedderMsg::SetClipboardText(window.webview_id(), string));
435        };
436
437        let open_url_in_new_webview = |cx: &mut JSContext, url: ServoUrl| {
438            let Some(browsing_context) = document.browsing_context() else {
439                return;
440            };
441            let (browsing_context, new) = browsing_context.choose_browsing_context(
442                cx,
443                "_blank".into(),
444                true, /* nooopener */
445            );
446            let Some(browsing_context) = browsing_context else {
447                return;
448            };
449            assert!(new);
450            let Some(target_document) = browsing_context.document() else {
451                return;
452            };
453
454            let target_window = target_document.window();
455            let target = Trusted::new(target_window);
456            let load_data = LoadData::new_for_new_unrelated_webview(url);
457            let task = task!(open_link_in_new_webview: move |cx| {
458                navigate(cx, &target.root(), NavigationHistoryBehavior::Replace, false, load_data);
459            });
460            target_document
461                .owner_global()
462                .task_manager()
463                .dom_manipulation_task_source()
464                .queue(task);
465        };
466
467        match action {
468            ContextMenuAction::GoBack => {
469                let _ = window.History(cx).Back();
470            },
471            ContextMenuAction::GoForward => {
472                let _ = window.History(cx).Forward();
473            },
474            ContextMenuAction::Reload => {
475                window.Location(cx).reload_without_origin_check(cx);
476            },
477            ContextMenuAction::CopyLink => {
478                let Some(anchor_element) = &self.anchor_element else {
479                    return;
480                };
481
482                let url_string = anchor_element
483                    .full_href_url_for_user_interface()
484                    .as_ref()
485                    .map(ServoUrl::to_string)
486                    .unwrap_or_else(|| String::from(anchor_element.Href()));
487                set_clipboard_text(url_string);
488            },
489            ContextMenuAction::OpenLinkInNewWebView => {
490                let Some(anchor_element) = &self.anchor_element else {
491                    return;
492                };
493                if let Some(url) = anchor_element.full_href_url_for_user_interface() {
494                    open_url_in_new_webview(cx, url);
495                };
496            },
497            ContextMenuAction::CopyImageLink => {
498                let Some(image_element) = &self.image_element else {
499                    return;
500                };
501                let url_string = image_element
502                    .full_image_url_for_user_interface()
503                    .as_ref()
504                    .map(ServoUrl::to_string)
505                    .unwrap_or_else(|| String::from(image_element.CurrentSrc()));
506                set_clipboard_text(url_string);
507            },
508            ContextMenuAction::OpenImageInNewView => {
509                let Some(image_element) = &self.image_element else {
510                    return;
511                };
512                if let Some(url) = image_element.full_image_url_for_user_interface() {
513                    open_url_in_new_webview(cx, url);
514                }
515            },
516            ContextMenuAction::Cut => {
517                window.Document().event_handler().handle_editing_action(
518                    cx,
519                    self.text_input_element.as_deref().map(DomRoot::from_ref),
520                    EditingActionEvent::Cut,
521                );
522            },
523            ContextMenuAction::Copy => {
524                window.Document().event_handler().handle_editing_action(
525                    cx,
526                    self.text_input_element.as_deref().map(DomRoot::from_ref),
527                    EditingActionEvent::Copy,
528                );
529            },
530            ContextMenuAction::Paste => {
531                window.Document().event_handler().handle_editing_action(
532                    cx,
533                    self.text_input_element.as_deref().map(DomRoot::from_ref),
534                    EditingActionEvent::Paste,
535                );
536            },
537            ContextMenuAction::SelectAll => {
538                if let Some(text_input_element) = &self.text_input_element {
539                    text_input_element.select_all();
540                }
541            },
542        }
543    }
544}
545
546impl Node {
547    fn as_text_input(&self) -> Option<DomRoot<Element>> {
548        if let Some(input_element) = self
549            .downcast::<HTMLInputElement>()
550            .filter(|input_element| input_element.is_textual_or_password())
551        {
552            return Some(DomRoot::from_ref(input_element.upcast::<Element>()));
553        }
554        self.downcast::<HTMLTextAreaElement>()
555            .map(Castable::upcast)
556            .map(DomRoot::from_ref)
557    }
558}
559
560impl Element {
561    fn has_uncollapsed_selection(&self) -> bool {
562        self.downcast::<HTMLTextAreaElement>()
563            .map(TextControlElement::has_uncollapsed_selection)
564            .or(self
565                .downcast::<HTMLInputElement>()
566                .map(TextControlElement::has_uncollapsed_selection))
567            .unwrap_or_default()
568    }
569
570    fn has_selectable_text(&self) -> bool {
571        self.downcast::<HTMLTextAreaElement>()
572            .map(TextControlElement::has_selectable_text)
573            .or(self
574                .downcast::<HTMLInputElement>()
575                .map(TextControlElement::has_selectable_text))
576            .unwrap_or_default()
577    }
578
579    fn select_all(&self) {
580        self.downcast::<HTMLTextAreaElement>()
581            .map(TextControlElement::select_all)
582            .or(self
583                .downcast::<HTMLInputElement>()
584                .map(TextControlElement::select_all))
585            .unwrap_or_default()
586    }
587}