snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 17:00:42 +05:00
commit feda81bf2f
1178 changed files with 156404 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
let CHANNEL_PREFIX = "__CHANNEL__:"
let channelDataKey = CodingUserInfoKey(rawValue: "sendChannelData")!
public class Channel: Decodable {
public let id: UInt64
let handler: (UInt64, String) -> Void
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let channelDef = try container.decode(String.self)
let components = channelDef.components(separatedBy: CHANNEL_PREFIX)
if components.count < 2 {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid channel definition from \(channelDef)"
)
}
guard let channelId = UInt64(components[1]) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid channel ID from \(channelDef)"
)
}
guard let handler = decoder.userInfo[channelDataKey] as? (UInt64, String) -> Void else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "missing userInfo for Channel handler. This is a Tauri issue"
)
}
self.id = channelId
self.handler = handler
}
func serialize(_ data: JsonValue) -> String {
do {
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
} catch {
return "\"\(error)\""
}
}
public func send(_ data: JsonObject) {
send(.dictionary(data))
}
public func send(_ data: JsonValue) {
handler(id, serialize(data))
}
public func send<T: Encodable>(_ data: T) throws {
let json = try JSONEncoder().encode(data)
handler(id, String(decoding: json, as: UTF8.self))
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
import UIKit
@objc public class Invoke: NSObject {
public let command: String
let callback: UInt64
let error: UInt64
let data: String
let sendResponse: (UInt64, String?) -> Void
let sendChannelData: (UInt64, String) -> Void
public init(
command: String, callback: UInt64, error: UInt64,
sendResponse: @escaping (UInt64, String?) -> Void,
sendChannelData: @escaping (UInt64, String) -> Void, data: String
) {
self.command = command
self.callback = callback
self.error = error
self.data = data
self.sendResponse = sendResponse
self.sendChannelData = sendChannelData
}
public func getRawArgs() -> String {
return self.data
}
public func getArgs() throws -> JSObject {
let jsonData = self.data.data(using: .utf8)!
let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
return JSTypes.coerceDictionaryToJSObject(
(data as! NSDictionary), formattingDatesAsStrings: true)!
}
public func parseArgs<T: Decodable>(_ type: T.Type) throws -> T {
let jsonData = self.data.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.userInfo[channelDataKey] = sendChannelData
return try decoder.decode(type, from: jsonData)
}
func serialize(_ data: JsonValue) -> String {
do {
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
} catch {
return "\"\(error)\""
}
}
public func resolve() {
sendResponse(callback, nil)
}
public func resolve(_ data: JsonObject) {
resolve(.dictionary(data))
}
public func resolve(_ data: JsonValue) {
sendResponse(callback, serialize(data))
}
public func resolve<T: Encodable>(_ data: T) {
do {
let json = try JSONEncoder().encode(data)
sendResponse(callback, String(decoding: json, as: UTF8.self))
} catch {
sendResponse(self.error, "\"\(error)\"")
}
}
public func reject(
_ message: String, code: String? = nil, error: Error? = nil, data: JsonValue? = nil
) {
let payload: NSMutableDictionary = [
"message": message
]
if let code = code {
payload["code"] = code
}
if let error = error {
payload["error"] = error
}
if let data = data {
switch data {
case .dictionary(let dict):
for entry in dict {
payload[entry.key] = entry.value
}
}
}
sendResponse(self.error, serialize(.dictionary(payload as! JsonObject)))
}
public func unimplemented() {
unimplemented("not implemented")
}
public func unimplemented(_ message: String) {
reject(message)
}
public func unavailable() {
unavailable("not available")
}
public func unavailable(_ message: String) {
reject(message)
}
}

View File

@@ -0,0 +1,123 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
// declare our empty protocol, and conformance, for typing
public protocol JSValue {}
extension String: JSValue {}
extension Bool: JSValue {}
extension Int: JSValue {}
extension Float: JSValue {}
extension Double: JSValue {}
extension NSNumber: JSValue {}
extension NSNull: JSValue {}
extension Array: JSValue {}
extension Date: JSValue {}
extension Dictionary: JSValue where Key == String, Value == JSValue {}
// convenience aliases
public typealias JSObject = [String: JSValue]
public typealias JSArray = [JSValue]
extension Dictionary where Key == String, Value == JSValue {
public func getValue(_ key: String) -> JSValue? {
return self[key]
}
public func getString(_ key: String) -> String? {
return self[key] as? String
}
public func getBool(_ key: String) -> Bool? {
return self[key] as? Bool
}
public func getInt(_ key: String) -> Int? {
return self[key] as? Int
}
public func getFloat(_ key: String) -> Float? {
if let floatValue = self[key] as? Float {
return floatValue
} else if let doubleValue = self[key] as? Double {
return Float(doubleValue)
}
return nil
}
public func getDouble(_ key: String) -> Double? {
return self[key] as? Double
}
public func getArray(_ key: String) -> JSArray? {
return self[key] as? JSArray
}
public func getObject(_ key: String) -> JSObject? {
return self[key] as? JSObject
}
}
/*
Simply casting objects from foundation class clusters (such as __NSArrayM)
doesn't work with the JSValue protocol and will always fail. So we need to
recursively and explicitly convert each value in the dictionary.
*/
public enum JSTypes {}
extension JSTypes {
public static func coerceDictionaryToJSObject(
_ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false
) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
public static func coerceDictionaryToJSObject(
_ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false
) -> JSObject? {
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
}
}
private let dateStringFormatter = ISO8601DateFormatter()
// We need a large switch statement because we have a lot of types.
// swiftlint:disable:next cyclomatic_complexity
private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? {
guard let value = value else {
return nil
}
switch value {
case let stringValue as String:
return stringValue
case let numberValue as NSNumber:
return numberValue
case let boolValue as Bool:
return boolValue
case let intValue as Int:
return intValue
case let floatValue as Float:
return floatValue
case let doubleValue as Double:
return doubleValue
case let dateValue as Date:
if formattingDates {
return dateStringFormatter.string(from: dateValue)
}
return dateValue
case let nullValue as NSNull:
return nullValue
case let arrayValue as NSArray:
return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) }
case let dictionaryValue as NSDictionary:
let keys = dictionaryValue.allKeys.compactMap { $0 as? String }
var result: JSObject = [:]
for key in keys {
result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates)
}
return result
default:
return nil
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
public typealias JsonObject = [String: Any?]
public enum JsonValue {
case dictionary(JsonObject)
enum SerializationError: Error {
case invalidObject
}
public func jsonRepresentation(includingFields: JsonObject? = nil) throws -> String? {
switch self {
case .dictionary(var dictionary):
if let fields = includingFields {
dictionary.merge(fields) { (current, _) in current }
}
dictionary = prepare(dictionary: dictionary)
guard JSONSerialization.isValidJSONObject(dictionary) else {
throw SerializationError.invalidObject
}
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return String(data: data, encoding: .utf8)
}
}
private static let formatter = ISO8601DateFormatter()
private func prepare(dictionary: JsonObject) -> JsonObject {
return dictionary.mapValues { (value) -> Any in
if let date = value as? Date {
return JsonValue.formatter.string(from: date)
} else if let aDictionary = value as? JsonObject {
return prepare(dictionary: aDictionary)
} else if let anArray = value as? [Any] {
return prepare(array: anArray)
}
return value
}
}
private func prepare(array: [Any]) -> [Any] {
return array.map { (value) -> Any in
if let date = value as? Date {
return JsonValue.formatter.string(from: date)
} else if let aDictionary = value as? JsonObject {
return prepare(dictionary: aDictionary)
} else if let anArray = value as? [Any] {
return prepare(array: anArray)
}
return value
}
}
}

