chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
configurationForConnecting connectingSceneSession: UISceneSession,
|
||||
options: UIScene.ConnectionOptions
|
||||
) -> UISceneConfiguration {
|
||||
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,18 @@
|
||||
// Zig's std.debug stack-trace symbolication (pulled in by the embed lib's
|
||||
// panic path) references `_dyld_get_image_header_containing_address`, which
|
||||
// the iOS SDK marks __API_UNAVAILABLE(ios). Provide the documented
|
||||
// replacement (dladdr) under the old symbol so the static lib links; it
|
||||
// only runs while formatting a panic trace. Mirrors the same shim in the
|
||||
// mobile-canvas example's ObjC host (examples/mobile-canvas/ios/main.m).
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <stddef.h>
|
||||
|
||||
const struct mach_header *_dyld_get_image_header_containing_address(const void *address) {
|
||||
Dl_info info;
|
||||
if (dladdr(address, &info) != 0 && info.dli_fbase != NULL) {
|
||||
return (const struct mach_header *)info.dli_fbase;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
final class NativeSdkHostViewController: UIViewController {
|
||||
private let headerView = UIView()
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let statusLabel = UILabel()
|
||||
private let backButton = UIButton(type: .system)
|
||||
private let refreshButton = UIButton(type: .system)
|
||||
private let webView = WKWebView(frame: .zero)
|
||||
private var webViewBottomConstraint: NSLayoutConstraint?
|
||||
private var nativeApp: UnsafeMutableRawPointer?
|
||||
private var keyboardBottomInset: CGFloat = 0
|
||||
private var widgetAccessibilityElements: [UIAccessibilityElement] = []
|
||||
|
||||
private struct WidgetSemantics {
|
||||
let id: UInt64
|
||||
let parentId: UInt64
|
||||
let role: Int32
|
||||
let flags: UInt32
|
||||
let actions: UInt32
|
||||
let bounds: CGRect
|
||||
let value: Float?
|
||||
let label: String
|
||||
let text: String
|
||||
let textSelectionStart: Int
|
||||
let textSelectionEnd: Int
|
||||
let textCompositionStart: Int
|
||||
let textCompositionEnd: Int
|
||||
let gridRowIndex: Int
|
||||
let gridColumnIndex: Int
|
||||
let gridRowCount: Int
|
||||
let gridColumnCount: Int
|
||||
let listItemIndex: Int
|
||||
let listItemCount: Int
|
||||
let scrollOffset: Float
|
||||
let scrollViewportExtent: Float
|
||||
let scrollContentExtent: Float
|
||||
let hasScroll: Bool
|
||||
}
|
||||
|
||||
private struct WidgetTextGeometry {
|
||||
let id: UInt64
|
||||
let caretBounds: CGRect?
|
||||
let selectionBounds: CGRect?
|
||||
let selectionRectCount: Int
|
||||
let compositionBounds: CGRect?
|
||||
let compositionRectCount: Int
|
||||
}
|
||||
|
||||
private final class WidgetAccessibilityElement: UIAccessibilityElement {
|
||||
private weak var owner: NativeSdkHostViewController?
|
||||
private let node: WidgetSemantics
|
||||
|
||||
init(accessibilityContainer container: Any, owner: NativeSdkHostViewController, node: WidgetSemantics) {
|
||||
self.owner = owner
|
||||
self.node = node
|
||||
super.init(accessibilityContainer: container)
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
owner?.activateWidgetAccessibilityNode(node) ?? false
|
||||
}
|
||||
|
||||
override func accessibilityIncrement() {
|
||||
_ = owner?.incrementWidgetAccessibilityNode(node)
|
||||
}
|
||||
|
||||
override func accessibilityDecrement() {
|
||||
_ = owner?.decrementWidgetAccessibilityNode(node)
|
||||
}
|
||||
|
||||
override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
|
||||
switch direction {
|
||||
case .down, .right:
|
||||
return owner?.incrementWidgetAccessibilityNode(node) ?? false
|
||||
case .up, .left:
|
||||
return owner?.decrementWidgetAccessibilityNode(node) ?? false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
configureHeader()
|
||||
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(headerView)
|
||||
view.addSubview(webView)
|
||||
let webViewBottomConstraint = webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
self.webViewBottomConstraint = webViewBottomConstraint
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
headerView.heightAnchor.constraint(equalToConstant: 104),
|
||||
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
webView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
webViewBottomConstraint,
|
||||
])
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardFrameWillChange), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
|
||||
nativeApp = native_sdk_app_create()
|
||||
if let nativeApp {
|
||||
native_sdk_app_start(nativeApp)
|
||||
refreshWidgetAccessibility()
|
||||
}
|
||||
|
||||
webView.loadHTMLString(Self.html, baseURL: nil)
|
||||
}
|
||||
|
||||
func activateNativeApp() {
|
||||
guard let nativeApp else { return }
|
||||
native_sdk_app_activate(nativeApp)
|
||||
}
|
||||
|
||||
func deactivateNativeApp() {
|
||||
guard let nativeApp else { return }
|
||||
native_sdk_app_deactivate(nativeApp)
|
||||
}
|
||||
|
||||
private func configureHeader() {
|
||||
headerView.backgroundColor = .secondarySystemBackground
|
||||
|
||||
titleLabel.text = "Mobile Shell"
|
||||
titleLabel.font = .preferredFont(forTextStyle: .title2)
|
||||
titleLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
subtitleLabel.text = "Native header with WebView workspace"
|
||||
subtitleLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
subtitleLabel.textColor = .secondaryLabel
|
||||
subtitleLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
statusLabel.text = "System WebView"
|
||||
statusLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
statusLabel.textColor = .secondaryLabel
|
||||
statusLabel.backgroundColor = .tertiarySystemFill
|
||||
statusLabel.layer.cornerRadius = 11
|
||||
statusLabel.layer.masksToBounds = true
|
||||
statusLabel.textAlignment = .center
|
||||
|
||||
backButton.setTitle("Back", for: .normal)
|
||||
backButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||
backButton.addTarget(self, action: #selector(sendBackCommand), for: .touchUpInside)
|
||||
|
||||
refreshButton.setTitle("Refresh", for: .normal)
|
||||
refreshButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||
refreshButton.addTarget(self, action: #selector(sendRefreshCommand), for: .touchUpInside)
|
||||
|
||||
[titleLabel, subtitleLabel, statusLabel, backButton, refreshButton].forEach {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
headerView.addSubview($0)
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 20),
|
||||
titleLabel.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 18),
|
||||
statusLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -20),
|
||||
statusLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
|
||||
statusLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 112),
|
||||
statusLabel.heightAnchor.constraint(equalToConstant: 24),
|
||||
refreshButton.trailingAnchor.constraint(equalTo: statusLabel.trailingAnchor),
|
||||
refreshButton.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 8),
|
||||
backButton.trailingAnchor.constraint(equalTo: refreshButton.leadingAnchor, constant: -12),
|
||||
backButton.centerYAnchor.constraint(equalTo: refreshButton.centerYAnchor),
|
||||
subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
||||
subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: backButton.leadingAnchor, constant: -16),
|
||||
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6),
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func sendBackCommand() {
|
||||
dispatchNativeCommand("mobile.back")
|
||||
}
|
||||
|
||||
@objc private func sendRefreshCommand() {
|
||||
dispatchNativeCommand("mobile.refresh")
|
||||
}
|
||||
|
||||
private func dispatchNativeCommand(_ command: String) {
|
||||
guard let nativeApp else { return }
|
||||
command.withCString { pointer in
|
||||
native_sdk_app_command(nativeApp, pointer, UInt(command.utf8.count))
|
||||
}
|
||||
let count = native_sdk_app_last_command_count(nativeApp)
|
||||
let name = String(cString: native_sdk_app_last_command_name(nativeApp))
|
||||
statusLabel.text = "\(name) #\(count)"
|
||||
native_sdk_app_frame(nativeApp)
|
||||
refreshWidgetAccessibility()
|
||||
}
|
||||
|
||||
@objc private func keyboardFrameWillChange(_ notification: Notification) {
|
||||
guard let frameValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
|
||||
let keyboardFrame = view.convert(frameValue.cgRectValue, from: nil)
|
||||
let overlap = max(0, view.bounds.maxY - keyboardFrame.minY)
|
||||
keyboardBottomInset = overlap
|
||||
webViewBottomConstraint?.constant = -overlap
|
||||
animateKeyboardLayout(notification)
|
||||
sendViewportUpdate()
|
||||
}
|
||||
|
||||
@objc private func keyboardWillHide(_ notification: Notification) {
|
||||
keyboardBottomInset = 0
|
||||
webViewBottomConstraint?.constant = 0
|
||||
animateKeyboardLayout(notification)
|
||||
sendViewportUpdate()
|
||||
}
|
||||
|
||||
private func animateKeyboardLayout(_ notification: Notification) {
|
||||
let userInfo = notification.userInfo ?? [:]
|
||||
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
||||
let curve = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 0
|
||||
let options = UIView.AnimationOptions(rawValue: curve << 16)
|
||||
UIView.animate(withDuration: duration, delay: 0, options: options) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
sendViewportUpdate()
|
||||
}
|
||||
|
||||
private func sendViewportUpdate() {
|
||||
guard let nativeApp else { return }
|
||||
let scale = Float(view.window?.screen.scale ?? UIScreen.main.scale)
|
||||
let safe = view.safeAreaInsets
|
||||
native_sdk_app_viewport(
|
||||
nativeApp,
|
||||
Float(webView.bounds.width),
|
||||
Float(webView.bounds.height),
|
||||
scale,
|
||||
nil,
|
||||
Float(safe.top),
|
||||
Float(safe.right),
|
||||
Float(safe.bottom),
|
||||
Float(safe.left),
|
||||
0,
|
||||
0,
|
||||
Float(keyboardBottomInset),
|
||||
0
|
||||
)
|
||||
native_sdk_app_frame(nativeApp)
|
||||
refreshWidgetAccessibility()
|
||||
}
|
||||
|
||||
private func widgetSemanticsSnapshot() -> [WidgetSemantics] {
|
||||
guard let nativeApp else { return [] }
|
||||
let count = Int(native_sdk_app_widget_semantics_count(nativeApp))
|
||||
var nodes: [WidgetSemantics] = []
|
||||
nodes.reserveCapacity(count)
|
||||
for index in 0..<count {
|
||||
if let node = widgetSemantics(at: index) {
|
||||
nodes.append(node)
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
private func widgetSemantics(at index: Int) -> WidgetSemantics? {
|
||||
guard let nativeApp else { return nil }
|
||||
var node = native_sdk_widget_semantics_t()
|
||||
guard native_sdk_app_widget_semantics_at(nativeApp, UInt(index), &node) != 0 else { return nil }
|
||||
return widgetSemantics(from: node)
|
||||
}
|
||||
|
||||
private func widgetSemantics(id: UInt64) -> WidgetSemantics? {
|
||||
guard let nativeApp else { return nil }
|
||||
var node = native_sdk_widget_semantics_t()
|
||||
guard native_sdk_app_widget_semantics_by_id(nativeApp, id, &node) != 0 else { return nil }
|
||||
return widgetSemantics(from: node)
|
||||
}
|
||||
|
||||
private func widgetSemantics(from node: native_sdk_widget_semantics_t) -> WidgetSemantics {
|
||||
return WidgetSemantics(
|
||||
id: node.id,
|
||||
parentId: node.parent_id,
|
||||
role: Int32(node.role),
|
||||
flags: node.flags,
|
||||
actions: node.actions,
|
||||
bounds: CGRect(x: CGFloat(node.x), y: CGFloat(node.y), width: CGFloat(node.width), height: CGFloat(node.height)),
|
||||
value: node.has_value != 0 ? node.value : nil,
|
||||
label: Self.utf8String(node.label, length: node.label_len),
|
||||
text: Self.utf8String(node.text, length: node.text_len),
|
||||
textSelectionStart: Int(node.text_selection_start),
|
||||
textSelectionEnd: Int(node.text_selection_end),
|
||||
textCompositionStart: Int(node.text_composition_start),
|
||||
textCompositionEnd: Int(node.text_composition_end),
|
||||
gridRowIndex: Int(node.grid_row_index),
|
||||
gridColumnIndex: Int(node.grid_column_index),
|
||||
gridRowCount: Int(node.grid_row_count),
|
||||
gridColumnCount: Int(node.grid_column_count),
|
||||
listItemIndex: Int(node.list_item_index),
|
||||
listItemCount: Int(node.list_item_count),
|
||||
scrollOffset: node.scroll_offset,
|
||||
scrollViewportExtent: node.scroll_viewport_extent,
|
||||
scrollContentExtent: node.scroll_content_extent,
|
||||
hasScroll: node.has_scroll != 0
|
||||
)
|
||||
}
|
||||
|
||||
private func widgetTextGeometry(id: UInt64) -> WidgetTextGeometry? {
|
||||
guard let nativeApp else { return nil }
|
||||
var geometry = native_sdk_widget_text_geometry_t()
|
||||
guard native_sdk_app_widget_text_geometry(nativeApp, id, &geometry) != 0 else { return nil }
|
||||
return WidgetTextGeometry(
|
||||
id: id,
|
||||
caretBounds: geometry.has_caret_bounds != 0 ? CGRect(x: CGFloat(geometry.caret_x), y: CGFloat(geometry.caret_y), width: CGFloat(geometry.caret_width), height: CGFloat(geometry.caret_height)) : nil,
|
||||
selectionBounds: geometry.has_selection_bounds != 0 ? CGRect(x: CGFloat(geometry.selection_x), y: CGFloat(geometry.selection_y), width: CGFloat(geometry.selection_width), height: CGFloat(geometry.selection_height)) : nil,
|
||||
selectionRectCount: Int(geometry.selection_rect_count),
|
||||
compositionBounds: geometry.has_composition_bounds != 0 ? CGRect(x: CGFloat(geometry.composition_x), y: CGFloat(geometry.composition_y), width: CGFloat(geometry.composition_width), height: CGFloat(geometry.composition_height)) : nil,
|
||||
compositionRectCount: Int(geometry.composition_rect_count)
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func dispatchWidgetAction(
|
||||
id: UInt64,
|
||||
action: Int32,
|
||||
text: String? = nil,
|
||||
selectionAnchor: UInt = 0,
|
||||
selectionFocus: UInt = 0,
|
||||
hasSelection: Bool = false
|
||||
) -> Bool {
|
||||
guard let nativeApp else { return false }
|
||||
var request = native_sdk_widget_action_t()
|
||||
request.id = id
|
||||
request.action = action
|
||||
request.selection_anchor = selectionAnchor
|
||||
request.selection_focus = selectionFocus
|
||||
request.has_selection = hasSelection ? 1 : 0
|
||||
let ok: Int32
|
||||
if let text {
|
||||
ok = text.withCString { pointer in
|
||||
request.text = pointer
|
||||
request.text_len = UInt(text.utf8.count)
|
||||
return native_sdk_app_widget_action(nativeApp, &request)
|
||||
}
|
||||
} else {
|
||||
request.text = nil
|
||||
request.text_len = 0
|
||||
ok = native_sdk_app_widget_action(nativeApp, &request)
|
||||
}
|
||||
if ok != 0 {
|
||||
native_sdk_app_frame(nativeApp)
|
||||
refreshWidgetAccessibility()
|
||||
}
|
||||
return ok != 0
|
||||
}
|
||||
|
||||
private func refreshWidgetAccessibility() {
|
||||
let semantics = widgetSemanticsSnapshot()
|
||||
statusLabel.accessibilityValue = "Accessible items: \(semantics.count)"
|
||||
widgetAccessibilityElements = semantics.map { node in
|
||||
let element = WidgetAccessibilityElement(accessibilityContainer: webView, owner: self, node: node)
|
||||
element.accessibilityIdentifier = "native-sdk-widget-\(node.id)"
|
||||
element.accessibilityLabel = node.label.isEmpty ? node.text : node.label
|
||||
element.accessibilityValue = widgetAccessibilityValue(node)
|
||||
element.accessibilityFrameInContainerSpace = node.bounds
|
||||
element.accessibilityTraits = widgetAccessibilityTraits(node)
|
||||
return element
|
||||
}
|
||||
webView.accessibilityElements = widgetAccessibilityElements.isEmpty ? nil : widgetAccessibilityElements as [Any]
|
||||
}
|
||||
|
||||
private func widgetAccessibilityValue(_ node: WidgetSemantics) -> String? {
|
||||
var states: [String] = []
|
||||
if (node.flags & UInt32(NATIVE_SDK_WIDGET_FLAG_EXPANDED)) != 0 {
|
||||
states.append("Expanded")
|
||||
}
|
||||
if (node.flags & UInt32(NATIVE_SDK_WIDGET_FLAG_COLLAPSED)) != 0 {
|
||||
states.append("Collapsed")
|
||||
}
|
||||
if (node.flags & UInt32(NATIVE_SDK_WIDGET_FLAG_REQUIRED)) != 0 {
|
||||
states.append("Required")
|
||||
}
|
||||
if (node.flags & UInt32(NATIVE_SDK_WIDGET_FLAG_READ_ONLY)) != 0 {
|
||||
states.append("Read only")
|
||||
}
|
||||
if (node.flags & UInt32(NATIVE_SDK_WIDGET_FLAG_INVALID)) != 0 {
|
||||
states.append("Invalid")
|
||||
}
|
||||
if !states.isEmpty {
|
||||
return states.joined(separator: ", ")
|
||||
}
|
||||
if let value = node.value {
|
||||
switch node.role {
|
||||
case Int32(NATIVE_SDK_WIDGET_ROLE_CHECKBOX), Int32(NATIVE_SDK_WIDGET_ROLE_SWITCH):
|
||||
return value >= 0.5 ? "On" : "Off"
|
||||
case Int32(NATIVE_SDK_WIDGET_ROLE_SLIDER), Int32(NATIVE_SDK_WIDGET_ROLE_PROGRESSBAR):
|
||||
return "\(Int((value * 100).rounded()))%"
|
||||
default:
|
||||
return "\(value)"
|
||||
}
|
||||
}
|
||||
return node.text.isEmpty ? nil : node.text
|
||||
}
|
||||
|
||||
private func activateWidgetAccessibilityNode(_ node: WidgetSemantics) -> Bool {
|
||||
let current = widgetSemantics(id: node.id) ?? node
|
||||
if widgetSupportsAction(current, UInt32(NATIVE_SDK_WIDGET_ACTION_TOGGLE)) {
|
||||
return dispatchWidgetAction(id: current.id, action: Int32(NATIVE_SDK_WIDGET_ACTION_KIND_TOGGLE))
|
||||
}
|
||||
if widgetSupportsAction(current, UInt32(NATIVE_SDK_WIDGET_ACTION_PRESS)) {
|
||||
return dispatchWidgetAction(id: current.id, action: Int32(NATIVE_SDK_WIDGET_ACTION_KIND_PRESS))
|
||||
}
|
||||
if widgetSupportsAction(current, UInt32(NATIVE_SDK_WIDGET_ACTION_SELECT)) {
|
||||
return dispatchWidgetAction(id: current.id, action: Int32(NATIVE_SDK_WIDGET_ACTION_KIND_SELECT))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func incrementWidgetAccessibilityNode(_ node: WidgetSemantics) -> Bool {
|
||||
let current = widgetSemantics(id: node.id) ?? node
|
||||
guard widgetSupportsAction(current, UInt32(NATIVE_SDK_WIDGET_ACTION_INCREMENT)) else { return false }
|
||||
return dispatchWidgetAction(id: current.id, action: Int32(NATIVE_SDK_WIDGET_ACTION_KIND_INCREMENT))
|
||||
}
|
||||
|
||||
private func decrementWidgetAccessibilityNode(_ node: WidgetSemantics) -> Bool {
|
||||
let current = widgetSemantics(id: node.id) ?? node
|
||||
guard widgetSupportsAction(current, UInt32(NATIVE_SDK_WIDGET_ACTION_DECREMENT)) else { return false }
|
||||
return dispatchWidgetAction(id: current.id, action: Int32(NATIVE_SDK_WIDGET_ACTION_KIND_DECREMENT))
|
||||
}
|
||||
|
||||
private func widgetSupportsAction(_ node: WidgetSemantics, _ action: UInt32) -> Bool {
|
||||
return (node.actions & action) != 0
|
||||
}
|
||||
|
||||
private func widgetAccessibilityTraits(_ node: WidgetSemantics) -> UIAccessibilityTraits {
|
||||
var traits: UIAccessibilityTraits = []
|
||||
switch node.role {
|
||||
case Int32(NATIVE_SDK_WIDGET_ROLE_BUTTON), Int32(NATIVE_SDK_WIDGET_ROLE_MENUITEM):
|
||||
traits.insert(.button)
|
||||
case Int32(NATIVE_SDK_WIDGET_ROLE_CHECKBOX), Int32(NATIVE_SDK_WIDGET_ROLE_SWITCH), Int32(NATIVE_SDK_WIDGET_ROLE_TAB):
|
||||
traits.insert(.button)
|
||||
case Int32(NATIVE_SDK_WIDGET_ROLE_SLIDER):
|
||||
traits.insert(.adjustable)
|
||||
case Int32(NATIVE_SDK_WIDGET_ROLE_IMAGE):
|
||||
traits.insert(.image)
|
||||
case Int32(NATIVE_SDK_WIDGET_ROLE_TEXT), Int32(NATIVE_SDK_WIDGET_ROLE_PROGRESSBAR):
|
||||
traits.insert(.staticText)
|
||||
default:
|
||||
break
|
||||
}
|
||||
if (node.flags & UInt32(NATIVE_SDK_WIDGET_FLAG_SELECTED)) != 0 {
|
||||
traits.insert(.selected)
|
||||
}
|
||||
if (node.flags & UInt32(NATIVE_SDK_WIDGET_FLAG_DISABLED)) != 0 {
|
||||
traits.insert(.notEnabled)
|
||||
}
|
||||
return traits
|
||||
}
|
||||
|
||||
private static func utf8String(_ pointer: UnsafePointer<CChar>?, length: UInt) -> String {
|
||||
guard let pointer, length > 0 else { return "" }
|
||||
let bytes = UnsafeBufferPointer(start: UnsafeRawPointer(pointer).assumingMemoryBound(to: UInt8.self), count: Int(length))
|
||||
return String(decoding: bytes, as: UTF8.self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
guard let nativeApp else { return }
|
||||
native_sdk_app_stop(nativeApp)
|
||||
native_sdk_app_destroy(nativeApp)
|
||||
}
|
||||
|
||||
private static let html = """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<body style="margin:0;font-family:-apple-system,system-ui;background:#f7f8fa;color:#171717;">
|
||||
<main style="padding:28px 22px;display:grid;gap:16px;">
|
||||
<h1 style="margin:0;font-size:30px;letter-spacing:0;">Workspace</h1>
|
||||
<p style="margin:0;color:#5f6672;line-height:1.5;">This content is rendered by WKWebView while the header remains native UIKit.</p>
|
||||
<section style="display:grid;gap:10px;">
|
||||
<div style="padding:14px;border:1px solid #e1e5ea;border-radius:8px;background:white;">Inbox review</div>
|
||||
<div style="padding:14px;border:1px solid #e1e5ea;border-radius:8px;background:white;">Sync queue</div>
|
||||
<div style="padding:14px;border:1px solid #e1e5ea;border-radius:8px;background:white;">Offline cache</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import UIKit
|
||||
|
||||
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(
|
||||
_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions
|
||||
) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = NativeSdkHostViewController()
|
||||
window.makeKeyAndVisible()
|
||||
self.window = window
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
(window?.rootViewController as? NativeSdkHostViewController)?.activateNativeApp()
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
(window?.rootViewController as? NativeSdkHostViewController)?.deactivateNativeApp()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum {
|
||||
NATIVE_SDK_WIDGET_ROLE_NONE = 0,
|
||||
NATIVE_SDK_WIDGET_ROLE_GROUP = 1,
|
||||
NATIVE_SDK_WIDGET_ROLE_TEXT = 2,
|
||||
NATIVE_SDK_WIDGET_ROLE_IMAGE = 3,
|
||||
NATIVE_SDK_WIDGET_ROLE_BUTTON = 4,
|
||||
NATIVE_SDK_WIDGET_ROLE_TEXTBOX = 5,
|
||||
NATIVE_SDK_WIDGET_ROLE_TOOLTIP = 6,
|
||||
NATIVE_SDK_WIDGET_ROLE_DIALOG = 7,
|
||||
NATIVE_SDK_WIDGET_ROLE_MENU = 8,
|
||||
NATIVE_SDK_WIDGET_ROLE_MENUITEM = 9,
|
||||
NATIVE_SDK_WIDGET_ROLE_LIST = 10,
|
||||
NATIVE_SDK_WIDGET_ROLE_LISTITEM = 11,
|
||||
NATIVE_SDK_WIDGET_ROLE_ROW = 12,
|
||||
NATIVE_SDK_WIDGET_ROLE_GRID = 13,
|
||||
NATIVE_SDK_WIDGET_ROLE_GRIDCELL = 14,
|
||||
NATIVE_SDK_WIDGET_ROLE_TAB = 15,
|
||||
NATIVE_SDK_WIDGET_ROLE_CHECKBOX = 16,
|
||||
NATIVE_SDK_WIDGET_ROLE_SWITCH = 17,
|
||||
NATIVE_SDK_WIDGET_ROLE_SLIDER = 18,
|
||||
NATIVE_SDK_WIDGET_ROLE_PROGRESSBAR = 19,
|
||||
};
|
||||
|
||||
enum {
|
||||
NATIVE_SDK_WIDGET_FLAG_FOCUSED = 1u << 0,
|
||||
NATIVE_SDK_WIDGET_FLAG_HOVERED = 1u << 1,
|
||||
NATIVE_SDK_WIDGET_FLAG_PRESSED = 1u << 2,
|
||||
NATIVE_SDK_WIDGET_FLAG_SELECTED = 1u << 3,
|
||||
NATIVE_SDK_WIDGET_FLAG_DISABLED = 1u << 4,
|
||||
NATIVE_SDK_WIDGET_FLAG_FOCUSABLE = 1u << 5,
|
||||
NATIVE_SDK_WIDGET_FLAG_EXPANDED = 1u << 6,
|
||||
NATIVE_SDK_WIDGET_FLAG_COLLAPSED = 1u << 7,
|
||||
NATIVE_SDK_WIDGET_FLAG_REQUIRED = 1u << 8,
|
||||
NATIVE_SDK_WIDGET_FLAG_READ_ONLY = 1u << 9,
|
||||
NATIVE_SDK_WIDGET_FLAG_INVALID = 1u << 10,
|
||||
};
|
||||
|
||||
enum {
|
||||
NATIVE_SDK_WIDGET_ACTION_FOCUS = 1u << 0,
|
||||
NATIVE_SDK_WIDGET_ACTION_PRESS = 1u << 1,
|
||||
NATIVE_SDK_WIDGET_ACTION_TOGGLE = 1u << 2,
|
||||
NATIVE_SDK_WIDGET_ACTION_INCREMENT = 1u << 3,
|
||||
NATIVE_SDK_WIDGET_ACTION_DECREMENT = 1u << 4,
|
||||
NATIVE_SDK_WIDGET_ACTION_SET_TEXT = 1u << 5,
|
||||
NATIVE_SDK_WIDGET_ACTION_SET_SELECTION = 1u << 6,
|
||||
NATIVE_SDK_WIDGET_ACTION_SELECT = 1u << 7,
|
||||
NATIVE_SDK_WIDGET_ACTION_DRAG = 1u << 8,
|
||||
NATIVE_SDK_WIDGET_ACTION_DROP_FILES = 1u << 9,
|
||||
};
|
||||
|
||||
enum {
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_FOCUS = 0,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_PRESS = 1,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_TOGGLE = 2,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_INCREMENT = 3,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_DECREMENT = 4,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_SET_TEXT = 5,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_SET_SELECTION = 6,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_SET_COMPOSITION = 7,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_COMMIT_COMPOSITION = 8,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_CANCEL_COMPOSITION = 9,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_SELECT = 10,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_DRAG = 11,
|
||||
NATIVE_SDK_WIDGET_ACTION_KIND_DROP_FILES = 12,
|
||||
};
|
||||
enum {
|
||||
NATIVE_SDK_GPU_SURFACE_STATUS_UNAVAILABLE = 0,
|
||||
NATIVE_SDK_GPU_SURFACE_STATUS_INITIALIZING = 1,
|
||||
NATIVE_SDK_GPU_SURFACE_STATUS_READY = 2,
|
||||
NATIVE_SDK_GPU_SURFACE_STATUS_LOST = 3,
|
||||
};
|
||||
|
||||
typedef struct native_sdk_widget_semantics {
|
||||
uint64_t id;
|
||||
uint64_t parent_id;
|
||||
int role;
|
||||
uint32_t flags;
|
||||
uint32_t actions;
|
||||
float x;
|
||||
float y;
|
||||
float width;
|
||||
float height;
|
||||
float value;
|
||||
int has_value;
|
||||
const char *label;
|
||||
uintptr_t label_len;
|
||||
const char *text;
|
||||
uintptr_t text_len;
|
||||
const char *placeholder;
|
||||
uintptr_t placeholder_len;
|
||||
intptr_t text_selection_start;
|
||||
intptr_t text_selection_end;
|
||||
intptr_t text_composition_start;
|
||||
intptr_t text_composition_end;
|
||||
intptr_t grid_row_index;
|
||||
intptr_t grid_column_index;
|
||||
intptr_t grid_row_count;
|
||||
intptr_t grid_column_count;
|
||||
intptr_t list_item_index;
|
||||
intptr_t list_item_count;
|
||||
float scroll_offset;
|
||||
float scroll_viewport_extent;
|
||||
float scroll_content_extent;
|
||||
int has_scroll;
|
||||
} native_sdk_widget_semantics_t;
|
||||
|
||||
typedef struct native_sdk_widget_text_geometry {
|
||||
uint64_t id;
|
||||
int has_caret_bounds;
|
||||
float caret_x;
|
||||
float caret_y;
|
||||
float caret_width;
|
||||
float caret_height;
|
||||
int has_selection_bounds;
|
||||
float selection_x;
|
||||
float selection_y;
|
||||
float selection_width;
|
||||
float selection_height;
|
||||
uintptr_t selection_rect_count;
|
||||
int has_composition_bounds;
|
||||
float composition_x;
|
||||
float composition_y;
|
||||
float composition_width;
|
||||
float composition_height;
|
||||
uintptr_t composition_rect_count;
|
||||
} native_sdk_widget_text_geometry_t;
|
||||
|
||||
typedef struct native_sdk_widget_action {
|
||||
uint64_t id;
|
||||
int action;
|
||||
const char *text;
|
||||
uintptr_t text_len;
|
||||
uintptr_t selection_anchor;
|
||||
uintptr_t selection_focus;
|
||||
int has_selection;
|
||||
} native_sdk_widget_action_t;
|
||||
|
||||
typedef struct native_sdk_canvas_pixels {
|
||||
uintptr_t width;
|
||||
uintptr_t height;
|
||||
uintptr_t byte_len;
|
||||
} native_sdk_canvas_pixels_t;
|
||||
|
||||
// Result of native_sdk_app_render_pixels_damage: the surface dimensions
|
||||
// plus the damaged region the call wrote into the caller's RETAINED
|
||||
// buffer, in device pixels. damage_width == 0 (or damage_height == 0)
|
||||
// means nothing changed since the previous call: the buffer already
|
||||
// shows the current frame and the host skips its upload entirely.
|
||||
typedef struct native_sdk_canvas_pixels_damage {
|
||||
uintptr_t width;
|
||||
uintptr_t height;
|
||||
uintptr_t byte_len;
|
||||
uintptr_t damage_x;
|
||||
uintptr_t damage_y;
|
||||
uintptr_t damage_width;
|
||||
uintptr_t damage_height;
|
||||
// The retained-canvas revision the buffer now REFLECTS: gate re-renders
|
||||
// on canvas_revision != this value (a change whose frame has not
|
||||
// presented yet reports the OLD revision with empty damage - call again
|
||||
// next tick), never on your own last sighting of canvas_revision.
|
||||
uint64_t revision;
|
||||
} native_sdk_canvas_pixels_damage_t;
|
||||
|
||||
typedef struct native_sdk_text_input_state {
|
||||
int active;
|
||||
uint64_t widget_id;
|
||||
float x;
|
||||
float y;
|
||||
float width;
|
||||
float height;
|
||||
} native_sdk_text_input_state_t;
|
||||
|
||||
typedef struct native_sdk_viewport_state {
|
||||
float width;
|
||||
float height;
|
||||
float scale;
|
||||
int has_surface;
|
||||
float safe_top;
|
||||
float safe_right;
|
||||
float safe_bottom;
|
||||
float safe_left;
|
||||
float keyboard_top;
|
||||
float keyboard_right;
|
||||
float keyboard_bottom;
|
||||
float keyboard_left;
|
||||
float content_x;
|
||||
float content_y;
|
||||
float content_width;
|
||||
float content_height;
|
||||
} native_sdk_viewport_state_t;
|
||||
typedef struct native_sdk_gpu_frame_state {
|
||||
uint64_t surface_id;
|
||||
uint64_t window_id;
|
||||
float width;
|
||||
float height;
|
||||
float scale;
|
||||
uint64_t frame_index;
|
||||
uint64_t timestamp_ns;
|
||||
uint64_t frame_interval_ns;
|
||||
uint64_t input_timestamp_ns;
|
||||
uint64_t input_latency_ns;
|
||||
uint64_t input_latency_budget_ns;
|
||||
uintptr_t input_latency_budget_exceeded_count;
|
||||
int input_latency_budget_ok;
|
||||
uint64_t first_frame_latency_ns;
|
||||
uint64_t first_frame_latency_budget_ns;
|
||||
uintptr_t first_frame_latency_budget_exceeded_count;
|
||||
int first_frame_latency_budget_ok;
|
||||
int nonblank;
|
||||
uint32_t sample_color;
|
||||
int status;
|
||||
int vsync;
|
||||
uint64_t canvas_revision;
|
||||
uintptr_t canvas_command_count;
|
||||
int canvas_frame_requires_render;
|
||||
int canvas_frame_full_repaint;
|
||||
uintptr_t canvas_frame_batch_count;
|
||||
uintptr_t canvas_frame_budget_exceeded_count;
|
||||
int canvas_frame_budget_ok;
|
||||
uint64_t widget_revision;
|
||||
uintptr_t widget_node_count;
|
||||
uintptr_t widget_semantics_count;
|
||||
} native_sdk_gpu_frame_state_t;
|
||||
|
||||
// Form-factor ordinals accepted by native_sdk_app_set_form_factor.
|
||||
enum {
|
||||
NATIVE_SDK_FORM_FACTOR_UNKNOWN = 0,
|
||||
NATIVE_SDK_FORM_FACTOR_COMPACT = 1,
|
||||
NATIVE_SDK_FORM_FACTOR_REGULAR = 2,
|
||||
};
|
||||
|
||||
// One declared platform-chrome tab (or the primary action) from the
|
||||
// app's shell metadata. Strings reference static app data (valid for
|
||||
// the app's lifetime, not NUL-terminated). Layout mirrors
|
||||
// src/embed/chrome.zig MobileChromeItem.
|
||||
typedef struct native_sdk_chrome_item {
|
||||
const char *id;
|
||||
uintptr_t id_len;
|
||||
const char *label;
|
||||
uintptr_t label_len;
|
||||
const char *icon;
|
||||
uintptr_t icon_len;
|
||||
} native_sdk_chrome_item_t;
|
||||
|
||||
void *native_sdk_app_create(void);
|
||||
void native_sdk_app_destroy(void *app);
|
||||
void native_sdk_app_start(void *app);
|
||||
void native_sdk_app_activate(void *app);
|
||||
void native_sdk_app_deactivate(void *app);
|
||||
void native_sdk_app_stop(void *app);
|
||||
void native_sdk_app_resize(void *app, float width, float height, float scale, void *surface);
|
||||
void native_sdk_app_viewport(void *app, float width, float height, float scale, void *surface, float safe_top, float safe_right, float safe_bottom, float safe_left, float keyboard_top, float keyboard_right, float keyboard_bottom, float keyboard_left);
|
||||
int native_sdk_app_viewport_state(void *app, native_sdk_viewport_state_t *out);
|
||||
int native_sdk_app_gpu_frame_state(void *app, native_sdk_gpu_frame_state_t *out);
|
||||
void native_sdk_app_touch(void *app, uint64_t id, int phase, float x, float y, float pressure);
|
||||
void native_sdk_app_scroll(void *app, uint64_t id, float x, float y, float delta_x, float delta_y);
|
||||
void native_sdk_app_key(void *app, int phase, const char *key, uintptr_t key_len, const char *text, uintptr_t text_len, uint32_t modifiers_mask);
|
||||
void native_sdk_app_text(void *app, const char *text, uintptr_t len);
|
||||
void native_sdk_app_ime(void *app, int kind, const char *text, uintptr_t len, intptr_t cursor);
|
||||
void native_sdk_app_command(void *app, const char *name, uintptr_t len);
|
||||
void native_sdk_app_frame(void *app);
|
||||
// Declared platform chrome (the app's shell-metadata tab set + optional
|
||||
// primary action): query the declaration once at startup, build REAL
|
||||
// native controls, poll the model-selected tab index each frame (-1 =
|
||||
// none), and dispatch taps back through native_sdk_app_command with the
|
||||
// declared ids. The icon rasterizer renders a declared icon-vocabulary
|
||||
// glyph as premultiplied white on transparent RGBA8 (size_px * size_px
|
||||
// * 4 bytes) — a template image the system control tints.
|
||||
uintptr_t native_sdk_app_chrome_tab_count(void *app);
|
||||
int native_sdk_app_chrome_tab_at(void *app, uintptr_t index, native_sdk_chrome_item_t *out);
|
||||
int native_sdk_app_chrome_primary_action(void *app, native_sdk_chrome_item_t *out);
|
||||
intptr_t native_sdk_app_chrome_selected_tab(void *app);
|
||||
// Model-driven navigation depth for platform push/pop transitions (0 =
|
||||
// the root page, 1 = one push in; -1 = the app declares no navigation
|
||||
// projection), polled each tick, and the declared back command a
|
||||
// completed platform back gesture dispatches through
|
||||
// native_sdk_app_command (1 with out->id filled when declared, 0
|
||||
// otherwise — never arm the interactive back gesture without it).
|
||||
intptr_t native_sdk_app_chrome_navigation_depth(void *app);
|
||||
int native_sdk_app_chrome_navigation_back_command(void *app, native_sdk_chrome_item_t *out);
|
||||
int native_sdk_app_chrome_icon_pixels(void *app, const char *name, uintptr_t name_len, uintptr_t size_px, uint8_t *pixels, uintptr_t pixels_len);
|
||||
// Host chrome reports on the window-chrome channel: the reported form
|
||||
// factor (host truth apps prefer over width derivation) and whether the
|
||||
// declared tabs are currently projected as native controls.
|
||||
int native_sdk_app_set_form_factor(void *app, int form_factor);
|
||||
int native_sdk_app_set_chrome_tabs_projected(void *app, int projected);
|
||||
void native_sdk_app_set_asset_root(void *app, const char *path, uintptr_t len);
|
||||
void native_sdk_app_set_asset_entry(void *app, const char *path, uintptr_t len);
|
||||
uintptr_t native_sdk_app_last_command_count(void *app);
|
||||
const char *native_sdk_app_last_command_name(void *app);
|
||||
const char *native_sdk_app_last_error_name(void *app);
|
||||
uintptr_t native_sdk_app_widget_semantics_count(void *app);
|
||||
int native_sdk_app_widget_semantics_at(void *app, uintptr_t index, native_sdk_widget_semantics_t *out);
|
||||
int native_sdk_app_widget_semantics_by_id(void *app, uint64_t id, native_sdk_widget_semantics_t *out);
|
||||
int native_sdk_app_widget_text_geometry(void *app, uint64_t id, native_sdk_widget_text_geometry_t *out);
|
||||
int native_sdk_app_widget_action(void *app, const native_sdk_widget_action_t *action);
|
||||
int native_sdk_app_text_input_state(void *app, native_sdk_text_input_state_t *out);
|
||||
// Platform text measurement for layout: returns the typographic width of a
|
||||
// single-line UTF-8 run at `size` for `font_id` (1 = sans, 2 = mono),
|
||||
// measured with the same font resolution presentation draws with. Return a
|
||||
// negative value to fall back to the deterministic estimator (e.g. invalid
|
||||
// UTF-8). Register before native_sdk_app_start; pass NULL to fall back to
|
||||
// the estimator on the next layout.
|
||||
typedef double (*native_sdk_text_measure_fn)(void *context, uint64_t font_id, double size, const char *text, uintptr_t text_len);
|
||||
int native_sdk_app_set_text_measure(void *app, native_sdk_text_measure_fn measure, void *context);
|
||||
int native_sdk_app_set_automation_dir(void *app, const char *path, uintptr_t len);
|
||||
int native_sdk_app_render_pixel_size(void *app, float scale, native_sdk_canvas_pixels_t *out);
|
||||
int native_sdk_app_render_pixels(void *app, float scale, uint8_t *pixels, uintptr_t pixels_len, native_sdk_canvas_pixels_t *out);
|
||||
// Incremental sibling of native_sdk_app_render_pixels for a host that
|
||||
// keeps `pixels` RETAINED across calls (one buffer, one consumer): the
|
||||
// fast path copies only the pixels changed since the previous call —
|
||||
// captured off the runtime's own dirty-scissored raster, no second
|
||||
// render — and reports that region; the first call (and any size or
|
||||
// scale change) fills the whole buffer with full damage.
|
||||
int native_sdk_app_render_pixels_damage(void *app, float scale, uint8_t *pixels, uintptr_t pixels_len, native_sdk_canvas_pixels_damage_t *out);
|
||||
Reference in New Issue
Block a user