1use std::cell::Cell;
6
7use base::Epoch;
8use base::generic_channel::GenericSend;
9use constellation_traits::{LoadData, NavigationHistoryBehavior};
10use embedder_traits::{
11 ContextMenuAction, ContextMenuElementInformation, ContextMenuElementInformationFlags,
12 ContextMenuItem, ContextMenuRequest, EditingActionEvent, EmbedderControlId,
13 EmbedderControlRequest, EmbedderControlResponse, EmbedderMsg,
14};
15use euclid::{Point2D, Rect, Size2D};
16use ipc_channel::router::ROUTER;
17use net_traits::CoreResourceMsg;
18use net_traits::filemanager_thread::FileManagerThreadMsg;
19use rustc_hash::FxHashMap;
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::script_runtime::CanGc;
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;
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 (sender, receiver) = profile_traits::ipc::channel(
137 self.window.as_global_scope().time_profiler_chan().clone(),
138 )
139 .expect("Error initializing channel");
140 let main_thread_sender = self.window.main_thread_script_chan().clone();
141 ROUTER.add_typed_route(
142 receiver.to_ipc_receiver(),
143 Box::new(move |result| {
144 let Ok(embedder_control_response) = result else {
145 return;
146 };
147 if let Err(error) = main_thread_sender.send(
148 MainThreadScriptMsg::ForwardEmbedderControlResponseFromFileManager(
149 id,
150 embedder_control_response,
151 ),
152 ) {
153 warn!("Could not send FileManager response to main thread: {error}")
154 }
155 }),
156 );
157 self.window
158 .as_global_scope()
159 .resource_threads()
160 .sender()
161 .send(CoreResourceMsg::ToFileManager(
162 FileManagerThreadMsg::SelectFiles(id, file_picker_request, sender),
163 ))
164 .unwrap();
165 },
166 }
167
168 id
169 }
170
171 pub(crate) fn hide_embedder_control(&self, element: &Element) {
172 self.visible_elements
173 .borrow_mut()
174 .retain(|index, control_element| {
175 if control_element.node() != element.upcast() {
176 return true;
177 }
178 let id = EmbedderControlId {
179 webview_id: self.window.webview_id(),
180 pipeline_id: self.window.pipeline_id(),
181 index: index.0,
182 };
183 self.window
184 .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
185 false
186 });
187 }
188
189 pub(crate) fn handle_embedder_control_response(
190 &self,
191 id: EmbedderControlId,
192 response: EmbedderControlResponse,
193 can_gc: CanGc,
194 ) {
195 assert_eq!(self.window.pipeline_id(), id.pipeline_id);
196 assert_eq!(self.window.webview_id(), id.webview_id);
197
198 let Some(element) = self.visible_elements.borrow_mut().remove(&id.index.into()) else {
199 return;
200 };
201
202 if !element.node().owner_doc().is_active() {
204 return;
205 }
206
207 match (element, response) {
208 (
209 ControlElement::Select(select_element),
210 EmbedderControlResponse::SelectElement(response),
211 ) => {
212 select_element.handle_menu_response(response, can_gc);
213 },
214 (
215 ControlElement::ColorInput(input_element),
216 EmbedderControlResponse::ColorPicker(response),
217 ) => {
218 input_element.handle_color_picker_response(response, can_gc);
219 },
220 (
221 ControlElement::FileInput(input_element),
222 EmbedderControlResponse::FilePicker(response),
223 ) => {
224 input_element.handle_file_picker_response(response, can_gc);
225 },
226 (
227 ControlElement::ContextMenu(context_menu_nodes),
228 EmbedderControlResponse::ContextMenu(action),
229 ) => {
230 context_menu_nodes.handle_context_menu_action(action, can_gc);
231 },
232 (_, _) => unreachable!(
233 "The response to a form control should always match it's originating type."
234 ),
235 }
236 }
237
238 pub(crate) fn show_context_menu(&self, hit_test_result: &HitTestResult) {
239 {
240 let mut visible_elements = self.visible_elements.borrow_mut();
241 visible_elements.retain(|index, control_element| {
242 if matches!(control_element, ControlElement::ContextMenu(..)) {
243 let id = EmbedderControlId {
244 webview_id: self.window.webview_id(),
245 pipeline_id: self.window.pipeline_id(),
246 index: index.0,
247 };
248 self.window
249 .send_to_embedder(EmbedderMsg::HideEmbedderControl(id));
250 false
251 } else {
252 true
253 }
254 });
255 }
256
257 let mut anchor_element = None;
258 let mut image_element = None;
259 let mut text_input_element = None;
260 for node in hit_test_result
261 .node
262 .inclusive_ancestors(ShadowIncluding::Yes)
263 {
264 if anchor_element.is_none() {
265 if let Some(candidate_anchor_element) = node.downcast::<HTMLAnchorElement>() {
266 if candidate_anchor_element.is_instance_activatable() {
267 anchor_element = Some(DomRoot::from_ref(candidate_anchor_element));
268 }
269 }
270 }
271
272 if image_element.is_none() {
273 if let Some(candidate_image_element) = node.downcast::<HTMLImageElement>() {
274 image_element = Some(DomRoot::from_ref(candidate_image_element))
275 }
276 }
277
278 if text_input_element.is_none() {
279 if let Some(candidate_text_input_element) = node.as_text_input() {
280 text_input_element = Some(candidate_text_input_element);
281 }
282 }
283 }
284
285 let mut info = ContextMenuElementInformation::default();
286 let mut items = Vec::new();
287 if let Some(anchor_element) = anchor_element.as_ref() {
288 info.flags.insert(ContextMenuElementInformationFlags::Link);
289 info.link_url = anchor_element
290 .full_href_url_for_user_interface()
291 .map(ServoUrl::into_url);
292
293 items.extend(vec![
294 ContextMenuItem::Item {
295 label: "Open Link in New View".into(),
296 action: ContextMenuAction::OpenLinkInNewWebView,
297 enabled: true,
298 },
299 ContextMenuItem::Item {
300 label: "Copy Link".into(),
301 action: ContextMenuAction::CopyLink,
302 enabled: true,
303 },
304 ContextMenuItem::Separator,
305 ]);
306 }
307
308 if let Some(image_element) = image_element.as_ref() {
309 info.flags.insert(ContextMenuElementInformationFlags::Image);
310 info.image_url = image_element
311 .full_image_url_for_user_interface()
312 .map(ServoUrl::into_url);
313
314 items.extend(vec![
315 ContextMenuItem::Item {
316 label: "Open Image in New View".into(),
317 action: ContextMenuAction::OpenImageInNewView,
318 enabled: true,
319 },
320 ContextMenuItem::Item {
321 label: "Copy Image Link".into(),
322 action: ContextMenuAction::CopyImageLink,
323 enabled: true,
324 },
325 ContextMenuItem::Separator,
326 ]);
327 }
328
329 if let Some(text_input_element) = &text_input_element {
330 let has_selection = text_input_element.has_uncollapsed_selection();
331
332 info.flags
333 .insert(ContextMenuElementInformationFlags::EditableText);
334 if has_selection {
335 info.flags
336 .insert(ContextMenuElementInformationFlags::Selection);
337 }
338
339 items.extend(vec![
340 ContextMenuItem::Item {
341 label: "Cut".into(),
342 action: ContextMenuAction::Cut,
343 enabled: has_selection,
344 },
345 ContextMenuItem::Item {
346 label: "Copy".into(),
347 action: ContextMenuAction::Copy,
348 enabled: has_selection,
349 },
350 ContextMenuItem::Item {
351 label: "Paste".into(),
352 action: ContextMenuAction::Paste,
353 enabled: true,
354 },
355 ContextMenuItem::Item {
356 label: "Select All".into(),
357 action: ContextMenuAction::SelectAll,
358 enabled: text_input_element.has_selectable_text(),
359 },
360 ContextMenuItem::Separator,
361 ]);
362 }
363
364 items.extend(vec![
365 ContextMenuItem::Item {
366 label: "Back".into(),
367 action: ContextMenuAction::GoBack,
368 enabled: true,
369 },
370 ContextMenuItem::Item {
371 label: "Forward".into(),
372 action: ContextMenuAction::GoForward,
373 enabled: true,
374 },
375 ContextMenuItem::Item {
376 label: "Reload".into(),
377 action: ContextMenuAction::Reload,
378 enabled: true,
379 },
380 ]);
381
382 let context_menu_nodes = ContextMenuNodes {
383 node: hit_test_result.node.clone(),
384 anchor_element,
385 image_element,
386 text_input_element,
387 };
388
389 self.show_embedder_control(
390 ControlElement::ContextMenu(context_menu_nodes),
391 EmbedderControlRequest::ContextMenu(ContextMenuRequest {
392 element_info: info,
393 items,
394 }),
395 Some(hit_test_result.point_in_frame.cast_unit()),
396 );
397 }
398}
399
400#[derive(JSTraceable, MallocSizeOf)]
401pub(crate) struct ContextMenuNodes {
402 node: DomRoot<Node>,
404 anchor_element: Option<DomRoot<HTMLAnchorElement>>,
406 image_element: Option<DomRoot<HTMLImageElement>>,
408 text_input_element: Option<DomRoot<Element>>,
410}
411
412impl ContextMenuNodes {
413 fn handle_context_menu_action(&self, action: Option<ContextMenuAction>, can_gc: CanGc) {
414 let Some(action) = action else {
415 return;
416 };
417
418 let window = self.node.owner_window();
419 let document = window.Document();
420 let set_clipboard_text = |string: String| {
421 if string.is_empty() {
422 return;
423 }
424 window.send_to_embedder(EmbedderMsg::SetClipboardText(window.webview_id(), string));
425 };
426
427 let open_url_in_new_webview = |url: ServoUrl| {
428 let Some(browsing_context) = document.browsing_context() else {
429 return;
430 };
431 let (browsing_context, new) = browsing_context
432 .choose_browsing_context("_blank".into(), true );
433 let Some(browsing_context) = browsing_context else {
434 return;
435 };
436 assert!(new);
437 let Some(target_document) = browsing_context.document() else {
438 return;
439 };
440
441 let target_window = target_document.window();
442 let target = Trusted::new(target_window);
443 let load_data = LoadData::new_for_new_unrelated_webview(url);
444 let task = task!(open_link_in_new_webview: move || {
445 target.root().load_url(NavigationHistoryBehavior::Replace, false, load_data, CanGc::note());
446 });
447 target_document
448 .owner_global()
449 .task_manager()
450 .dom_manipulation_task_source()
451 .queue(task);
452 };
453
454 match action {
455 ContextMenuAction::GoBack => {
456 let _ = window.History().Back();
457 },
458 ContextMenuAction::GoForward => {
459 let _ = window.History().Forward();
460 },
461 ContextMenuAction::Reload => {
462 window.Location().reload_without_origin_check(can_gc);
463 },
464 ContextMenuAction::CopyLink => {
465 let Some(anchor_element) = &self.anchor_element else {
466 return;
467 };
468
469 let url_string = anchor_element
470 .full_href_url_for_user_interface()
471 .as_ref()
472 .map(ServoUrl::to_string)
473 .unwrap_or_else(|| anchor_element.Href().to_string());
474 set_clipboard_text(url_string);
475 },
476 ContextMenuAction::OpenLinkInNewWebView => {
477 let Some(anchor_element) = &self.anchor_element else {
478 return;
479 };
480 if let Some(url) = anchor_element.full_href_url_for_user_interface() {
481 open_url_in_new_webview(url);
482 };
483 },
484 ContextMenuAction::CopyImageLink => {
485 let Some(image_element) = &self.image_element else {
486 return;
487 };
488 let url_string = image_element
489 .full_image_url_for_user_interface()
490 .as_ref()
491 .map(ServoUrl::to_string)
492 .unwrap_or_else(|| image_element.CurrentSrc().to_string());
493 set_clipboard_text(url_string.to_string());
494 },
495 ContextMenuAction::OpenImageInNewView => {
496 let Some(image_element) = &self.image_element else {
497 return;
498 };
499 if let Some(url) = image_element.full_image_url_for_user_interface() {
500 open_url_in_new_webview(url);
501 }
502 },
503 ContextMenuAction::Cut => {
504 window.Document().event_handler().handle_editing_action(
505 self.text_input_element.clone(),
506 EditingActionEvent::Cut,
507 can_gc,
508 );
509 },
510 ContextMenuAction::Copy => {
511 window.Document().event_handler().handle_editing_action(
512 self.text_input_element.clone(),
513 EditingActionEvent::Copy,
514 can_gc,
515 );
516 },
517 ContextMenuAction::Paste => {
518 window.Document().event_handler().handle_editing_action(
519 self.text_input_element.clone(),
520 EditingActionEvent::Paste,
521 can_gc,
522 );
523 },
524 ContextMenuAction::SelectAll => {
525 if let Some(text_input_element) = &self.text_input_element {
526 text_input_element.select_all();
527 }
528 },
529 }
530 }
531}
532
533impl Node {
534 fn as_text_input(&self) -> Option<DomRoot<Element>> {
535 if let Some(input_element) = self
536 .downcast::<HTMLInputElement>()
537 .filter(|input_element| input_element.renders_as_text_input_widget())
538 {
539 return Some(DomRoot::from_ref(input_element.upcast::<Element>()));
540 }
541 self.downcast::<HTMLTextAreaElement>()
542 .map(Castable::upcast)
543 .map(DomRoot::from_ref)
544 }
545}
546
547impl Element {
548 fn has_uncollapsed_selection(&self) -> bool {
549 self.downcast::<HTMLTextAreaElement>()
550 .map(TextControlElement::has_uncollapsed_selection)
551 .or(self
552 .downcast::<HTMLInputElement>()
553 .map(TextControlElement::has_uncollapsed_selection))
554 .unwrap_or_default()
555 }
556
557 fn has_selectable_text(&self) -> bool {
558 self.downcast::<HTMLTextAreaElement>()
559 .map(TextControlElement::has_selectable_text)
560 .or(self
561 .downcast::<HTMLInputElement>()
562 .map(TextControlElement::has_selectable_text))
563 .unwrap_or_default()
564 }
565
566 fn select_all(&self) {
567 self.downcast::<HTMLTextAreaElement>()
568 .map(TextControlElement::select_all)
569 .or(self
570 .downcast::<HTMLInputElement>()
571 .map(TextControlElement::select_all))
572 .unwrap_or_default()
573 }
574}