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, 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 window: Dom<Window>,
75 #[no_trace]
78 user_interface_element_index: Cell<Epoch>,
79 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 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 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 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 can_cut = has_selection && editing_context.cutting_enabled();
343 let can_copy = has_selection && editing_context.copying_enabled();
344 let can_paste = editing_context.pasting_enabled();
345 if can_paste {
346 info.flags
347 .insert(ContextMenuElementInformationFlags::EditableText);
348 }
349 items.extend(vec![
350 ContextMenuItem::Item {
351 label: "Cut".into(),
352 action: ContextMenuAction::Cut,
353 enabled: can_cut,
354 },
355 ContextMenuItem::Item {
356 label: "Copy".into(),
357 action: ContextMenuAction::Copy,
358 enabled: can_copy,
359 },
360 ContextMenuItem::Item {
361 label: "Paste".into(),
362 action: ContextMenuAction::Paste,
363 enabled: can_paste,
364 },
365 ContextMenuItem::Item {
366 label: "Select All".into(),
367 action: ContextMenuAction::SelectAll,
368 enabled: editing_context.has_selectable_text(),
369 },
370 ContextMenuItem::Separator,
371 ]);
372
373 items.extend(vec![
374 ContextMenuItem::Item {
375 label: "Back".into(),
376 action: ContextMenuAction::GoBack,
377 enabled: true,
378 },
379 ContextMenuItem::Item {
380 label: "Forward".into(),
381 action: ContextMenuAction::GoForward,
382 enabled: true,
383 },
384 ContextMenuItem::Item {
385 label: "Reload".into(),
386 action: ContextMenuAction::Reload,
387 enabled: true,
388 },
389 ]);
390
391 self.show_embedder_control(
392 ControlElement::ContextMenu(ContextMenuNodes {
393 node: hit_test_result.node.as_traced(),
394 anchor_element: anchor_element.map(|element| element.as_traced()),
395 image_element: image_element.map(|element| element.as_traced()),
396 }),
397 EmbedderControlRequest::ContextMenu(ContextMenuRequest {
398 element_info: info,
399 items,
400 }),
401 Some(hit_test_result.point_in_frame.cast_unit()),
402 );
403 }
404}
405
406#[derive(JSTraceable, MallocSizeOf)]
407#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
408pub(crate) struct ContextMenuNodes {
409 node: Dom<Node>,
411 anchor_element: Option<Dom<HTMLAnchorElement>>,
413 image_element: Option<Dom<HTMLImageElement>>,
415}
416
417impl ContextMenuNodes {
418 fn handle_context_menu_action(&self, action: Option<ContextMenuAction>, cx: &mut JSContext) {
419 let Some(action) = action else {
420 return;
421 };
422
423 let window = self.node.owner_window();
424 let document = window.Document();
425 let set_clipboard_text = |string: String| {
426 if string.is_empty() {
427 return;
428 }
429 window.send_to_embedder(EmbedderMsg::SetClipboardText(window.webview_id(), string));
430 };
431
432 let open_url_in_new_webview = |cx: &mut JSContext, url: ServoUrl| {
433 let Some(browsing_context) = document.browsing_context() else {
434 return;
435 };
436 let (browsing_context, new) = browsing_context.choose_a_navigable(
437 cx,
438 DOMString::from_static("_blank"),
439 true, );
441 let Some(browsing_context) = browsing_context else {
442 return;
443 };
444 assert!(new);
445 let Some(target_document) = browsing_context.document() else {
446 return;
447 };
448
449 let target_window = target_document.window();
450 let target = Trusted::new(target_window);
451 let load_data = LoadData::new_for_new_unrelated_webview(url);
452 let task = task!(open_link_in_new_webview: move |cx| {
453 navigate(cx, &target.root(), NavigationHistoryBehavior::Replace, false, load_data);
454 });
455 target_document
456 .owner_global()
457 .task_manager()
458 .dom_manipulation_task_source()
459 .queue(task);
460 };
461
462 match action {
463 ContextMenuAction::GoBack => {
464 let _ = window.History(cx).Back();
465 },
466 ContextMenuAction::GoForward => {
467 let _ = window.History(cx).Forward();
468 },
469 ContextMenuAction::Reload => {
470 window.Location(cx).reload_without_origin_check(cx, &window);
471 },
472 ContextMenuAction::CopyLink => {
473 let Some(anchor_element) = &self.anchor_element else {
474 return;
475 };
476
477 let url_string = anchor_element
478 .full_href_url_for_user_interface(cx.no_gc())
479 .as_ref()
480 .map(ServoUrl::to_string)
481 .unwrap_or_else(|| String::from(anchor_element.Href(cx.no_gc())));
482 set_clipboard_text(url_string);
483 },
484 ContextMenuAction::OpenLinkInNewWebView => {
485 let Some(anchor_element) = &self.anchor_element else {
486 return;
487 };
488 if let Some(url) = anchor_element.full_href_url_for_user_interface(cx.no_gc()) {
489 open_url_in_new_webview(cx, url);
490 };
491 },
492 ContextMenuAction::CopyImageLink => {
493 let Some(image_element) = &self.image_element else {
494 return;
495 };
496 let url_string = image_element
497 .full_image_url_for_user_interface()
498 .as_ref()
499 .map(ServoUrl::to_string)
500 .unwrap_or_else(|| String::from(image_element.CurrentSrc()));
501 set_clipboard_text(url_string);
502 },
503 ContextMenuAction::OpenImageInNewView => {
504 let Some(image_element) = &self.image_element else {
505 return;
506 };
507 if let Some(url) = image_element.full_image_url_for_user_interface() {
508 open_url_in_new_webview(cx, url);
509 }
510 },
511 ContextMenuAction::Cut => {
512 let editing_context = document.editing_context(cx.no_gc(), &self.node);
513 document.handle_clipboard_action(cx, &editing_context, ClipboardAction::Cut);
514 },
515 ContextMenuAction::Copy => {
516 let editing_context = document.editing_context(cx.no_gc(), &self.node);
517 document.handle_clipboard_action(cx, &editing_context, ClipboardAction::Copy);
518 },
519 ContextMenuAction::Paste => {
520 let editing_context = document.editing_context(cx.no_gc(), &self.node);
521 document.handle_clipboard_action(cx, &editing_context, ClipboardAction::Paste);
522 },
523 ContextMenuAction::SelectAll => {
524 let editing_context = document.editing_context(cx.no_gc(), &self.node);
525 editing_context.select_all(cx);
526 },
527 }
528 }
529}