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 embedder_traits::{
9    EmbedderControlId, EmbedderControlRequest, EmbedderControlResponse, EmbedderMsg,
10};
11use euclid::{Point2D, Rect, Size2D};
12use ipc_channel::router::ROUTER;
13use net_traits::CoreResourceMsg;
14use net_traits::filemanager_thread::FileManagerThreadMsg;
15use rustc_hash::FxHashMap;
16use script_bindings::root::{Dom, DomRoot};
17use script_bindings::script_runtime::CanGc;
18use webrender_api::units::DeviceIntRect;
19
20use crate::dom::bindings::cell::DomRefCell;
21use crate::dom::bindings::inheritance::Castable as _;
22use crate::dom::bindings::trace::NoTrace;
23use crate::dom::node::Node;
24use crate::dom::types::{Element, HTMLElement, HTMLInputElement, HTMLSelectElement, Window};
25use crate::messaging::MainThreadScriptMsg;
26
27#[derive(JSTraceable, MallocSizeOf)]
28pub(crate) enum ControlElement {
29    Select(DomRoot<HTMLSelectElement>),
30    ColorInput(DomRoot<HTMLInputElement>),
31    FileInput(DomRoot<HTMLInputElement>),
32    Ime(DomRoot<HTMLElement>),
33}
34
35impl ControlElement {
36    fn element(&self) -> &Element {
37        match self {
38            ControlElement::Select(element) => element.upcast::<Element>(),
39            ControlElement::ColorInput(element) => element.upcast::<Element>(),
40            ControlElement::FileInput(element) => element.upcast::<Element>(),
41            ControlElement::Ime(element) => element.upcast::<Element>(),
42        }
43    }
44}
45
46#[derive(JSTraceable, MallocSizeOf)]
47#[cfg_attr(crown, allow(crown::unrooted_must_root))]
48pub(crate) struct DocumentEmbedderControls {
49    /// The [`Window`] element for this [`DocumentUserInterfaceElements`].
50    window: Dom<Window>,
51    /// The id of the next user interface element that the `Document` requests that the
52    /// embedder show. This is used to track user interface elements in the API.
53    #[no_trace]
54    user_interface_element_index: Cell<Epoch>,
55    /// A map of visible user interface elements.
56    visible_elements: DomRefCell<FxHashMap<NoTrace<Epoch>, ControlElement>>,
57}
58
59impl DocumentEmbedderControls {
60    pub fn new(window: &Window) -> Self {
61        Self {
62            window: Dom::from_ref(window),
63            user_interface_element_index: Default::default(),
64            visible_elements: Default::default(),
65        }
66    }
67
68    /// Generate the next unused [`EmbedderControlId`]. This method is only needed for some older
69    /// types of controls that are still being migrated, and it will eventually be removed.
70    pub(crate) fn next_control_id(&self) -> EmbedderControlId {
71        let index = self.user_interface_element_index.get();
72        self.user_interface_element_index.set(index.next());
73        EmbedderControlId {
74            webview_id: self.window.webview_id(),
75            pipeline_id: self.window.pipeline_id(),
76            index,
77        }
78    }
79
80    pub(crate) fn show_embedder_control(
81        &self,
82        element: ControlElement,
83        request: EmbedderControlRequest,
84    ) -> EmbedderControlId {
85        let id = self.next_control_id();
86        let rect = element
87            .element()
88            .upcast::<Node>()
89            .border_box()
90            .unwrap_or_default();
91        let rect = Rect::new(
92            Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
93            Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
94        );
95        // FIXME: this is a CSS px rect, not a device rect
96        let rect = DeviceIntRect::from_untyped(&rect.to_box2d());
97        self.visible_elements
98            .borrow_mut()
99            .insert(id.index.into(), element);
100
101        match request {
102            EmbedderControlRequest::SelectElement(..) |
103            EmbedderControlRequest::ColorPicker(..) |
104            EmbedderControlRequest::InputMethod(..) => self
105                .window
106                .send_to_embedder(EmbedderMsg::ShowEmbedderControl(id, rect, request)),
107            EmbedderControlRequest::FilePicker(file_picker_request) => {
108                let (sender, receiver) = profile_traits::ipc::channel(
109                    self.window.as_global_scope().time_profiler_chan().clone(),
110                )
111                .expect("Error initializing channel");
112                let main_thread_sender = self.window.main_thread_script_chan().clone();
113                ROUTER.add_typed_route(
114                    receiver.to_ipc_receiver(),
115                    Box::new(move |result| {
116                        let Ok(embedder_control_response) = result else {
117                            return;
118                        };
119                        if let Err(error) = main_thread_sender.send(
120                            MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager(
121                                id,
122                                embedder_control_response,
123                            ),
124                        ) {
125                            warn!("Could not send FileManager response to main thread: {error}")
126                        }
127                    }),
128                );
129                self.window
130                    .as_global_scope()
131                    .resource_threads()
132                    .sender()
133                    .send(CoreResourceMsg::ToFileManager(
134                        FileManagerThreadMsg::SelectFiles(id, file_picker_request, sender),
135                    ))
136                    .unwrap();
137            },
138        }
139
140        id
141    }
142
143    pub(crate) fn hide_embedder_control(&self, element: &Element) {
144        self.visible_elements
145            .borrow_mut()
146            .retain(|index, control_element| {
147                if control_element.element() != element {
148                    return true;
149                }
150                let id = EmbedderControlId {
151                    webview_id: self.window.webview_id(),
152                    pipeline_id: self.window.pipeline_id(),
153                    index: index.0,
154                };
155                self.window
156                    .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
157                false
158            });
159    }
160
161    pub(crate) fn handle_embedder_control_response(
162        &self,
163        id: EmbedderControlId,
164        response: EmbedderControlResponse,
165        can_gc: CanGc,
166    ) {
167        assert_eq!(self.window.pipeline_id(), id.pipeline_id);
168        assert_eq!(self.window.webview_id(), id.webview_id);
169
170        let Some(element) = self.visible_elements.borrow_mut().remove(&id.index.into()) else {
171            return;
172        };
173
174        match (element, response) {
175            (
176                ControlElement::Select(select_element),
177                EmbedderControlResponse::SelectElement(response),
178            ) => {
179                select_element.handle_menu_response(response, can_gc);
180            },
181            (
182                ControlElement::ColorInput(input_element),
183                EmbedderControlResponse::ColorPicker(response),
184            ) => {
185                input_element.handle_color_picker_response(response, can_gc);
186            },
187            (
188                ControlElement::FileInput(input_element),
189                EmbedderControlResponse::FilePicker(response),
190            ) => {
191                input_element.handle_file_picker_response(response, can_gc);
192            },
193            (_, _) => unreachable!(
194                "The response to a form control should always match it's originating type."
195            ),
196        }
197    }
198}