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