ContentDelegate.swift (12040B)
1 // This Source Code Form is subject to the terms of the Mozilla Public 2 // License, v. 2.0. If a copy of the MPL was not distributed with this 3 // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 /// Element details for onContextMenu callbacks 6 public struct ContextElement { 7 public enum ElementType { 8 case none, image, video, audio 9 } 10 11 /// The base URI of the element's document. 12 public let baseUri: String? 13 14 /// The absolute link URI (href) of the element. 15 public let linkUri: String? 16 17 /// The title text of the element. 18 public let title: String? 19 20 /// The alternative text (alt) for the element. 21 public let altText: String? 22 23 /// The type of the element. One of the flags. 24 public let type: ElementType 25 26 /// The source URI (src) of the element. Set for (nested) media elements. 27 public let srcUri: String? 28 29 /// The text content of the element 30 public let textContent: String? 31 } 32 33 public enum SlowScriptResponse { 34 case halt, resume 35 } 36 37 public protocol ContentDelegate { 38 /// A page title was discovered in the content or updated after the content 39 /// loaded. 40 func onTitleChange(session: GeckoSession, title: String) 41 42 /// A preview image was discovered in the content after the content loaded. 43 func onPreviewImage(session: GeckoSession, previewImageUrl: String) 44 45 /// A page has requested focus. Note that window.focus() in content will not 46 /// result in this being called. 47 func onFocusRequest(session: GeckoSession) 48 49 /// A page has requested to close 50 func onCloseRequest(session: GeckoSession) 51 52 /// A page has entered or exited full screen mode. 53 /// 54 /// Typically the implementation would set the GeckoView to full screen when 55 /// the page is in full screen mode. 56 func onFullScreen(session: GeckoSession, fullScreen: Bool) 57 58 /// A viewport-filt was discovered in the content or updated after the 59 /// content. 60 /// 61 /// See https://drafts.csswg.org/css-round-display/#viewport-fit-descriptor 62 func onMetaViewportFitChange(session: GeckoSession, viewportFit: String) 63 64 /// Session is on a product url. 65 func onProductUrl(session: GeckoSession) 66 67 /// A user has initiated the context menu via long-press. 68 /// 69 /// This event is fired on links, (nested) images, and (nested) media 70 /// elements. 71 func onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, element: ContextElement) 72 73 /// This is fired when there is a response that cannot be handled by Gecko 74 /// (e.g. a download). 75 // FIXME: Implement onExternalResponse & WebResponse 76 // func onExternalResponse(session: GeckoSession, response: WebResponse) 77 78 /// The content process hosting this GeckoSession has crashed. 79 /// 80 /// The GeckoSession is now closed and unusable. You may call `open` to 81 /// recover the session, but no state is preserved. Most applications will 82 /// want to call `load` or `restoreState` at this point. 83 func onCrash(session: GeckoSession) 84 85 /// The content process hosting this GeckoSession has been killed. 86 /// 87 /// The GeckoSession is now closed and unusable. You may call `open` to 88 /// recover the session, but no state is preserved. Most applications will 89 /// want to call `load` or `restoreState` at this point. 90 func onKill(session: GeckoSession) 91 92 /// Notification that the first content composition has occurred. 93 /// 94 /// This callback is invoked for the first content composite after either a 95 /// start or a restart of the compositor. 96 func onFirstComposite(session: GeckoSession) 97 98 /// Notification that the first content paint has occurred. 99 /// 100 /// This callback is invoked for the first content paint after a page has 101 /// been loaded, or after a `onPaintStatusReset` event. The 102 /// `onFirstComposite` will be called once the compositor has started 103 /// rendering. 104 /// 105 /// However, it is possible for the compositor to start rendering before 106 /// there is any content to render. `onFirstContentfulPaint` is called once 107 /// some content has been rendered. It may be nothing more than the page 108 /// background color. It is not an indication that the whole page has been 109 /// rendered. 110 func onFirstContentfulPaint(session: GeckoSession) 111 112 /// Notification that the paint status has been reset. 113 /// 114 /// This callback is invoked whenever the painted content is no longer being 115 /// displayed. This can occur in response to the session being paused. 116 /// After this has fired the compositor may continue rendering, but may not 117 /// render the page content. This callback can therefore be used in 118 /// conjunction with `onFirstContentfulPaint` to determine when there is 119 /// valid content being rendered. 120 func onPaintStatusReset(session: GeckoSession) 121 122 /// This is fired when the loaded document has a valid Web App Manifest 123 /// present. 124 /// 125 /// The various colors (theme_color, background_color, etc.) present in the 126 /// manifest have been transformed into #AARRGGBB format. 127 /// 128 /// See https://www.w3.org/TR/appmanifest/ 129 func onWebAppManifest(session: GeckoSession, manifest: Any) 130 131 /// A script has exceeded its execution timeout value 132 /// 133 /// Returning `.halt` will halt the slow script, and `.resume` will pause 134 /// notifications for a period of time before resuming. 135 func onSlowScript(session: GeckoSession, scriptFileName: String) async -> SlowScriptResponse 136 137 /// The app should display its dynamic toolbar, fully expanded to the height 138 /// that was previously specified via 139 /// `GeckoView.setDynamicToolbarMaxHeight`. 140 func onShowDynamicToolbar(session: GeckoSession) 141 142 /// This method is called when a cookie banner is detected. 143 /// 144 /// Note: this method is called only if the cookie banner setting is such 145 /// that allows to handle the banner. For example, if 146 /// `cookiebanners.service.mode=1` (Reject only), but a cookie banner can 147 /// only be accepted on the website - the detection in that case won't be 148 /// reported. The exception is `MODE_DETECT_ONLY` mode, when only the 149 /// detection event is emitted. 150 func onCookieBannerDetected(session: GeckoSession) 151 152 /// This method is called when a cookie banner was handled. 153 func onCookieBannerHandled(session: GeckoSession) 154 } 155 156 enum ContentEvents: String, CaseIterable { 157 case contentCrash = "GeckoView:ContentCrash" 158 case contentKill = "GeckoView:ContentKill" 159 case contextMenu = "GeckoView:ContextMenu" 160 case domMetaViewportFit = "GeckoView:DOMMetaViewportFit" 161 case pageTitleChanged = "GeckoView:PageTitleChanged" 162 case domWindowClose = "GeckoView:DOMWindowClose" 163 case externalResponse = "GeckoView:ExternalResponse" 164 case focusRequest = "GeckoView:FocusRequest" 165 case fullscreenEnter = "GeckoView:FullScreenEnter" 166 case fullscreenExit = "GeckoView:FullScreenExit" 167 case webAppManifest = "GeckoView:WebAppManifest" 168 case firstContentfulPaint = "GeckoView:FirstContentfulPaint" 169 case paintStatusReset = "GeckoView:PaintStatusReset" 170 case previewImage = "GeckoView:PreviewImage" 171 case cookieBannerEventDetected = "GeckoView:CookieBannerEvent:Detected" 172 case cookieBannerEventHandled = "GeckoView:CookieBannerEvent:Handled" 173 case savePdf = "GeckoView:SavePdf" 174 case onProductUrl = "GeckoView:OnProductUrl" 175 } 176 177 func newContentHandler(_ session: GeckoSession) -> GeckoSessionHandler< 178 ContentDelegate, ContentEvents 179 > { 180 GeckoSessionHandler(moduleName: "GeckoViewContent", session: session) { 181 @MainActor session, delegate, event, message in 182 switch event { 183 case .contentCrash: 184 session.close() 185 delegate?.onCrash(session: session) 186 return nil 187 case .contentKill: 188 session.close() 189 delegate?.onKill(session: session) 190 return nil 191 case .contextMenu: 192 func parseType(type: String) -> ContextElement.ElementType { 193 switch type { 194 case "HTMLImageElement": return .image 195 case "HTMLVideoElement": return .video 196 case "HTMLAudioElement": return .audio 197 default: return .none 198 } 199 } 200 201 let contextElement = ContextElement( 202 baseUri: message!["baseUri"] as? String, 203 linkUri: message!["linkUri"] as? String, 204 title: message!["title"] as? String, 205 altText: message!["alt"] as? String, 206 type: parseType(type: message!["elementType"] as! String), 207 srcUri: message!["elementSrc"] as? String, 208 textContent: message!["textContent"] as? String) 209 210 delegate?.onContextMenu( 211 session: session, 212 screenX: message!["screenX"] as! Int, 213 screenY: message!["screenY"] as! Int, 214 element: contextElement) 215 return nil 216 case .domMetaViewportFit: 217 delegate?.onMetaViewportFitChange( 218 session: session, viewportFit: message!["viewportfit"] as! String) 219 return nil 220 case .pageTitleChanged: 221 delegate?.onTitleChange(session: session, title: message!["title"] as! String) 222 return nil 223 case .domWindowClose: 224 delegate?.onCloseRequest(session: session) 225 return nil 226 case .externalResponse: 227 // FIXME: implement 228 throw HandlerError("GeckoView:ExternalResponse is unimplemented") 229 case .focusRequest: 230 delegate?.onFocusRequest(session: session) 231 return nil 232 case .fullscreenEnter: 233 delegate?.onFullScreen(session: session, fullScreen: true) 234 return nil 235 case .fullscreenExit: 236 delegate?.onFullScreen(session: session, fullScreen: false) 237 return nil 238 case .webAppManifest: 239 delegate?.onWebAppManifest(session: session, manifest: message!["manifest"]!!) 240 return nil 241 case .firstContentfulPaint: 242 delegate?.onFirstContentfulPaint(session: session) 243 return nil 244 case .paintStatusReset: 245 delegate?.onPaintStatusReset(session: session) 246 return nil 247 case .previewImage: 248 delegate?.onPreviewImage( 249 session: session, previewImageUrl: message!["previewImageUrl"] as! String) 250 return nil 251 case .cookieBannerEventDetected: 252 delegate?.onCookieBannerDetected(session: session) 253 return nil 254 case .cookieBannerEventHandled: 255 delegate?.onCookieBannerHandled(session: session) 256 return nil 257 case .savePdf: 258 // FIXME: implement 259 throw HandlerError("GeckoView:SavePdf is unimplemented") 260 case .onProductUrl: 261 delegate?.onProductUrl(session: session) 262 return nil 263 } 264 } 265 } 266 267 enum ProcessHangEvents: String, CaseIterable { 268 case hangReport = "GeckoView:HangReport" 269 } 270 271 func newProcessHangHandler(_ session: GeckoSession) -> GeckoSessionHandler< 272 ContentDelegate, ProcessHangEvents 273 > { 274 GeckoSessionHandler(moduleName: "GeckoViewProcessHangMonitor", session: session) { 275 @MainActor session, delegate, event, message in 276 switch event { 277 case .hangReport: 278 let reportId = message!["hangId"] as! Int 279 let response = await delegate?.onSlowScript( 280 session: session, scriptFileName: message!["scriptFileName"] as! String) 281 switch response { 282 case .resume: 283 session.dispatcher.dispatch( 284 type: "GeckoView:HangReportWait", message: ["hangId": reportId]) 285 default: 286 session.dispatcher.dispatch( 287 type: "GeckoView:HangReportStop", message: ["hangId": reportId]) 288 } 289 return nil 290 } 291 } 292 }