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