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::codegen::GenericBindings::HTMLAnchorElementBinding::HTMLAnchorElementMethods;
18use script_bindings::codegen::GenericBindings::HTMLImageElementBinding::HTMLImageElementMethods;
19use script_bindings::codegen::GenericBindings::HistoryBinding::HistoryMethods;
20use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
21use script_bindings::inheritance::Castable;
22use script_bindings::root::{Dom, DomRoot};
23use script_bindings::script_runtime::CanGc;
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::cell::DomRefCell;
32use crate::dom::bindings::refcounted::Trusted;
33use crate::dom::bindings::trace::NoTrace;
34use crate::dom::inputevent::HitTestResult;
35use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
36use crate::dom::textcontrol::TextControlElement;
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)]
45pub(crate) enum ControlElement {
46 Select(DomRoot<HTMLSelectElement>),
47 ColorInput(DomRoot<HTMLInputElement>),
48 FileInput(DomRoot<HTMLInputElement>),
49 Ime(DomRoot<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
65#[derive(JSTraceable, MallocSizeOf)]
66#[cfg_attr(crown, expect(crown::unrooted_must_root))]
67pub(crate) struct DocumentEmbedderControls {
68 window: Dom<Window>,
70 #[no_trace]
73 user_interface_element_index: Cell<Epoch>,
74 visible_elements: DomRefCell<FxHashMap<NoTrace<Epoch>, ControlElement>>,
76}
77
78impl DocumentEmbedderControls {
79 pub fn new(window: &Window) -> Self {
80 Self {
81 window: Dom::from_ref(window),
82 user_interface_element_index: Default::default(),
83 visible_elements: Default::default(),
84 }
85 }
86
87 pub(crate) fn next_control_id(&self) -> EmbedderControlId {
90 let index = self.user_interface_element_index.get();
91 self.user_interface_element_index.set(index.next());
92 EmbedderControlId {
93 webview_id: self.window.webview_id(),
94 pipeline_id: self.window.pipeline_id(),
95 index,
96 }
97 }
98
99 pub(crate) fn show_embedder_control(
100 &self,
101 element: ControlElement,
102 request: EmbedderControlRequest,
103 point: Option<DevicePoint>,
104 ) -> EmbedderControlId {
105 let id = self.next_control_id();
106 let rect = point
107 .map(|point| DeviceIntRect::from_origin_and_size(point.to_i32(), Size2D::zero()))
108 .unwrap_or_else(|| {
109 let rect = element
110 .node()
111 .upcast::<Node>()
112 .border_box()
113 .unwrap_or_default();
114
115 let rect = Rect::new(
116 Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()),
117 Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()),
118 );
119
120 DeviceIntRect::from_untyped(&rect.to_box2d())
123 });
124
125 self.visible_elements
126 .borrow_mut()
127 .insert(id.index.into(), element);
128
129 match request {
130 EmbedderControlRequest::SelectElement(..) |
131 EmbedderControlRequest::ColorPicker(..) |
132 EmbedderControlRequest::InputMethod(..) |
133 EmbedderControlRequest::ContextMenu(..) => self
134 .window
135 .send_to_embedder(EmbedderMsg::ShowEmbedderControl(id, rect, request)),
136 EmbedderControlRequest::FilePicker(file_picker_request) => {
137 let main_thread_sender = self.window.main_thread_script_chan().clone();
138 let callback = profile_traits::generic_callback::GenericCallback::new(
139 self.window.as_global_scope().time_profiler_chan().clone(),
140 move |result| {
141 let Ok(embedder_control_response) = result else {
142 return;
143 };
144 if let Err(error) = main_thread_sender.send(
145 MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager(
146 id,
147 embedder_control_response,
148 ),
149 ) {
150 warn!("Could not send FileManager response to main thread: {error}")
151 }
152 },
153 )
154 .expect("Could not create callback");
155 self.window
156 .as_global_scope()
157 .resource_threads()
158 .sender()
159 .send(CoreResourceMsg::ToFileManager(
160 FileManagerThreadMsg::SelectFiles(id, file_picker_request, callback),
161 ))
162 .unwrap();
163 },
164 }
165
166 id
167 }
168
169 pub(crate) fn hide_embedder_control(&self, element: &Element) {
170 self.visible_elements
171 .borrow_mut()
172 .retain(|index, control_element| {
173 if control_element.node() != element.upcast() {
174 return true;
175 }
176 let id = EmbedderControlId {
177 webview_id: self.window.webview_id(),
178 pipeline_id: self.window.pipeline_id(),
179 index: index.0,
180 };
181 self.window
182 .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
183 false
184 });
185 }
186
187 pub(crate) fn handle_embedder_control_response(
188 &self,
189 cx: &mut JSContext,
190 id: EmbedderControlId,
191 response: EmbedderControlResponse,
192 ) {
193 assert_eq!(self.window.pipeline_id(), id.pipeline_id);
194 assert_eq!(self.window.webview_id(), id.webview_id);
195
196 let Some(element) = self.visible_elements.borrow_mut().remove(&id.index.into()) else {
197 return;
198 };
199
200 if !element.node().owner_doc().is_active() {
202 return;
203 }
204
205 match (element, response) {
206 (
207 ControlElement::Select(select_element),
208 EmbedderControlResponse::SelectElement(response),
209 ) => {
210 select_element.handle_menu_response(cx, response);
211 },
212 (
213 ControlElement::ColorInput(input_element),
214 EmbedderControlResponse::ColorPicker(response),
215 ) => {
216 input_element.handle_color_picker_response(response, CanGc::from_cx(cx));
217 },
218 (
219 ControlElement::FileInput(input_element),
220 EmbedderControlResponse::FilePicker(response),
221 ) => {
222 input_element.handle_file_picker_response(response, CanGc::from_cx(cx));
223 },
224 (
225 ControlElement::ContextMenu(context_menu_nodes),
226 EmbedderControlResponse::ContextMenu(action),
227 ) => {
228 context_menu_nodes.handle_context_menu_action(action, cx);
229 },
230 (_, _) => unreachable!(
231 "The response to a form control should always match it's originating type."
232 ),
233 }
234 }
235
236 pub(crate) fn show_context_menu(&self, hit_test_result: &HitTestResult) {
237 {
238 let mut visible_elements = self.visible_elements.borrow_mut();
239 visible_elements.retain(|index, control_element| {
240 if matches!(control_element, ControlElement::ContextMenu(..)) {
241 let id = EmbedderControlId {
242 webview_id: self.window.webview_id(),
243 pipeline_id: self.window.pipeline_id(),
244 index: index.0,
245 };
246 self.window
247 .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
248 false
249 } else {
250 true
251 }
252 });
253 }
254
255 let mut anchor_element = None;
256 let mut image_element = None;
257 let mut text_input_element = None;
258 for node in hit_test_result
259 .node
260 .inclusive_ancestors(ShadowIncluding::Yes)
261 {
262 if anchor_element.is_none() {
263 if let Some(candidate_anchor_element) = node.downcast::<HTMLAnchorElement>() {
264 if candidate_anchor_element.is_instance_activatable() {
265 anchor_element = Some(DomRoot::from_ref(candidate_anchor_element));
266 }
267 }
268 }
269
270 if image_element.is_none() {
271 if let Some(candidate_image_element) = node.downcast::<HTMLImageElement>() {
272 image_element = Some(DomRoot::from_ref(candidate_image_element))
273 }
274 }
275
276 if text_input_element.is_none() {
277 if let Some(candidate_text_input_element) = node.as_text_input() {
278 text_input_element = Some(candidate_text_input_element);
279 }
280 }
281 }
282
283 let mut info = ContextMenuElementInformation::default();
284 let mut items = Vec::new();
285 if let Some(anchor_element) = anchor_element.as_ref() {
286 info.flags.insert(ContextMenuElementInformationFlags::Link);
287 info.link_url = anchor_element
288 .full_href_url_for_user_interface()
289 .map(ServoUrl::into_url);
290
291 items.extend(vec![
292 ContextMenuItem::Item {
293 label: "Open Link in New View".into(),
294 action: ContextMenuAction::OpenLinkInNewWebView,
295 enabled: true,
296 },
297 ContextMenuItem::Item {
298 label: "Copy Link".into(),
299 action: ContextMenuAction::CopyLink,
300 enabled: true,
301 },
302 ContextMenuItem::Separator,
303 ]);
304 }
305
306 if let Some(image_element) = image_element.as_ref() {
307 info.flags.insert(ContextMenuElementInformationFlags::Image);
308 info.image_url = image_element
309 .full_image_url_for_user_interface()
310 .map(ServoUrl::into_url);
311
312 items.extend(vec![
313 ContextMenuItem::Item {
314 label: "Open Image in New View".into(),
315 action: ContextMenuAction::OpenImageInNewView,
316 enabled: true,
317 },
318 ContextMenuItem::Item {
319 label: "Copy Image Link".into(),
320 action: ContextMenuAction::CopyImageLink,
321 enabled: true,
322 },
323 ContextMenuItem::Separator,
324 ]);
325 }
326
327 if let Some(text_input_element) = &text_input_element {
328 let has_selection = text_input_element.has_uncollapsed_selection();
329
330 info.flags
331 .insert(ContextMenuElementInformationFlags::EditableText);
332 if has_selection {
333 info.flags
334 .insert(ContextMenuElementInformationFlags::Selection);
335 }
336
337 items.extend(vec![
338 ContextMenuItem::Item {
339 label: "Cut".into(),
340 action: ContextMenuAction::Cut,
341 enabled: has_selection,
342 },
343 ContextMenuItem::Item {
344 label: "Copy".into(),
345 action: ContextMenuAction::Copy,
346 enabled: has_selection,
347 },
348 ContextMenuItem::Item {
349 label: "Paste".into(),
350 action: ContextMenuAction::Paste,
351 enabled: true,
352 },
353 ContextMenuItem::Item {
354 label: "Select All".into(),
355 action: ContextMenuAction::SelectAll,
356 enabled: text_input_element.has_selectable_text(),
357 },
358 ContextMenuItem::Separator,
359 ]);
360 }
361
362 items.extend(vec![
363 ContextMenuItem::Item {
364 label: "Back".into(),
365 action: ContextMenuAction::GoBack,
366 enabled: true,
367 },
368 ContextMenuItem::Item {
369 label: "Forward".into(),
370 action: ContextMenuAction::GoForward,
371 enabled: true,
372 },
373 ContextMenuItem::Item {
374 label: "Reload".into(),
375 action: ContextMenuAction::Reload,
376 enabled: true,
377 },
378 ]);
379
380 let context_menu_nodes = ContextMenuNodes {
381 node: hit_test_result.node.clone(),
382 anchor_element,
383 image_element,
384 text_input_element,
385 };
386
387 self.show_embedder_control(
388 ControlElement::ContextMenu(context_menu_nodes),
389 EmbedderControlRequest::ContextMenu(ContextMenuRequest {
390 element_info: info,
391 items,
392 }),
393 Some(hit_test_result.point_in_frame.cast_unit()),
394 );
395 }
396}
397
398#[derive(JSTraceable, MallocSizeOf)]
399pub(crate) struct ContextMenuNodes {
400 node: DomRoot<Node>,
402 anchor_element: Option<DomRoot<HTMLAnchorElement>>,
404 image_element: Option<DomRoot<HTMLImageElement>>,
406 text_input_element: Option<DomRoot<Element>>,
408}
409
410impl ContextMenuNodes {
411 fn handle_context_menu_action(&self, action: Option<ContextMenuAction>, cx: &mut JSContext) {
412 let Some(action) = action else {
413 return;
414 };
415
416 let window = self.node.owner_window();
417 let document = window.Document();
418 let set_clipboard_text = |string: String| {
419 if string.is_empty() {
420 return;
421 }
422 window.send_to_embedder(EmbedderMsg::SetClipboardText(window.webview_id(), string));
423 };
424
425 let open_url_in_new_webview = |url: ServoUrl| {
426 let Some(browsing_context) = document.browsing_context() else {
427 return;
428 };
429 let (browsing_context, new) = browsing_context
430 .choose_browsing_context("_blank".into(), true );
431 let Some(browsing_context) = browsing_context else {
432 return;
433 };
434 assert!(new);
435 let Some(target_document) = browsing_context.document() else {
436 return;
437 };
438
439 let target_window = target_document.window();
440 let target = Trusted::new(target_window);
441 let load_data = LoadData::new_for_new_unrelated_webview(url);
442 let task = task!(open_link_in_new_webview: move |cx| {
443 navigate(cx, &target.root(), NavigationHistoryBehavior::Replace, false, load_data);
444 });
445 target_document
446 .owner_global()
447 .task_manager()
448 .dom_manipulation_task_source()
449 .queue(task);
450 };
451
452 match action {
453 ContextMenuAction::GoBack => {
454 let _ = window.History().Back();
455 },
456 ContextMenuAction::GoForward => {
457 let _ = window.History().Forward();
458 },
459 ContextMenuAction::Reload => {
460 window.Location(cx).reload_without_origin_check(cx);
461 },
462 ContextMenuAction::CopyLink => {
463 let Some(anchor_element) = &self.anchor_element else {
464 return;
465 };
466
467 let url_string = anchor_element
468 .full_href_url_for_user_interface()
469 .as_ref()
470 .map(ServoUrl::to_string)
471 .unwrap_or_else(|| anchor_element.Href().to_string());
472 set_clipboard_text(url_string);
473 },
474 ContextMenuAction::OpenLinkInNewWebView => {
475 let Some(anchor_element) = &self.anchor_element else {
476 return;
477 };
478 if let Some(url) = anchor_element.full_href_url_for_user_interface() {
479 open_url_in_new_webview(url);
480 };
481 },
482 ContextMenuAction::CopyImageLink => {
483 let Some(image_element) = &self.image_element else {
484 return;
485 };
486 let url_string = image_element
487 .full_image_url_for_user_interface()
488 .as_ref()
489 .map(ServoUrl::to_string)
490 .unwrap_or_else(|| image_element.CurrentSrc().to_string());
491 set_clipboard_text(url_string);
492 },
493 ContextMenuAction::OpenImageInNewView => {
494 let Some(image_element) = &self.image_element else {
495 return;
496 };
497 if let Some(url) = image_element.full_image_url_for_user_interface() {
498 open_url_in_new_webview(url);
499 }
500 },
501 ContextMenuAction::Cut => {
502 window.Document().event_handler().handle_editing_action(
503 self.text_input_element.clone(),
504 EditingActionEvent::Cut,
505 CanGc::from_cx(cx),
506 );
507 },
508 ContextMenuAction::Copy => {
509 window.Document().event_handler().handle_editing_action(
510 self.text_input_element.clone(),
511 EditingActionEvent::Copy,
512 CanGc::from_cx(cx),
513 );
514 },
515 ContextMenuAction::Paste => {
516 window.Document().event_handler().handle_editing_action(
517 self.text_input_element.clone(),
518 EditingActionEvent::Paste,
519 CanGc::from_cx(cx),
520 );
521 },
522 ContextMenuAction::SelectAll => {
523 if let Some(text_input_element) = &self.text_input_element {
524 text_input_element.select_all();
525 }
526 },
527 }
528 }
529}
530
531impl Node {
532 fn as_text_input(&self) -> Option<DomRoot<Element>> {
533 if let Some(input_element) = self
534 .downcast::<HTMLInputElement>()
535 .filter(|input_element| input_element.renders_as_text_input_widget())
536 {
537 return Some(DomRoot::from_ref(input_element.upcast::<Element>()));
538 }
539 self.downcast::<HTMLTextAreaElement>()
540 .map(Castable::upcast)
541 .map(DomRoot::from_ref)
542 }
543}
544
545impl Element {
546 fn has_uncollapsed_selection(&self) -> bool {
547 self.downcast::<HTMLTextAreaElement>()
548 .map(TextControlElement::has_uncollapsed_selection)
549 .or(self
550 .downcast::<HTMLInputElement>()
551 .map(TextControlElement::has_uncollapsed_selection))
552 .unwrap_or_default()
553 }
554
555 fn has_selectable_text(&self) -> bool {
556 self.downcast::<HTMLTextAreaElement>()
557 .map(TextControlElement::has_selectable_text)
558 .or(self
559 .downcast::<HTMLInputElement>()
560 .map(TextControlElement::has_selectable_text))
561 .unwrap_or_default()
562 }
563
564 fn select_all(&self) {
565 self.downcast::<HTMLTextAreaElement>()
566 .map(TextControlElement::select_all)
567 .or(self
568 .downcast::<HTMLInputElement>()
569 .map(TextControlElement::select_all))
570 .unwrap_or_default()
571 }
572}