fix(KAN-71): correct Apple navigation gestures and scroll bounds
CI/CD / preconditions (pull_request) Successful in 2s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m4s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m31s
CI/CD / test_and_check_app (pull_request) Successful in 6m44s
CI/CD / Publish immutable image (pull_request) Skipped
CI/CD / preconditions (pull_request) Successful in 2s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m4s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m31s
CI/CD / test_and_check_app (pull_request) Successful in 6m44s
CI/CD / Publish immutable image (pull_request) Skipped
This commit is contained in:
@@ -177,6 +177,7 @@ fn open_workspace(app: &AppHandle, input: &str) -> Result<(), &'static str> {
|
||||
.center()
|
||||
.visible(false)
|
||||
.data_store_identifier(server.profile)
|
||||
.initialization_script(include_str!("../../ui/workspace-navigation.js"))
|
||||
.on_navigation(move |destination| {
|
||||
if !allows_server_navigation(&server_url, destination) {
|
||||
open_external(destination);
|
||||
@@ -227,6 +228,15 @@ fn open_workspace(app: &AppHandle, input: &str) -> Result<(), &'static str> {
|
||||
})
|
||||
.build()
|
||||
.map_err(|_| "connectionError")?;
|
||||
#[cfg(target_os = "macos")]
|
||||
window
|
||||
.with_webview(|webview| unsafe {
|
||||
unsafe extern "C" {
|
||||
fn install_workspace_navigation(webview: *mut std::ffi::c_void);
|
||||
}
|
||||
install_workspace_navigation(webview.inner().cast());
|
||||
})
|
||||
.map_err(|_| "connectionError")?;
|
||||
let handle = app.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
// Keep AppKit's system swipe preferences and cancellation thresholds, but use
|
||||
// script history traversal so asynchronously created Inertia entries aren't skipped.
|
||||
@interface KanbanNavigation : NSObject
|
||||
@property(nonatomic, weak) WKWebView *webview;
|
||||
@property(nonatomic, strong) id monitor;
|
||||
@property(nonatomic) BOOL pending;
|
||||
@property(nonatomic) BOOL tracking;
|
||||
@property(nonatomic) BOOL blocked;
|
||||
@property(nonatomic) NSUInteger sequence;
|
||||
@end
|
||||
|
||||
@implementation KanbanNavigation
|
||||
- (BOOL)swipeTrackingEnabled {
|
||||
return NSEvent.isSwipeTrackingFromScrollEventsEnabled;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_monitor) [NSEvent removeMonitor:_monitor];
|
||||
}
|
||||
|
||||
- (NSEvent *)handleEvent:(NSEvent *)event {
|
||||
WKWebView *webview = self.webview;
|
||||
if (!webview || event.window != webview.window || !webview.window.isKeyWindow ||
|
||||
webview.hidden || ![self swipeTrackingEnabled]) return event;
|
||||
if (event.phase == NSEventPhaseBegan) {
|
||||
self.sequence++;
|
||||
self.blocked = NO;
|
||||
self.pending = NO;
|
||||
self.tracking = NO;
|
||||
}
|
||||
if (self.tracking) return nil;
|
||||
if (event.phase == NSEventPhaseEnded || event.phase == NSEventPhaseCancelled) {
|
||||
self.sequence++;
|
||||
self.pending = NO;
|
||||
return event;
|
||||
}
|
||||
if (self.blocked || self.pending || event.momentumPhase != NSEventPhaseNone ||
|
||||
!(event.phase & (NSEventPhaseBegan | NSEventPhaseChanged)) || !event.hasPreciseScrollingDeltas) return event;
|
||||
if (fabs(event.scrollingDeltaX) <= fabs(event.scrollingDeltaY)) {
|
||||
if (fabs(event.scrollingDeltaY) > 0) self.blocked = YES;
|
||||
return event;
|
||||
}
|
||||
NSPoint point = [webview convertPoint:event.locationInWindow fromView:nil];
|
||||
if (!NSPointInRect(point, webview.bounds)) return event;
|
||||
if (!webview.isFlipped) point.y = webview.bounds.size.height - point.y;
|
||||
NSString *script = [NSString stringWithFormat:@"window.__kanbanNavigation?.canSwipe(%f, %f) === true", point.x, point.y];
|
||||
self.pending = YES;
|
||||
NSUInteger sequence = self.sequence;
|
||||
NSURL *page = webview.URL;
|
||||
__weak KanbanNavigation *weakSelf = self;
|
||||
[webview evaluateJavaScript:script completionHandler:^(id allowed, NSError *error) {
|
||||
KanbanNavigation *owner = weakSelf;
|
||||
if (!owner || owner.sequence != sequence) return;
|
||||
owner.pending = NO;
|
||||
owner.blocked = YES;
|
||||
if (error || ![allowed isEqual:@YES] || ![webview.URL isEqual:page] ||
|
||||
(!webview.canGoBack && !webview.canGoForward)) return;
|
||||
owner.tracking = YES;
|
||||
[event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection | NSEventSwipeTrackingClampGestureAmount
|
||||
dampenAmountThresholdMin:webview.canGoForward ? -1 : 0
|
||||
max:webview.canGoBack ? 1 : 0
|
||||
usingHandler:^(CGFloat amount, NSEventPhase phase, BOOL complete, BOOL *stop) {
|
||||
KanbanNavigation *current = weakSelf;
|
||||
if (current.sequence != sequence) {
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (!current || !webview.window.isKeyWindow || ![webview.URL isEqual:page]) {
|
||||
current.tracking = NO;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (!complete) return;
|
||||
current.tracking = NO;
|
||||
if (phase == NSEventPhaseCancelled) return;
|
||||
NSInteger direction = amount >= 1 ? -1 : amount <= -1 ? 1 : 0;
|
||||
if ((direction == -1 && webview.canGoBack) || (direction == 1 && webview.canGoForward)) {
|
||||
NSString *navigate = [NSString stringWithFormat:@"if (%@) window.__kanbanNavigation.step(%ld)", script, (long)direction];
|
||||
[webview evaluateJavaScript:navigate completionHandler:nil];
|
||||
}
|
||||
}];
|
||||
}];
|
||||
return event;
|
||||
}
|
||||
@end
|
||||
|
||||
void install_workspace_navigation(void *pointer) {
|
||||
WKWebView *webview = (__bridge WKWebView *)pointer;
|
||||
webview.allowsBackForwardNavigationGestures = NO;
|
||||
KanbanNavigation *navigation = [KanbanNavigation new];
|
||||
navigation.webview = webview;
|
||||
__weak KanbanNavigation *weakNavigation = navigation;
|
||||
navigation.monitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel handler:^NSEvent *(NSEvent *event) {
|
||||
KanbanNavigation *owner = weakNavigation;
|
||||
return owner ? [owner handleEvent:event] : event;
|
||||
}];
|
||||
objc_setAssociatedObject(webview, @selector(handleEvent:), navigation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
Reference in New Issue
Block a user