View File

@@ -0,0 +1,139 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import os.log
import UIKit
import Foundation
class StdoutRedirector {
private var originalStdout: Int32 = -1
private var originalStderr: Int32 = -1
private var stdoutPipe: [Int32] = [-1, -1]
private var stderrPipe: [Int32] = [-1, -1]
private var stdoutReadSource: DispatchSourceRead?
private var stderrReadSource: DispatchSourceRead?
func start() {
originalStdout = dup(STDOUT_FILENO)
originalStderr = dup(STDERR_FILENO)
guard Darwin.pipe(&stdoutPipe) == 0,
Darwin.pipe(&stderrPipe) == 0 else {
Logger.error("Failed to create stdout/stderr pipes")
return
}
dup2(stdoutPipe[1], STDOUT_FILENO)
dup2(stderrPipe[1], STDERR_FILENO)
close(stdoutPipe[1])
close(stderrPipe[1])
stdoutReadSource = createReader(
readPipe: stdoutPipe[0],
writeToOriginal: originalStdout,
label: "stdout"
)
stderrReadSource = createReader(
readPipe: stderrPipe[0],
writeToOriginal: originalStderr,
label: "stderr"
)
}
private func createReader(
readPipe: Int32,
writeToOriginal: Int32,
label: String
) -> DispatchSourceRead {
let source = DispatchSource.makeReadSource(
fileDescriptor: readPipe,
queue: .global(qos: .utility)
)
source.setEventHandler {
let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
let bytesRead = read(readPipe, &buffer, bufferSize)
if bytesRead > 0 {
let output = String(
bytes: buffer[0..<bytesRead],
encoding: .utf8
) ?? ""
let trimmed = output.trimmingCharacters(in: .newlines)
if !trimmed.isEmpty {
// we're sending stderr to oslog, so we need to avoid recursive calls
if trimmed.hasPrefix("OSLOG-") {
// make sure the system can parse the oslogs
write(writeToOriginal, &buffer, bytesRead)
} else {
Logger.info("[\(label)] \(trimmed)")
}
}
}
}
source.setCancelHandler {
close(readPipe)
}
source.resume()
return source
}
}
/// Wrapper class for os_log function
public class Logger {
private static var _enabled = false
public static var enabled: Bool {
get {
#if DEBUG
return true
#else
return _enabled
#endif
}
set {
Logger._enabled = newValue
}
}
static func log(_ items: [Any], category: String, type: OSLogType) {
if Logger.enabled {
var message = ""
let last = items.count - 1
for (index, item) in items.enumerated() {
message += "\(item)"
if index != last {
message += " "
}
}
let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "-", category: category)
os_log("%{public}@", log: log, type: type, String(message.prefix(4068)))
}
}
public static func debug(_ items: Any..., category: String = "app") {
#if DEBUG
Logger.log(items, category: category, type: OSLogType.default)
#else
Logger.log(items, category: category, type: OSLogType.debug)
#endif
}
public static func info(_ items: Any..., category: String = "app") {
#if DEBUG
Logger.log(items, category: category, type: OSLogType.default)
#else
Logger.log(items, category: category, type: OSLogType.info)
#endif
}
public static func error(_ items: Any..., category: String = "app") {
Logger.log(items, category: category, type: OSLogType.error)
}
}

