1#![cfg_attr(crown, allow(crown::jscontext_first_arg))]
6
7use std::cell::Cell;
8
9use embedder_traits::{
10 ClipboardAction, ContextMenuAction, ContextMenuElementInformation,
11 ContextMenuElementInformationFlags, ContextMenuItem, ContextMenuRequest, EmbedderControlId,
12 EmbedderControlRequest, EmbedderControlResponse, EmbedderMsg,
13};
14use euclid::{Point2D, Rect, Size2D};
15use js::context::JSContext;
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 servo_base::Epoch;
27use servo_base::generic_channel::GenericSend;
28use servo_url::ServoUrl;
29use webrender_api::units::{DeviceIntRect, DevicePoint};
30
31use crate::dom::activation::Activatable;
32use crate::dom::bindings::trace::NoTrace;
33use crate::dom::inputevent::HitTestResult;
34use crate::dom::iterators::ShadowIncluding;
35use crate::dom::links::htmlhyperlinkelementutils::HyperlinkElementTraits;
36use crate::dom::node::{Node, NodeTraits};
37use crate::dom::types::{
38 Element, HTMLAnchorElement, HTMLElement, HTMLImageElement, HTMLInputElement, HTMLSelectElement,
39 Window,
40};
41use crate::messaging::MainThreadScriptMsg;
42
43#[derive(JSTraceable, MallocSizeOf)]
44#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
45pub(crate) enum ControlElement {
46 Select(Dom<HTMLSelectElement>),
47 ColorInput(Dom<HTMLInputElement>),
48 FileInput(Dom<HTMLInputElement>),
49 Ime(Dom<HTMLElement>),
50 ContextMenu(ContextMenuNodes),
51}
52
53impl ControlElement {
54 fn node(&self) -> &Node {
55 match self {
56 ControlElement::Select(element) => element.upcast::<Node>(),
57 ControlElement::ColorInput(element) => element.upcast::<Node>(),
58 ControlElement::FileInput(element) => element.upcast::<Node>(),
59 ControlElement::Ime(element) => element.upcast::<Node>(),
60 ControlElement::ContextMenu(context_menu_nodes) => &context_menu_nodes.node,
61 }
62 }
63}
64
65impl js::gc::Rootable for ControlElement {}
66
67#[derive(JSTraceable, MallocSizeOf)]
68#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
69pub(crate) struct DocumentEmbedderControls {
70 window: Dom<Window>,
72 #[no_trace]
75 user_interface_element_index: Cell<Epoch>,
76 visible_elements: DomRefCell<FxHashMap<NoTrace<Epoch>, ControlElement>>,
78}
79
80impl DocumentEmbedderControls {
81 pub fn new(window: &Window) -> Self {
82 Self {
83 window: Dom::from_ref(window),
84 user_interface_element_index: Default::default(),
85 visible_elements: Default::default(),
86 }
87 }
88
89 pub(crate) fn next_control_id(&self) -> EmbedderControlId {
92 let index = self.user_interface_element_index.get();
93 self.user_interface_element_index.set(index.next());
94 EmbedderControlId {
95 webview_id: self.window.webview_id(),
96 pipeline_id: self.window.pipeline_id(),
97 index,
98 }
99 }
100
101 #[cfg_attr(crown, expect(crown::unrooted_must_root))]
102 pub(crate) fn show_embedder_control(
103 &self,
104 cx: &mut JSContext,
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(cx)
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 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 =
153 profile_traits::generic_callback::GenericCallback::new(move |result| {
154 let Ok(embedder_control_response) = result else {
155 return;
156 };
157 if let Err(error) = main_thread_sender.send(
158 MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager(
159 id,
160 embedder_control_response,
161 ),
162 ) {
163 warn!("Could not send FileManager response to main thread: {error}")
164 }
165 })
166 .expect("Could not create callback");
167 self.window
168 .as_global_scope()
169 .resource_threads()
170 .sender()
171 .send(CoreResourceMsg::ToFileManager(
172 FileManagerThreadMsg::SelectFiles(id, file_picker_request, callback),
173 ))
174 .unwrap();
175 },
176 }
177 }
178
179 pub(crate) fn hide_embedder_control(&self, element: &Element) {
180 self.visible_elements
181 .borrow_mut()
182 .retain(|index, control_element| {
183 if control_element.node() != element.upcast() {
184 return true;
185 }
186 let id = EmbedderControlId {
187 webview_id: self.window.webview_id(),
188 pipeline_id: self.window.pipeline_id(),
189 index: index.0,
190 };
191 self.window
192 .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
193 false
194 });
195 }
196
197 pub(crate) fn handle_embedder_control_response(
198 &self,
199 cx: &mut JSContext,
200 id: EmbedderControlId,
201 response: EmbedderControlResponse,
202 ) {
203 assert_eq!(self.window.pipeline_id(), id.pipeline_id);
204 assert_eq!(self.window.webview_id(), id.webview_id);
205
206 rooted!(&in(cx) let visible_element = self.visible_elements.borrow_mut().remove(&id.index.into()));
207
208 let Some(element) = &*visible_element else {
209 return;
210 };
211
212 if !element.node().owner_doc().is_active() {
214 return;
215 }
216
217 match (element, response) {
218 (
219 ControlElement::Select(select_element),
220 EmbedderControlResponse::SelectElement(response),
221 ) => {
222 select_element.handle_embedder_response(cx, response);
223 },
224 (
225 ControlElement::ColorInput(input_element),
226 EmbedderControlResponse::ColorPicker(response),
227 ) => {
228 input_element.handle_color_picker_response(cx, response);
229 },
230 (
231 ControlElement::FileInput(input_element),
232 EmbedderControlResponse::FilePicker(response),
233 ) => {
234 input_element.handle_file_picker_response(cx, response);
235 },
236 (
237 ControlElement::ContextMenu(context_menu_nodes),
238 EmbedderControlResponse::ContextMenu(action),
239 ) => {
240 context_menu_nodes.handle_context_menu_action(action, cx);
241 },
242 (_, _) => unreachable!(
243 "The response to a form control should always match it's originating type."
244 ),
245 }
246 }
247
248 pub(crate) fn show_context_menu(&self, cx: &mut JSContext, hit_test_result: &HitTestResult) {
249 {
250 let mut visible_elements = self.visible_elements.borrow_mut();
251 visible_elements.retain(|index, control_element| {
252 if matches!(control_element, ControlElement::ContextMenu(..)) {
253 let id = EmbedderControlId {
254 webview_id: self.window.webview_id(),
255 pipeline_id: self.window.pipeline_id(),
256 index: index.0,
257 };
258 self.window
259 .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
260 false
261 } else {
262 true
263 }
264 });
265 }
266
267 let mut anchor_element = None;
268 let mut image_element = None;
269 for node in hit_test_result
270 .node
271 .inclusive_ancestors(ShadowIncluding::Yes)
272 {
273 if anchor_element.is_none() &&
274 let Some(candidate_anchor_element) = node.downcast::<HTMLAnchorElement>() &&
275 candidate_anchor_element.is_instance_activatable()
276 {
277 anchor_element = Some(DomRoot::from_ref(candidate_anchor_element));
278 }
279
280 if image_element.is_none() &&
281 let Some(candidate_image_element) = node.downcast::<HTMLImageElement>()
282 {
283 image_element = Some(DomRoot::from_ref(candidate_image_element))
284 }
285 }
286
287 let mut info = ContextMenuElementInformation::default();
288 let mut items = Vec::new();
289 if let Some(anchor_element) = anchor_element.as_ref() {
290 info.flags.insert(ContextMenuElementInformationFlags::Link);
291 info.link_url = anchor_element
292 .full_href_url_for_user_interface()
293 .map(ServoUrl::into_url);
294
295 items.extend(vec![
296 ContextMenuItem::Item {
297 label: "Open Link in New View".into(),
298 action: ContextMenuAction::OpenLinkInNewWebView,
299 enabled: true,
300 },
301 ContextMenuItem::Item {
302 label: "Copy Link".into(),
303 action: ContextMenuAction::CopyLink,
304 enabled: true,
305 },
306 ContextMenuItem::Separator,
307 ]);
308 }
309
310 if let Some(image_element) = image_element.as_ref() {
311 info.flags.insert(ContextMenuElementInformationFlags::Image);
312 info.image_url = image_element
313 .full_image_url_for_user_interface()
314 .map(ServoUrl::into_url);
315
316 items.extend(vec![
317 ContextMenuItem::Item {
318 label: "Open Image in New View".into(),
319 action: ContextMenuAction::OpenImageInNewView,
320 enabled: true,
321 },
322 ContextMenuItem::Item {
323 label: "Copy Image Link".into(),
324 action: ContextMenuAction::CopyImageLink,
325 enabled: true,
326 },
327 ContextMenuItem::Separator,
328 ]);
329 }
330
331 let document = self.window.Document();
332 let editing_context = document.editing_context(cx.no_gc(), &hit_test_result.node);
333
334 let has_selection = editing_context.has_uncollapsed_selection();
335 if has_selection {
336 info.flags
337 .insert(ContextMenuElementInformationFlags::Selection);
338 }
339
340 let can_cut = has_selection && editing_context.cutting_enabled();
341 let can_copy = has_selection && editing_context.copying_enabled();
342 let can_paste = editing_context.pasting_enabled();
343 if can_paste {
344 info.flags
345 .insert(ContextMenuElementInformationFlags::EditableText);
346 }
347 items.extend(vec![
348 ContextMenuItem::Item {
349 label: "Cut".into(),
350 action: ContextMenuAction::Cut,
351 enabled: can_cut,
352 },
353 ContextMenuItem::Item {
354 label: "Copy".into(),
355 action: ContextMenuAction::Copy,
356 enabled: can_copy,
357 },
358 ContextMenuItem::Item {
359 label: "Paste".into(),
360 action: ContextMenuAction::Paste,
361 enabled: can_paste,
362 },
363 ContextMenuItem::Item {
364 label: "Select All".into(),
365 action: ContextMenuAction::SelectAll,
366 enabled: editing_context.has_selectable_text(),
367 },
368 ContextMenuItem::Separator,
369 ]);
370
371 items.extend(vec![
372 ContextMenuItem::Item {
373 label: "Back".into(),
374 action: ContextMenuAction::GoBack,
375 enabled: true,
376 },
377 ContextMenuItem::Item {
378 label: "Forward".into(),
379 action: ContextMenuAction::GoForward,
380 enabled: true,
381 },
382 ContextMenuItem::Item {
383 label: "Reload".into(),
384 action: ContextMenuAction::Reload,
385 enabled: true,
386 },
387 ]);
388
389 self.show_embedder_control(
390 cx,
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 node: Dom<Node>,
410 anchor_element: Option<Dom<HTMLAnchorElement>>,
412 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 match action {
432 ContextMenuAction::GoBack => {
433 let _ = window.History(cx).Back();
434 },
435 ContextMenuAction::GoForward => {
436 let _ = window.History(cx).Forward();
437 },
438 ContextMenuAction::Reload => {
439 window.Location(cx).reload_without_origin_check(cx, &window);
440 },
441 ContextMenuAction::CopyLink => {
442 let Some(anchor_element) = &self.anchor_element else {
443 return;
444 };
445
446 let url_string = anchor_element
447 .full_href_url_for_user_interface()
448 .as_ref()
449 .map(ServoUrl::to_string)
450 .unwrap_or_else(|| String::from(anchor_element.Href(cx.no_gc())));
451 set_clipboard_text(url_string);
452 },
453 ContextMenuAction::OpenLinkInNewWebView => {
454 let Some(anchor_element) = &self.anchor_element else {
455 return;
456 };
457 if let Some(url) = anchor_element.full_href_url_for_user_interface() {
458 document.event_handler().open_url_in_new_webview(cx, url);
459 };
460 },
461 ContextMenuAction::CopyImageLink => {
462 let Some(image_element) = &self.image_element else {
463 return;
464 };
465 let url_string = image_element
466 .full_image_url_for_user_interface()
467 .as_ref()
468 .map(ServoUrl::to_string)
469 .unwrap_or_else(|| String::from(image_element.CurrentSrc()));
470 set_clipboard_text(url_string);
471 },
472 ContextMenuAction::OpenImageInNewView => {
473 let Some(image_element) = &self.image_element else {
474 return;
475 };
476 if let Some(url) = image_element.full_image_url_for_user_interface() {
477 document.event_handler().open_url_in_new_webview(cx, url);
478 }
479 },
480 ContextMenuAction::Cut => {
481 let editing_context = document.editing_context(cx.no_gc(), &self.node);
482 document.handle_clipboard_action(cx, &editing_context, ClipboardAction::Cut);
483 },
484 ContextMenuAction::Copy => {
485 let editing_context = document.editing_context(cx.no_gc(), &self.node);
486 document.handle_clipboard_action(cx, &editing_context, ClipboardAction::Copy);
487 },
488 ContextMenuAction::Paste => {
489 let editing_context = document.editing_context(cx.no_gc(), &self.node);
490 document.handle_clipboard_action(cx, &editing_context, ClipboardAction::Paste);
491 },
492 ContextMenuAction::SelectAll => {
493 let editing_context = document.editing_context(cx.no_gc(), &self.node);
494 editing_context.select_all(cx);
495 },
496 }
497 }
498}