iOS: Fix canvas touch events and auto-hide status bubble

- Disable scroll on WKWebView to allow touch events to reach canvas
- Add WKNavigationDelegate to intercept clawdis:// deep links from canvas
- Wire up onDeepLink callback to handle taps on canvas buttons
- Auto-hide status bubble after 3 seconds
main
Peter Steinberger 2025-12-14 05:14:26 +00:00
parent 26bbddde8f
commit a48aebc78c
2 changed files with 52 additions and 0 deletions

View File

@ -35,6 +35,14 @@ final class NodeAppModel {
let enabled = UserDefaults.standard.bool(forKey: "voiceWake.enabled") let enabled = UserDefaults.standard.bool(forKey: "voiceWake.enabled")
self.voiceWake.setEnabled(enabled) self.voiceWake.setEnabled(enabled)
// Wire up deep links from canvas taps
self.screen.onDeepLink = { [weak self] url in
guard let self else { return }
Task { @MainActor in
await self.handleDeepLink(url: url)
}
}
} }
func setScenePhase(_ phase: ScenePhase) { func setScenePhase(_ phase: ScenePhase) {

View File

@ -7,14 +7,19 @@ import WebKit
@Observable @Observable
final class ScreenController { final class ScreenController {
let webView: WKWebView let webView: WKWebView
private let navigationDelegate: ScreenNavigationDelegate
var mode: ClawdisScreenMode = .canvas var mode: ClawdisScreenMode = .canvas
var urlString: String = "" var urlString: String = ""
var errorText: String? var errorText: String?
/// Callback invoked when a clawdis:// deep link is tapped in the canvas
var onDeepLink: ((URL) -> Void)?
init() { init() {
let config = WKWebViewConfiguration() let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent() config.websiteDataStore = .nonPersistent()
self.navigationDelegate = ScreenNavigationDelegate()
self.webView = WKWebView(frame: .zero, configuration: config) self.webView = WKWebView(frame: .zero, configuration: config)
self.webView.isOpaque = false self.webView.isOpaque = false
self.webView.backgroundColor = .clear self.webView.backgroundColor = .clear
@ -23,6 +28,11 @@ final class ScreenController {
self.webView.scrollView.contentInset = .zero self.webView.scrollView.contentInset = .zero
self.webView.scrollView.scrollIndicatorInsets = .zero self.webView.scrollView.scrollIndicatorInsets = .zero
self.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false self.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
// Disable scroll to allow touch events to pass through to canvas
self.webView.scrollView.isScrollEnabled = false
self.webView.scrollView.bounces = false
self.webView.navigationDelegate = self.navigationDelegate
self.navigationDelegate.controller = self
self.reload() self.reload()
} }
@ -195,6 +205,11 @@ final class ScreenController {
statusEl.style.display = 'grid'; statusEl.style.display = 'grid';
if (titleEl && typeof title === 'string') titleEl.textContent = title; if (titleEl && typeof title === 'string') titleEl.textContent = title;
if (subtitleEl && typeof subtitle === 'string') subtitleEl.textContent = subtitle; if (subtitleEl && typeof subtitle === 'string') subtitleEl.textContent = subtitle;
// Auto-hide after 3 seconds
clearTimeout(window.__statusTimeout);
window.__statusTimeout = setTimeout(() => {
statusEl.style.display = 'none';
}, 3000);
} }
}; };
})(); })();
@ -203,3 +218,32 @@ final class ScreenController {
</html> </html>
""" """
} }
// MARK: - Navigation Delegate
/// Handles navigation policy to intercept clawdis:// deep links from canvas
private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
weak var controller: ScreenController?
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// Intercept clawdis:// deep links
if url.scheme == "clawdis" {
decisionHandler(.cancel)
Task { @MainActor in
self.controller?.onDeepLink?(url)
}
return
}
decisionHandler(.allow)
}
}