blob: 2bba4df3407cf4d7b9c477ba5a3bec0fb52f9f29 [file] [log] [blame] [edit]
// Copyright (C) 2024 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
import SwiftUI
import UniformTypeIdentifiers
import WebKit
@_spi(Testing) import _WebKit_SwiftUI
private struct DialogActionsView: View {
private let dialog: DialogPresenter.Dialog
@State private var promptText = ""
init(dialog: DialogPresenter.Dialog) {
self.dialog = dialog
if case let .prompt(_, defaultText, _) = dialog.configuration, let defaultText {
_promptText = State(initialValue: defaultText)
}
}
var body: some View {
switch dialog.configuration {
case let .alert(_, dismissAlert):
Button("Close", action: dismissAlert)
case let .confirm(_, dismissConfirm):
Button("OK") {
dismissConfirm(.ok)
}
Button("Cancel", role: .cancel) {
dismissConfirm(.cancel)
}
case let .prompt(_, _, dismissPrompt):
TextField("Text", text: $promptText)
Button("OK") {
dismissPrompt(.ok(promptText))
}
.keyboardShortcut(.defaultAction)
Button("Cancel", role: .cancel) {
dismissPrompt(.cancel)
}
}
}
}
private struct DialogMessageView: View {
let dialog: DialogPresenter.Dialog
var body: some View {
switch dialog.configuration {
case let .alert(message, _):
Text(message)
case let .confirm(message, _):
Text(message)
case let .prompt(message, _, _):
Text(message)
}
}
}
struct ContentView: View {
@Binding
var url: URL?
let initialRequest: URLRequest
@State
private var findNavigatorIsPresented = false
@Environment(\.openWindow)
private var openWindow
@Environment(BrowserViewModel.self)
private var viewModel
@AppStorage(AppStorageKeys.scrollBounceBehaviorBasedOnSize)
private var scrollBounceBehaviorBasedOnSize: Bool?
@AppStorage(AppStorageKeys.backgroundHidden)
private var backgroundHidden: Bool?
@AppStorage(AppStorageKeys.showColorInTabBar)
private var showColorInTabBar: Bool = true
var body: some View {
NavigationStack {
@Bindable var viewModel = viewModel
WebView(viewModel.page)
.webViewBackForwardNavigationGestures(.enabled)
.webViewLinkPreviews(.enabled)
.webViewTextSelection(.enabled)
.webViewElementFullscreenBehavior(.enabled)
.findNavigator(isPresented: $findNavigatorIsPresented)
.task {
do {
#if compiler(>=6.2)
// Safety: this is actually safe; false positive is rdar://154775389
for try await unsafe event in viewModel.page.navigations {
print(event)
}
#else
for try await event in viewModel.page.navigations {
print(event)
}
#endif
} catch {
print(error)
}
}
.onAppear {
viewModel.displayedURL = initialRequest.url!.absoluteString
viewModel.navigateToSubmittedURL()
}
.onChange(of: viewModel.page.url) { _, newValue in
url = newValue
}
.onChange(of: viewModel.currentOpenRequest) { _, newValue in
guard let newValue else {
return
}
openWindow(value: CodableURLRequest(newValue.request))
viewModel.currentOpenRequest = nil
}
.navigationTitle(viewModel.page.title)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.focusedSceneValue(viewModel)
.onOpenURL(perform: viewModel.openURL(_:))
.fileExporter(isPresented: $viewModel.pdfExporterIsPresented, item: viewModel.exportedPDF, defaultFilename: viewModel.exportedPDF?.title, onCompletion: viewModel.didExportPDF(result:))
.fileImporter(isPresented: $viewModel.isPresentingFilePicker, allowedContentTypes: [.png, .pdf], allowsMultipleSelection: viewModel.currentFilePicker?.allowsMultipleSelection ?? false, onCompletion: viewModel.didImportFiles(result:))
.alert("\(url?.absoluteString ?? "") says:", isPresented: $viewModel.isPresentingDialog, presenting: viewModel.currentDialog) { dialog in
DialogActionsView(dialog: dialog)
} message: { dialog in
DialogMessageView(dialog: dialog)
}
.scrollBounceBehavior(scrollBounceBehaviorBasedOnSize == true ? .basedOnSize : .automatic)
.webViewContentBackground(backgroundHidden == true ? .hidden : .automatic)
.webViewScrollEdgeEffectStyle(showColorInTabBar ? .soft : .hard, for: .all)
.webContextMenu()
.webToolbar(findNavigatorIsPresented: $findNavigatorIsPresented)
}
}
}
#Preview {
@Previewable @State var viewModel = BrowserViewModel()
@Previewable @State var url: URL? = nil
let request = {
let url = URL(string: "https://www.apple.com")!
return URLRequest(url: url)
}()
ContentView(url: $url, initialRequest: request)
.environment(viewModel)
}