View File

@@ -0,0 +1,82 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import WebKit
import os.log
struct RegisterListenerArgs: Decodable {
let event: String
let handler: Channel
}
struct RemoveListenerArgs: Decodable {
let event: String
let channelId: UInt64
}
open class Plugin: NSObject {
public let manager: PluginManager = PluginManager.shared
var config: String = "{}"
private var listeners = [String: [Channel]]()
internal func setConfig(_ config: String) {
self.config = config
}
public func parseConfig<T: Decodable>(_ type: T.Type) throws -> T {
let jsonData = self.config.data(using: .utf8)!
let decoder = JSONDecoder()
return try decoder.decode(type, from: jsonData)
}
@objc open func load(webview: WKWebView) {}
@objc open func checkPermissions(_ invoke: Invoke) {
invoke.resolve()
}
@objc open func requestPermissions(_ invoke: Invoke) {
invoke.resolve()
}
public func trigger(_ event: String, data: JSObject) {
if let eventListeners = listeners[event] {
for channel in eventListeners {
channel.send(data)
}
}
}
public func trigger<T: Encodable>(_ event: String, data: T) throws {
if let eventListeners = listeners[event] {
for channel in eventListeners {
try channel.send(data)
}
}
}
@objc func registerListener(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(RegisterListenerArgs.self)
if var eventListeners = listeners[args.event] {
eventListeners.append(args.handler)
listeners[args.event] = eventListeners
} else {
listeners[args.event] = [args.handler]
}
invoke.resolve()
}
@objc func removeListener(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(RemoveListenerArgs.self)
if let eventListeners = listeners[args.event] {
listeners[args.event] = eventListeners.filter { $0.id != args.channelId }
}
invoke.resolve()
}
}

View File

