script/dom/
document_embedder_controls.rs

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