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