Rewild Your Web
web browser dweb
at main 967 lines 42 kB view raw
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+}