@@ -0,0 +1,157 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import Foundation
import SwiftRs
import UIKit
import WebKit
import os.log
class PluginHandle {
var instance: Plugin
var loaded = false
init(plugin: Plugin) {
instance = plugin
}
}
public class PluginManager {
static let shared: PluginManager = PluginManager()
public var viewController: UIViewController?
var plugins: [String: PluginHandle] = [:]
var ipcDispatchQueue = DispatchQueue(label: "ipc")
public var isSimEnvironment: Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
public func assetUrl(fromLocalURL url: URL?) -> URL? {
guard let inputURL = url else {
return nil
}
return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
}
func onWebviewCreated(_ webview: WKWebView) {
for (_, handle) in plugins {
if !handle.loaded {
handle.instance.load(webview: webview)
}
}
}
func load<P: Plugin>(name: String, plugin: P, config: String, webview: WKWebView?) {
plugin.setConfig(config)
let handle = PluginHandle(plugin: plugin)
if let webview = webview {
handle.instance.load(webview: webview)
handle.loaded = true
}
plugins[name] = handle
}
func invoke(name: String, invoke: Invoke) {
if let plugin = plugins[name] {
ipcDispatchQueue.async {
let selectorWithCompletionHandler = Selector(("\(invoke.command):completionHandler:"))
let selectorWithThrows = Selector(("\(invoke.command):error:"))
if plugin.instance.responds(to: selectorWithCompletionHandler) {
let completion: @convention(block) (NSError?) -> Void = { error in
if let error = error {
invoke.reject("\(error)")
}
}
let blockObj: AnyObject = unsafeBitCast(completion, to: AnyObject.self)
let imp = plugin.instance.method(for: selectorWithCompletionHandler)
typealias Fn = @convention(c) (AnyObject, Selector, Invoke, AnyObject) -> Void
let fn = unsafeBitCast(imp, to: Fn.self)
fn(plugin.instance, selectorWithCompletionHandler, invoke, blockObj)
} else if plugin.instance.responds(to: selectorWithThrows) {
var error: NSError? = nil
withUnsafeMutablePointer(to: &error) {
let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
unsafeBitCast(
methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)(
plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
}
if let error = error {
invoke.reject("\(error)")
// TODO: app crashes without this leak
let _ = Unmanaged.passRetained(error)
}
} else {
let selector = Selector(("\(invoke.command):"))
if plugin.instance.responds(to: selector) {
plugin.instance.perform(selector, with: invoke)
} else {
invoke.reject("No command \(invoke.command) found for plugin \(name)")
}
}
}
} else {
invoke.reject("Plugin \(name) not initialized")
}
}
}
extension PluginManager: NSCopying {
public func copy(with zone: NSZone? = nil) -> Any {
return self
}
}
private var stdoutRedirector: StdoutRedirector?
@_cdecl("log_stdout")
func logStdout() {
stdoutRedirector = StdoutRedirector()
stdoutRedirector!.start()
}
@_cdecl("register_plugin")
func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
PluginManager.shared.load(
name: name.toString(),
plugin: plugin as! Plugin,
config: config.toString(),
webview: webview
)
}
@_cdecl("on_webview_created")
func onWebviewCreated(webview: WKWebView, viewController: UIViewController) {
PluginManager.shared.viewController = viewController
PluginManager.shared.onWebviewCreated(webview)
}
@_cdecl("run_plugin_command")
func runCommand(
id: Int,
name: SRString,
command: SRString,
data: SRString,
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>) -> Void,
sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer<CChar>) -> Void
) {
let callbackId: UInt64 = 0
let errorId: UInt64 = 1
let invoke = Invoke(
command: command.toString(), callback: callbackId, error: errorId,
sendResponse: { (fn: UInt64, payload: String?) -> Void in
let success = fn == callbackId
callback(id, success, payload ?? "null")
},
sendChannelData: { (id: UInt64, payload: String) -> Void in
sendChannelData(id, payload)
}, data: data.toString())
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
}

View File

@@ -0,0 +1,15 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import UIKit
public class UIUtils {
public static func centerPopover(rootViewController: UIViewController?, popoverController: UIViewController) {
if let viewController = rootViewController {
popoverController.popoverPresentationController?.sourceRect = CGRect(x: viewController.view.center.x, y: viewController.view.center.y, width: 0, height: 0)
popoverController.popoverPresentationController?.sourceView = viewController.view
popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.up
}
}
}