Rewild Your Web
web
browser
dweb
1--- original
2+++ modified
3@@ -0,0 +1,964 @@
4+/* This Source Code Form is subject to the terms of the Mozilla Public
5+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
7+
8+//! Implementation of embedded webview functionality for HTMLIFrameElement.
9+
10+use std::rc::Rc;
11+
12+use base::Epoch;
13+use base::generic_channel::GenericSender;
14+use base::id::{Index, PipelineId, PipelineNamespaceId};
15+use constellation_traits::{
16+ BlobImpl, EmbeddedWebViewEventType, ScriptToConstellationMessage, TraversalDirection,
17+};
18+use embedder_traits::{
19+ AlertResponse, AllowOrDeny, ConfirmResponse, ContextMenuAction, ContextMenuItem,
20+ EmbeddedWebViewScreenshotError, EmbeddedWebViewScreenshotRequest,
21+ EmbeddedWebViewScreenshotResult, EmbedderControlId, EmbedderControlRequest,
22+ EmbedderControlResponse, InputMethodType, LoadStatus, PermissionFeature, PromptResponse,
23+ RgbColor, ScreenshotImageType, SimpleDialogRequest,
24+};
25+use js::jsval::UndefinedValue;
26+use script_bindings::codegen::GenericBindings::CustomEventBinding::CustomEventMethods;
27+use script_bindings::conversions::SafeToJSValConvertible;
28+use stylo_atoms::Atom;
29+
30+use crate::dom::bindings::codegen::Bindings::EmbeddedWebViewBinding::{
31+ EmbedderColorParameters, EmbedderContextMenuItem, EmbedderContextMenuParameters,
32+ EmbedderControlHideEventDetail, EmbedderControlPosition, EmbedderControlShowEventDetail,
33+ EmbedderDialogShowEventDetail, EmbedderFileParameters, EmbedderInputMethodParameters,
34+ EmbedderNotificationShowEventDetail, EmbedderPermissionParameters, EmbedderSelectOption,
35+ EmbedderSelectParameters, ScreenshotOptions,
36+};
37+use crate::dom::bindings::error::{Error, Fallible};
38+use crate::dom::bindings::inheritance::Castable;
39+use crate::dom::bindings::num::Finite;
40+use crate::dom::bindings::str::{DOMString, USVString};
41+use crate::dom::blob::Blob;
42+use crate::dom::customevent::CustomEvent;
43+use crate::dom::event::Event;
44+use crate::dom::eventtarget::EventTarget;
45+use crate::dom::globalscope::GlobalScope;
46+use crate::dom::html::htmliframeelement::HTMLIFrameElement;
47+use crate::dom::node::NodeTraits;
48+use crate::dom::promise::Promise;
49+use crate::routed_promise::{RoutedPromiseListener, callback_promise};
50+use crate::script_runtime::CanGc;
51+
52+/// Storage for pending dialog response senders.
53+/// When an embedded webview shows a dialog, the IPC sender is stored here
54+/// until the parent shell responds via respondToAlert/Confirm/Prompt.
55+pub(crate) enum PendingDialogSender {
56+ Alert(GenericSender<AlertResponse>),
57+ Confirm(GenericSender<ConfirmResponse>),
58+ Prompt(GenericSender<PromptResponse>),
59+}
60+
61+/// Embedded webview methods for HTMLIFrameElement.
62+/// These methods are separated into this module for easier maintenance.
63+impl HTMLIFrameElement {
64+ /// Dispatch an event from an embedded webview to this iframe element.
65+ /// This is called when the embedded webview's document reports changes
66+ /// (title, URL, load status) that should be exposed to the parent document.
67+ pub(crate) fn dispatch_embedded_webview_event(
68+ &self,
69+ event: EmbeddedWebViewEventType,
70+ can_gc: CanGc,
71+ ) {
72+ // Only dispatch events if this is actually an embedded webview iframe
73+ if !self.is_embedded_webview() {
74+ return;
75+ }
76+
77+ // Update history tracking when constellation sends history change
78+ if let EmbeddedWebViewEventType::HistoryChanged(ref entries, index) = event {
79+ self.set_embedded_history(entries.clone(), index);
80+ }
81+
82+ let event_type = match &event {
83+ EmbeddedWebViewEventType::UrlChanged(_) => Atom::from("embedurlchange"),
84+ EmbeddedWebViewEventType::TitleChanged(_) => Atom::from("embedtitlechange"),
85+ EmbeddedWebViewEventType::LoadStatusChanged(_) => Atom::from("embedloadstatuschange"),
86+ EmbeddedWebViewEventType::FaviconChanged { .. } => Atom::from("embedfaviconchange"),
87+ EmbeddedWebViewEventType::Closed => Atom::from("embedclosed"),
88+ EmbeddedWebViewEventType::HistoryChanged(_, _) => Atom::from("embedhistorychange"),
89+ EmbeddedWebViewEventType::HistoryTraversalComplete(_) => {
90+ Atom::from("embedhistorytraversalcomplete")
91+ },
92+ EmbeddedWebViewEventType::ThemeColorChanged(_) => Atom::from("embedthemecolorchange"),
93+ EmbeddedWebViewEventType::InputReceived => Atom::from("embedinputreceived"),
94+ EmbeddedWebViewEventType::EmbedderControlShow { .. } => Atom::from("embedcontrolshow"),
95+ EmbeddedWebViewEventType::EmbedderControlHide { .. } => Atom::from("embedcontrolhide"),
96+ EmbeddedWebViewEventType::SimpleDialogShow(_) => Atom::from("embeddialogshow"),
97+ EmbeddedWebViewEventType::NotificationShow(_) => Atom::from("embednotificationshow"),
98+ };
99+
100+ let cx = GlobalScope::get_cx();
101+ rooted!(in(*cx) let mut detail = UndefinedValue());
102+
103+ // Convert event data to JS value for CustomEvent detail
104+ match &event {
105+ EmbeddedWebViewEventType::UrlChanged(url) => {
106+ url.as_str().safe_to_jsval(cx, detail.handle_mut(), can_gc);
107+ },
108+ EmbeddedWebViewEventType::TitleChanged(title) => {
109+ // Option<String> needs special handling - convert to null or string
110+ match title {
111+ Some(t) => t.as_str().safe_to_jsval(cx, detail.handle_mut(), can_gc),
112+ None => {
113+ // detail stays as undefined for null/None title
114+ },
115+ }
116+ },
117+ EmbeddedWebViewEventType::LoadStatusChanged(status) => {
118+ let status_str = match status {
119+ LoadStatus::Started => "started",
120+ LoadStatus::HeadParsed => "headparsed",
121+ LoadStatus::Complete => "complete",
122+ };
123+ status_str.safe_to_jsval(cx, detail.handle_mut(), can_gc);
124+ },
125+ EmbeddedWebViewEventType::FaviconChanged {
126+ bytes,
127+ width: _,
128+ height: _,
129+ } => {
130+ // Create a Blob from the favicon image bytes
131+ let global = self.owner_global();
132+ let blob_impl = BlobImpl::new_from_bytes(bytes.to_vec(), "image/png".to_string());
133+ let blob = Blob::new(&global, blob_impl, can_gc);
134+ blob.safe_to_jsval(cx, detail.handle_mut(), can_gc);
135+ },
136+ EmbeddedWebViewEventType::Closed => {
137+ // detail stays as undefined for closed event
138+ },
139+ EmbeddedWebViewEventType::HistoryChanged(_, index) => {
140+ // For history change, expose the current index
141+ (*index as u32).safe_to_jsval(cx, detail.handle_mut(), can_gc);
142+ },
143+ EmbeddedWebViewEventType::HistoryTraversalComplete(traversal_id) => {
144+ // Expose the traversal ID so scripts can match with goBack()/goForward() return values
145+ traversal_id
146+ .to_string()
147+ .safe_to_jsval(cx, detail.handle_mut(), can_gc);
148+ },
149+ EmbeddedWebViewEventType::ThemeColorChanged(content) => {
150+ content.safe_to_jsval(cx, detail.handle_mut(), can_gc);
151+ },
152+ EmbeddedWebViewEventType::InputReceived => {
153+ // detail stays as undefined for input received event
154+ },
155+ EmbeddedWebViewEventType::EmbedderControlShow { id, rect, request } => {
156+ // Create a dictionary-based detail object with control information
157+ let control_type = match &request {
158+ EmbedderControlRequest::SelectElement(..) => "select",
159+ EmbedderControlRequest::ColorPicker(..) => "color",
160+ EmbedderControlRequest::FilePicker(..) => "file",
161+ EmbedderControlRequest::InputMethod(..) => "inputmethod",
162+ EmbedderControlRequest::ContextMenu(..) => "contextmenu",
163+ EmbedderControlRequest::PermissionPrompt(..) => "permission",
164+ };
165+
166+ // Serialize the control ID for response routing
167+ let control_id = format!("{:?}", id);
168+
169+ // Create position dictionary
170+ let position = EmbedderControlPosition {
171+ x: Finite::wrap(rect.min.x as f64),
172+ y: Finite::wrap(rect.min.y as f64),
173+ width: Finite::wrap(rect.width() as f64),
174+ height: Finite::wrap(rect.height() as f64),
175+ };
176+
177+ // Build control-specific parameters
178+ let mut select_parameters = None;
179+ let mut color_parameters = None;
180+ let mut file_parameters = None;
181+ let mut input_method_parameters = None;
182+ let mut context_menu_parameters = None;
183+ let mut permission_parameters = None;
184+
185+ match &request {
186+ EmbedderControlRequest::SelectElement(options, selected) => {
187+ let select_options: Vec<EmbedderSelectOption> = options
188+ .iter()
189+ .flat_map(|opt_or_group| match opt_or_group {
190+ embedder_traits::SelectElementOptionOrOptgroup::Option(opt) => {
191+ vec![EmbedderSelectOption {
192+ id: opt.id as u32,
193+ label: DOMString::from(opt.label.clone()),
194+ disabled: opt.is_disabled,
195+ group: None,
196+ }]
197+ },
198+ embedder_traits::SelectElementOptionOrOptgroup::Optgroup {
199+ label,
200+ options,
201+ } => options
202+ .iter()
203+ .map(|opt| EmbedderSelectOption {
204+ id: opt.id as u32,
205+ label: DOMString::from(opt.label.clone()),
206+ disabled: opt.is_disabled,
207+ group: Some(DOMString::from(label.clone())),
208+ })
209+ .collect(),
210+ })
211+ .collect();
212+ select_parameters = Some(EmbedderSelectParameters {
213+ options: Some(select_options),
214+ selectedIndex: selected.map(|i| i as i32).unwrap_or(-1),
215+ });
216+ },
217+ EmbedderControlRequest::ColorPicker(color) => {
218+ let current_color =
219+ format!("#{:02x}{:02x}{:02x}", color.red, color.green, color.blue);
220+ color_parameters = Some(EmbedderColorParameters {
221+ currentColor: DOMString::from(current_color),
222+ });
223+ },
224+ EmbedderControlRequest::FilePicker(file_request) => {
225+ let accept_types: Vec<DOMString> = file_request
226+ .filter_patterns
227+ .iter()
228+ .map(|p| DOMString::from(format!(".{}", p.0)))
229+ .collect();
230+ file_parameters = Some(EmbedderFileParameters {
231+ multiple: file_request.allow_select_multiple,
232+ acceptTypes: Some(accept_types),
233+ directory: None,
234+ });
235+ },
236+ EmbedderControlRequest::InputMethod(im_request) => {
237+ let input_type = match im_request.input_method_type {
238+ InputMethodType::Color => "color",
239+ InputMethodType::Date => "date",
240+ InputMethodType::DatetimeLocal => "datetime-local",
241+ InputMethodType::Email => "email",
242+ InputMethodType::Month => "month",
243+ InputMethodType::Number => "number",
244+ InputMethodType::Password => "password",
245+ InputMethodType::Search => "search",
246+ InputMethodType::Tel => "tel",
247+ InputMethodType::Text => "text",
248+ InputMethodType::Time => "time",
249+ InputMethodType::Url => "url",
250+ InputMethodType::Week => "week",
251+ };
252+ input_method_parameters = Some(EmbedderInputMethodParameters {
253+ inputType: DOMString::from(input_type),
254+ currentValue: Some(DOMString::from(im_request.text.clone())),
255+ placeholder: None,
256+ });
257+ },
258+ EmbedderControlRequest::ContextMenu(menu_request) => {
259+ // Check history state to properly enable/disable GoBack and GoForward
260+ let (can_go_back, can_go_forward) = self.embedded_history_state();
261+
262+ let items: Vec<EmbedderContextMenuItem> = menu_request
263+ .items
264+ .iter()
265+ .filter_map(|item| match item {
266+ ContextMenuItem::Item {
267+ label,
268+ action,
269+ enabled,
270+ } => {
271+ // Override enabled status for history actions based on actual state
272+ let is_enabled = match action {
273+ ContextMenuAction::GoBack => can_go_back,
274+ ContextMenuAction::GoForward => can_go_forward,
275+ _ => *enabled,
276+ };
277+ Some(EmbedderContextMenuItem {
278+ id: DOMString::from(format!("{:?}", action)),
279+ label: DOMString::from(label.clone()),
280+ disabled: !is_enabled,
281+ checked: false,
282+ icon: None,
283+ })
284+ },
285+ ContextMenuItem::Separator => None,
286+ })
287+ .collect();
288+ context_menu_parameters =
289+ Some(EmbedderContextMenuParameters { items: Some(items) });
290+ },
291+ EmbedderControlRequest::PermissionPrompt(permission_request) => {
292+ // Convert PermissionFeature to string representations
293+ let feature = match permission_request.feature {
294+ PermissionFeature::Geolocation => "geolocation",
295+ PermissionFeature::Notifications => "notifications",
296+ PermissionFeature::Push => "push",
297+ PermissionFeature::Midi => "midi",
298+ PermissionFeature::Camera => "camera",
299+ PermissionFeature::Microphone => "microphone",
300+ PermissionFeature::Speaker => "speaker",
301+ PermissionFeature::DeviceInfo => "device-info",
302+ PermissionFeature::BackgroundSync => "background-sync",
303+ PermissionFeature::Bluetooth => "bluetooth",
304+ PermissionFeature::PersistentStorage => "persistent-storage",
305+ };
306+
307+ let feature_name = match permission_request.feature {
308+ PermissionFeature::Geolocation => "Location",
309+ PermissionFeature::Notifications => "Notifications",
310+ PermissionFeature::Push => "Push",
311+ PermissionFeature::Midi => "MIDI",
312+ PermissionFeature::Camera => "Camera",
313+ PermissionFeature::Microphone => "Microphone",
314+ PermissionFeature::Speaker => "Speaker",
315+ PermissionFeature::DeviceInfo => "Device Info",
316+ PermissionFeature::BackgroundSync => "Background Sync",
317+ PermissionFeature::Bluetooth => "Bluetooth",
318+ PermissionFeature::PersistentStorage => "Persistent Storage",
319+ };
320+
321+ // Store the response sender for later use by respondToPermissionPrompt
322+ self.set_pending_permission_sender(Some(
323+ permission_request.response_sender.clone(),
324+ ));
325+
326+ permission_parameters = Some(EmbedderPermissionParameters {
327+ feature: DOMString::from(feature),
328+ featureName: DOMString::from(feature_name),
329+ });
330+ },
331+ }
332+
333+ // Build the event detail dictionary
334+ let event_detail = EmbedderControlShowEventDetail {
335+ controlType: DOMString::from(control_type),
336+ controlId: DOMString::from(control_id),
337+ position: Some(position),
338+ selectParameters: select_parameters,
339+ colorParameters: color_parameters,
340+ fileParameters: file_parameters,
341+ inputMethodParameters: input_method_parameters,
342+ contextMenuParameters: context_menu_parameters,
343+ permissionParameters: permission_parameters,
344+ };
345+
346+ event_detail.safe_to_jsval(cx, detail.handle_mut(), can_gc);
347+ },
348+ EmbeddedWebViewEventType::EmbedderControlHide { id } => {
349+ // Create a dictionary for hide events
350+ let event_detail = EmbedderControlHideEventDetail {
351+ controlId: DOMString::from(format!("{:?}", id)),
352+ };
353+ event_detail.safe_to_jsval(cx, detail.handle_mut(), can_gc);
354+ },
355+ EmbeddedWebViewEventType::SimpleDialogShow(request) => {
356+ // Clone the request to take ownership since we're matching on a reference
357+ let request = request.clone();
358+
359+ // Extract the request details and store the sender for response routing
360+ let (id, dialog_type_str, message, default_value, sender) = match request {
361+ SimpleDialogRequest::Alert {
362+ id,
363+ message,
364+ response_sender,
365+ } => (
366+ id,
367+ "alert",
368+ message,
369+ None,
370+ PendingDialogSender::Alert(response_sender),
371+ ),
372+ SimpleDialogRequest::Confirm {
373+ id,
374+ message,
375+ response_sender,
376+ } => (
377+ id,
378+ "confirm",
379+ message,
380+ None,
381+ PendingDialogSender::Confirm(response_sender),
382+ ),
383+ SimpleDialogRequest::Prompt {
384+ id,
385+ message,
386+ default,
387+ response_sender,
388+ } => (
389+ id,
390+ "prompt",
391+ message,
392+ Some(default),
393+ PendingDialogSender::Prompt(response_sender),
394+ ),
395+ };
396+
397+ // Store the sender for response routing
398+ self.set_pending_dialog(Some(sender));
399+
400+ let event_detail = EmbedderDialogShowEventDetail {
401+ dialogType: DOMString::from(dialog_type_str),
402+ controlId: DOMString::from(format!("{:?}", id)),
403+ message: DOMString::from(message),
404+ defaultValue: Some(default_value.map(DOMString::from)),
405+ };
406+ event_detail.safe_to_jsval(cx, detail.handle_mut(), can_gc);
407+ },
408+ EmbeddedWebViewEventType::NotificationShow(notification) => {
409+ let event_detail = EmbedderNotificationShowEventDetail {
410+ title: DOMString::from(notification.title.clone()),
411+ body: DOMString::from(notification.body.clone()),
412+ tag: DOMString::from(notification.tag.clone()),
413+ iconUrl: notification
414+ .icon_url
415+ .as_ref()
416+ .map(|url| DOMString::from(url.as_str())),
417+ };
418+ event_detail.safe_to_jsval(cx, detail.handle_mut(), can_gc);
419+ },
420+ }
421+
422+ let global = self.owner_global();
423+ let custom_event = CustomEvent::new_uninitialized(&global, can_gc);
424+ custom_event.InitCustomEvent(
425+ cx,
426+ DOMString::from(event_type.as_ref()),
427+ false, // bubbles
428+ false, // cancelable
429+ detail.handle(),
430+ );
431+
432+ custom_event
433+ .upcast::<Event>()
434+ .fire(self.upcast::<EventTarget>(), can_gc);
435+ }
436+
437+ /// Parse a control ID string back into an EmbedderControlId.
438+ /// The format is the Debug output: "EmbedderControlId { webview_id: ..., pipeline_id: ..., index: ... }"
439+ fn parse_control_id(&self, control_id: &DOMString) -> Fallible<EmbedderControlId> {
440+ if !self.is_embedded_webview() {
441+ return Err(Error::InvalidState(Some(
442+ "This iframe is not an embedded webview".to_string(),
443+ )));
444+ }
445+
446+ let s = control_id.str();
447+
448+ // Parse the control ID from Debug format:
449+ // "EmbedderControlId { webview_id: WebViewId(PainterId(N), (A,B)), pipeline_id: (X,Y), index: Epoch(E) }"
450+
451+ // Extract pipeline_id: (X,Y)
452+ let pipeline_id = if let Some(idx) = s.find("pipeline_id: (") {
453+ let start = idx + 14; // length of "pipeline_id: ("
454+ let end = s[start..].find(')').map(|e| start + e).unwrap_or(s.len());
455+ let parts: Vec<&str> = s[start..end].split(',').collect();
456+ if parts.len() == 2 {
457+ let namespace = parts[0].trim().parse::<u32>().ok();
458+ let index = parts[1].trim().parse::<u32>().ok();
459+ match (namespace, index) {
460+ (Some(ns), Some(idx)) => Index::new(idx).ok().map(|index| PipelineId {
461+ namespace_id: PipelineNamespaceId(ns),
462+ index,
463+ }),
464+ _ => None,
465+ }
466+ } else {
467+ None
468+ }
469+ } else {
470+ None
471+ };
472+
473+ let pipeline_id = pipeline_id.ok_or_else(|| {
474+ Error::InvalidState(Some(format!(
475+ "Could not parse pipeline_id from control ID: {:?}",
476+ s
477+ )))
478+ })?;
479+
480+ // Extract webview_id from the embedded webview or parse from string
481+ // The webview_id format is: WebViewId(PainterId(N), (A,B))
482+ // For simplicity, use the stored embedded_webview_id since it should match
483+ let webview_id = self
484+ .embedded_webview_id()
485+ .ok_or_else(|| Error::InvalidState(Some("No embedded webview ID".to_string())))?;
486+
487+ // Extract the epoch from the control ID string
488+ // Look for "index: Epoch(" and extract the number
489+ let epoch = if let Some(idx) = s.find("Epoch(") {
490+ let start = idx + 6;
491+ let end = s[start..].find(')').map(|e| start + e).unwrap_or(s.len());
492+ s[start..end].parse::<u32>().unwrap_or(0)
493+ } else {
494+ 0
495+ };
496+
497+ Ok(EmbedderControlId {
498+ webview_id,
499+ pipeline_id,
500+ index: Epoch(epoch),
501+ })
502+ }
503+
504+ /// Send a control response to the embedded webview.
505+ fn send_control_response(
506+ &self,
507+ id: EmbedderControlId,
508+ response: EmbedderControlResponse,
509+ ) -> Fallible<()> {
510+ if !self.is_embedded_webview() {
511+ return Err(Error::InvalidState(Some(
512+ "This iframe is not an embedded webview".to_string(),
513+ )));
514+ }
515+
516+ let window = self.owner_window();
517+ window
518+ .as_global_scope()
519+ .script_to_constellation_chan()
520+ .send(ScriptToConstellationMessage::EmbeddedWebViewControlResponse(id, response))
521+ .unwrap();
522+
523+ Ok(())
524+ }
525+
526+ // ========== Embedded WebView WebIDL helper methods ==========
527+ // These are called from the HTMLIFrameElementMethods impl in htmliframeelement.rs
528+
529+ /// Load a URL in the embedded webview.
530+ pub(crate) fn embedded_load(&self, url: USVString) -> Fallible<()> {
531+ let Some(webview_id) = self.embedded_webview_id() else {
532+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
533+ };
534+
535+ // Parse and validate URL
536+ let base_url = self.owner_document().base_url();
537+ let Ok(url) = base_url.join(&url.0) else {
538+ return Ok(());
539+ };
540+
541+ let window = self.owner_window();
542+ window
543+ .as_global_scope()
544+ .script_to_constellation_chan()
545+ .send(ScriptToConstellationMessage::EmbeddedWebViewLoad(
546+ webview_id, url,
547+ ))
548+ .unwrap();
549+ Ok(())
550+ }
551+
552+ /// Get the current page zoom level for the embedded webview.
553+ pub(crate) fn embedded_page_zoom(&self) -> Fallible<f64> {
554+ if !self.is_embedded_webview() {
555+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
556+ }
557+ Ok(self.get_page_zoom())
558+ }
559+
560+ /// Set the page zoom level for the embedded webview.
561+ pub(crate) fn embedded_set_page_zoom(&self, zoom: f64) -> Fallible<()> {
562+ let Some(webview_id) = self.embedded_webview_id() else {
563+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
564+ };
565+
566+ // Clamp to valid range (0.1 to 10.0)
567+ let zoom = zoom.clamp(0.1, 10.0);
568+ self.set_page_zoom(zoom);
569+
570+ let window = self.owner_window();
571+ window
572+ .as_global_scope()
573+ .script_to_constellation_chan()
574+ .send(ScriptToConstellationMessage::EmbeddedWebViewSetPageZoom(
575+ webview_id,
576+ zoom as f32,
577+ ))
578+ .unwrap();
579+ Ok(())
580+ }
581+
582+ /// Reload the embedded webview.
583+ pub(crate) fn embedded_reload(&self) -> Fallible<()> {
584+ let Some(webview_id) = self.embedded_webview_id() else {
585+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
586+ };
587+
588+ let window = self.owner_window();
589+ window
590+ .as_global_scope()
591+ .script_to_constellation_chan()
592+ .send(ScriptToConstellationMessage::EmbeddedWebViewReload(
593+ webview_id,
594+ ))
595+ .unwrap();
596+ Ok(())
597+ }
598+
599+ /// Returns true if the embedded webview can navigate back in history.
600+ pub(crate) fn embedded_can_go_back(&self) -> Fallible<bool> {
601+ if !self.is_embedded_webview() {
602+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
603+ }
604+ let (can_go_back, _) = self.embedded_history_state();
605+ Ok(can_go_back)
606+ }
607+
608+ /// Navigate back in the embedded webview's history.
609+ pub(crate) fn embedded_go_back(&self) -> Fallible<DOMString> {
610+ let Some(webview_id) = self.embedded_webview_id() else {
611+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
612+ };
613+ let (can_go_back, _) = self.embedded_history_state();
614+ if !can_go_back {
615+ return Ok(DOMString::new()); // Can't go back
616+ }
617+
618+ let traversal_id = embedder_traits::TraversalId::new();
619+ let window = self.owner_window();
620+ window
621+ .as_global_scope()
622+ .script_to_constellation_chan()
623+ .send(
624+ ScriptToConstellationMessage::EmbeddedWebViewTraverseHistory(
625+ webview_id,
626+ TraversalDirection::Back(1),
627+ traversal_id.clone(),
628+ ),
629+ )
630+ .unwrap();
631+ Ok(DOMString::from(traversal_id.to_string()))
632+ }
633+
634+ /// Returns true if the embedded webview can navigate forward in history.
635+ pub(crate) fn embedded_can_go_forward(&self) -> Fallible<bool> {
636+ if !self.is_embedded_webview() {
637+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
638+ }
639+ let (_, can_go_forward) = self.embedded_history_state();
640+ Ok(can_go_forward)
641+ }
642+
643+ /// Navigate forward in the embedded webview's history.
644+ pub(crate) fn embedded_go_forward(&self) -> Fallible<DOMString> {
645+ let Some(webview_id) = self.embedded_webview_id() else {
646+ return Err(crate::dom::bindings::error::Error::InvalidState(None));
647+ };
648+ let (_, can_go_forward) = self.embedded_history_state();
649+ if !can_go_forward {
650+ return Ok(DOMString::new()); // Can't go forward
651+ }
652+
653+ let traversal_id = embedder_traits::TraversalId::new();
654+ let window = self.owner_window();
655+ window
656+ .as_global_scope()
657+ .script_to_constellation_chan()
658+ .send(
659+ ScriptToConstellationMessage::EmbeddedWebViewTraverseHistory(
660+ webview_id,
661+ TraversalDirection::Forward(1),
662+ traversal_id.clone(),
663+ ),
664+ )
665+ .unwrap();
666+ Ok(DOMString::from(traversal_id.to_string()))
667+ }
668+
669+ /// Take a screenshot of the embedded webview.
670+ pub(crate) fn embedded_take_screenshot(
671+ &self,
672+ options: &ScreenshotOptions,
673+ ) -> Fallible<Rc<Promise>> {
674+ let can_gc = CanGc::note();
675+
676+ // Must be an embedded webview
677+ let Some(webview_id) = self.embedded_webview_id() else {
678+ return Err(Error::InvalidState(Some(
679+ "This iframe is not an embedded webview".to_string(),
680+ )));
681+ };
682+
683+ let global = self.owner_global();
684+ let promise = Promise::new(&global, can_gc);
685+
686+ // Parse image type from options
687+ let image_type = match &*options.type_.str() {
688+ "image/jpeg" => ScreenshotImageType::Jpeg,
689+ "image/webp" => ScreenshotImageType::Webp,
690+ _ => ScreenshotImageType::Png, // Default to PNG for unknown types
691+ };
692+
693+ // Clamp quality to valid range (0.0 to 1.0)
694+ let quality = options.quality.clamp(0.0, 1.0);
695+
696+ let request = EmbeddedWebViewScreenshotRequest {
697+ image_type,
698+ quality,
699+ };
700+
701+ let callback = callback_promise(
702+ &promise,
703+ self,
704+ global.task_manager().dom_manipulation_task_source(),
705+ );
706+
707+ // Send the screenshot request to the constellation
708+ global
709+ .script_to_constellation_chan()
710+ .send(ScriptToConstellationMessage::EmbeddedWebViewTakeScreenshot(
711+ webview_id, request, callback,
712+ ))
713+ .unwrap();
714+
715+ Ok(promise)
716+ }
717+
718+ /// Respond to a select control request.
719+ pub(crate) fn embedded_respond_to_select_control(
720+ &self,
721+ control_id: DOMString,
722+ selected_index: i32,
723+ ) -> Fallible<()> {
724+ let id = self.parse_control_id(&control_id)?;
725+ let response = if selected_index < 0 {
726+ EmbedderControlResponse::SelectElement(None)
727+ } else {
728+ EmbedderControlResponse::SelectElement(Some(selected_index as usize))
729+ };
730+ self.send_control_response(id, response)
731+ }
732+
733+ /// Respond to a color picker request.
734+ pub(crate) fn embedded_respond_to_color_picker(
735+ &self,
736+ control_id: DOMString,
737+ color: Option<DOMString>,
738+ ) -> Fallible<()> {
739+ let id = self.parse_control_id(&control_id)?;
740+ let response = match color {
741+ Some(ref c) => EmbedderControlResponse::ColorPicker(parse_color(c)),
742+ None => EmbedderControlResponse::ColorPicker(None),
743+ };
744+ self.send_control_response(id, response)
745+ }
746+
747+ /// Respond to a context menu request with the selected action.
748+ pub(crate) fn embedded_respond_to_context_menu(
749+ &self,
750+ control_id: DOMString,
751+ action_id: Option<DOMString>,
752+ ) -> Fallible<()> {
753+ let id = self.parse_control_id(&control_id)?;
754+
755+ // Parse the action ID string back to a ContextMenuAction
756+ let action = action_id.and_then(|action_str| {
757+ let s = action_str.str();
758+ if s == "GoBack" {
759+ Some(ContextMenuAction::GoBack)
760+ } else if s == "GoForward" {
761+ Some(ContextMenuAction::GoForward)
762+ } else if s == "Reload" {
763+ Some(ContextMenuAction::Reload)
764+ } else if s == "Cut" {
765+ Some(ContextMenuAction::Cut)
766+ } else if s == "Copy" {
767+ Some(ContextMenuAction::Copy)
768+ } else if s == "Paste" {
769+ Some(ContextMenuAction::Paste)
770+ } else if s == "SelectAll" {
771+ Some(ContextMenuAction::SelectAll)
772+ } else if s == "CopyLink" {
773+ Some(ContextMenuAction::CopyLink)
774+ } else if s == "OpenLinkInNewWebView" {
775+ Some(ContextMenuAction::OpenLinkInNewWebView)
776+ } else if s == "CopyImageLink" {
777+ Some(ContextMenuAction::CopyImageLink)
778+ } else if s == "OpenImageInNewView" {
779+ Some(ContextMenuAction::OpenImageInNewView)
780+ } else {
781+ None
782+ }
783+ });
784+
785+ let response = EmbedderControlResponse::ContextMenu(action);
786+ self.send_control_response(id, response)
787+ }
788+
789+ /// Cancel an embedder control request.
790+ pub(crate) fn embedded_cancel_embedder_control(&self, control_id: DOMString) -> Fallible<()> {
791+ let id = self.parse_control_id(&control_id)?;
792+ let response = EmbedderControlResponse::SelectElement(None);
793+ self.send_control_response(id, response)
794+ }
795+
796+ /// Respond to an alert dialog request.
797+ pub(crate) fn embedded_respond_to_alert(&self, _control_id: DOMString) -> Fallible<()> {
798+ if !self.is_embedded_webview() {
799+ return Err(Error::InvalidState(Some(
800+ "This iframe is not an embedded webview".to_string(),
801+ )));
802+ }
803+
804+ let Some(sender) = self.take_pending_dialog() else {
805+ return Err(Error::InvalidState(Some(
806+ "No pending dialog to respond to".to_string(),
807+ )));
808+ };
809+
810+ match sender {
811+ PendingDialogSender::Alert(sender) => {
812+ let _ = sender.send(AlertResponse::Ok);
813+ Ok(())
814+ },
815+ _ => Err(Error::InvalidState(Some(
816+ "Pending dialog is not an alert".to_string(),
817+ ))),
818+ }
819+ }
820+
821+ /// Respond to a confirm dialog request.
822+ pub(crate) fn embedded_respond_to_confirm(
823+ &self,
824+ _control_id: DOMString,
825+ confirmed: bool,
826+ ) -> Fallible<()> {
827+ if !self.is_embedded_webview() {
828+ return Err(Error::InvalidState(Some(
829+ "This iframe is not an embedded webview".to_string(),
830+ )));
831+ }
832+
833+ let Some(sender) = self.take_pending_dialog() else {
834+ return Err(Error::InvalidState(Some(
835+ "No pending dialog to respond to".to_string(),
836+ )));
837+ };
838+
839+ match sender {
840+ PendingDialogSender::Confirm(sender) => {
841+ let response = if confirmed {
842+ ConfirmResponse::Ok
843+ } else {
844+ ConfirmResponse::Cancel
845+ };
846+ let _ = sender.send(response);
847+ Ok(())
848+ },
849+ _ => Err(Error::InvalidState(Some(
850+ "Pending dialog is not a confirm".to_string(),
851+ ))),
852+ }
853+ }
854+
855+ /// Respond to a prompt dialog request.
856+ pub(crate) fn embedded_respond_to_prompt(
857+ &self,
858+ _control_id: DOMString,
859+ value: Option<DOMString>,
860+ ) -> Fallible<()> {
861+ if !self.is_embedded_webview() {
862+ return Err(Error::InvalidState(Some(
863+ "This iframe is not an embedded webview".to_string(),
864+ )));
865+ }
866+
867+ let Some(sender) = self.take_pending_dialog() else {
868+ return Err(Error::InvalidState(Some(
869+ "No pending dialog to respond to".to_string(),
870+ )));
871+ };
872+
873+ match sender {
874+ PendingDialogSender::Prompt(sender) => {
875+ let response = match value {
876+ Some(s) => PromptResponse::Ok(s.to_string()),
877+ None => PromptResponse::Cancel,
878+ };
879+ let _ = sender.send(response);
880+ Ok(())
881+ },
882+ _ => Err(Error::InvalidState(Some(
883+ "Pending dialog is not a prompt".to_string(),
884+ ))),
885+ }
886+ }
887+
888+ /// Respond to a permission prompt request.
889+ pub(crate) fn embedded_respond_to_permission_prompt(
890+ &self,
891+ _control_id: DOMString,
892+ allowed: bool,
893+ ) -> Fallible<()> {
894+ if !self.is_embedded_webview() {
895+ return Err(Error::InvalidState(Some(
896+ "This iframe is not an embedded webview".to_string(),
897+ )));
898+ }
899+
900+ let Some(sender) = self.take_pending_permission_sender() else {
901+ return Err(Error::InvalidState(Some(
902+ "No pending permission prompt to respond to".to_string(),
903+ )));
904+ };
905+
906+ let response = if allowed {
907+ AllowOrDeny::Allow
908+ } else {
909+ AllowOrDeny::Deny
910+ };
911+ let _ = sender.send(response);
912+ Ok(())
913+ }
914+}
915+
916+/// Handle screenshot responses from the compositor via IPC.
917+impl RoutedPromiseListener<Result<EmbeddedWebViewScreenshotResult, EmbeddedWebViewScreenshotError>>
918+ for HTMLIFrameElement
919+{
920+ fn handle_response(
921+ &self,
922+ response: Result<EmbeddedWebViewScreenshotResult, EmbeddedWebViewScreenshotError>,
923+ promise: &Rc<Promise>,
924+ can_gc: CanGc,
925+ ) {
926+ match response {
927+ Ok(result) => {
928+ // Create a Blob from the encoded bytes
929+ let global = self.owner_global();
930+ let blob_impl = BlobImpl::new_from_bytes(result.bytes, result.mime_type);
931+ let blob = Blob::new(&global, blob_impl, can_gc);
932+ promise.resolve_native(&blob, can_gc);
933+ },
934+ Err(error) => {
935+ let error_msg = match error {
936+ EmbeddedWebViewScreenshotError::NotEmbeddedWebView => {
937+ "This iframe is not an embedded webview"
938+ },
939+ EmbeddedWebViewScreenshotError::WebViewDoesNotExist => {
940+ "The webview does not exist"
941+ },
942+ EmbeddedWebViewScreenshotError::CaptureFailed => "Failed to capture screenshot",
943+ EmbeddedWebViewScreenshotError::EncodingFailed => "Failed to encode screenshot",
944+ };
945+ promise.reject_error(Error::InvalidState(Some(error_msg.to_string())), can_gc);
946+ },
947+ }
948+ }
949+}
950+
951+/// Parse a CSS color string (e.g., "#ff0000") into an RgbColor.
952+fn parse_color(color: &DOMString) -> Option<RgbColor> {
953+ let color_str = color.str();
954+ let s = color_str.trim();
955+
956+ // Parse hex format: #RRGGBB
957+ if let Some(hex) = s.strip_prefix('#') {
958+ if hex.len() == 6 {
959+ let red = u8::from_str_radix(&hex[0..2], 16).ok()?;
960+ let green = u8::from_str_radix(&hex[2..4], 16).ok()?;
961+ let blue = u8::from_str_radix(&hex[4..6], 16).ok()?;
962+ return Some(RgbColor { red, green, blue });
963+ }
964+ }
965+
966+ None
967+}