1use 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 window: Dom<Window>,
73 #[no_trace]
76 user_interface_element_index: Cell<Epoch>,
77 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 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 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 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 node: Dom<Node>,
414 anchor_element: Option<Dom<HTMLAnchorElement>>,
416 image_element: Option<Dom<HTMLImageElement>>,
418 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, );
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}