This commit is contained in:
Gellert Hegyi 2024-11-03 09:05:52 +01:00
commit 07b34e744a
11 changed files with 2211 additions and 0 deletions

40
.github/workflows/macos-build.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: electron-drag-click
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'npm'
- name: Install Xcode Command Line Tools
run: |
xcode-select --install || true
- name: Install dependencies
run: npm ci
- name: Verify native module build
run: |
npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: native-module-20.x
path: |
build/
*.node
retention-days: 7

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
*.log
build
.DS_Store

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Gellert Hegyi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

38
README.md Executable file
View file

@ -0,0 +1,38 @@
# electron-drag-click
## Description
```js
$ npm i electron-drag-click
```
This Native Node Module allows you to change the behavior of how frameless
Electron browser windows handle pointer events on macOS. Chromium's built-in
mechanism ignores pointer events in draggable regions in frameless windows.
This module changes the built-in hit testing so in frameless windows pointer
events are propagated even in draggable regions.
It is based on my earlier PR in the Electron repository (https://github.com/electron/electron/pull/38208), which after some discussion with maintainers was decided not to be
merged in, and rather be handled in a separate Native Module.
The code is using ObjectiveC's runtime method swizzling capability, which allows
you to alter the implementation of an existing selector. Shoutout to [@tzahola](https://github.com/tzahola), who helped me dealing with these APIs.
## Usage
``` typescript
const { app, BrowserWindow } = require('electron');
const electronDragClick = require('electron-drag-click');
electronDragClick();
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
frame: false,
});
win.loadFile('./index.html');
});
```

24
binding.gyp Normal file
View file

@ -0,0 +1,24 @@
{
"targets": [{
"target_name": "electron_drag_click",
"sources": [ ],
"conditions": [
['OS=="mac"', {
"sources": [
"electron_drag_click.mm"
],
}]
],
'include_dirs': [
"<!@(node -p \"require('node-addon-api').include\")"
],
'libraries': [],
'dependencies': [
"<!(node -p \"require('node-addon-api').gyp\")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
"xcode_settings": {
"OTHER_CPLUSPLUSFLAGS": ["-std=c++20", "-stdlib=libc++", "-mmacosx-version-min=10.12"],
}
}]
}

78
electron_drag_click.mm Normal file
View file

@ -0,0 +1,78 @@
#import <Cocoa/Cocoa.h>
#include <Foundation/Foundation.h>
#import <objc/runtime.h>
#include <napi.h>
static IMP g_originalHitTest;
static IMP g_originalMouseEvent;
static char kAssociatedObjectKey;
NSView* viewUnderneathPoint(NSView* self, NSPoint point) {
NSView *contentView = self.window.contentView;
NSArray *views = [contentView subviews];
for (NSView *v in views) {
if (v != self) {
NSPoint pointInView = [v convertPoint:point fromView:nil];
if ([v hitTest:pointInView] && [v mouse:pointInView inRect:v.bounds]) {
return v;
}
}
}
return nil;
}
NSView* swizzledHitTest(id obj, SEL sel, NSPoint point) {
NSView* originalReturn =
((NSView*(*) (id, SEL, NSPoint))g_originalHitTest) (obj, sel, point);
NSNumber* isDraggable = @(originalReturn == nil);
objc_setAssociatedObject(obj,
&kAssociatedObjectKey,
isDraggable,
OBJC_ASSOCIATION_COPY_NONATOMIC);
NSView* viewUnderPoint = viewUnderneathPoint(obj, point);
return [viewUnderPoint hitTest:point];
}
void swizzledMouseEvent(id obj, SEL sel, NSEvent* theEvent) {
((void(*) (id, SEL, NSEvent*))g_originalMouseEvent) (obj, sel, theEvent);
NSView* view = obj;
NSNumber* isDragging = objc_getAssociatedObject(view.window.contentView,
&kAssociatedObjectKey);
if ([theEvent type] == NSEventTypeLeftMouseDown && isDragging.boolValue) {
NSView* self = obj;
[self.window performWindowDragWithEvent:theEvent];
}
}
void Setup(const Napi::CallbackInfo &info) {
auto hitTestMethod = class_getInstanceMethod(
NSClassFromString(@"BridgedContentView"),
NSSelectorFromString(@"hitTest:"));
g_originalHitTest = method_setImplementation(hitTestMethod,
(IMP)&swizzledHitTest);
auto mouseEventMethod = class_getInstanceMethod(
NSClassFromString(@"RenderWidgetHostViewCocoa"),
NSSelectorFromString(@"mouseEvent:"));
g_originalMouseEvent = method_setImplementation(mouseEventMethod,
(IMP)&swizzledMouseEvent);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "setup"),
Napi::Function::New(env, Setup));
return exports;
}
NODE_API_MODULE(electron_drag_click, Init)

3
index.js Executable file
View file

@ -0,0 +1,3 @@
const { setup } = require('bindings')('electron_drag_click.node');
module.exports = setup;

1898
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

42
package.json Normal file
View file

@ -0,0 +1,42 @@
{
"name": "electron-drag-click",
"version": "1.0.0",
"description": "A native module that propagates click events to draggable areas in Electron on macOS.",
"main": "index.js",
"scripts": {
"build": "node-gyp build",
"build:dev": "node-gyp build --debug",
"clean": "node-gyp clean",
"rebuild": "node-gyp rebuild",
"rebuild:dev": "node-gyp rebuild --debug",
"start": "electron ./test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/gerhardberger/electron-drag-click.git"
},
"keywords": [
"macos",
"node",
"electron",
"native",
"draggable"
],
"author": "Gellert Hegyi <gellihegyi@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/gerhardberger/electron-drag-click/issues"
},
"homepage": "https://github.com/gerhardberger/electron-drag-click#readme",
"devDependencies": {
"electron": "^33.0.2",
"node-gyp": "^9.0.0"
},
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^3.0.2"
},
"os": [
"darwin"
]
}

49
test/index.html Normal file
View file

@ -0,0 +1,49 @@
<html>
<head>
<style>
.drag-container {
-webkit-app-region: drag;
background-color: cadetblue;
padding: 40px;
margin: 40px;
width: 200px;
height: 200px;
}
</style>
</head>
<body>
<h1>draggability test</h1>
<div class="drag-container">
<h2>can be dragged</h2>
<span>0</span> <br>
<input type="text" placeholder="type..." />
<button id="foo">cannot be clicked</button>
</div>
<input type="text" placeholder="type..." />
<button id="bar">can be clicked</button>
<script>
window.onload = () => {
let counter = 0;
const foo = document.getElementById('foo');
const bar = document.getElementById('bar');
const span = document.querySelector('span');
const inc = () => {
span.innerText = ++counter
};
foo.addEventListener('click', inc);
bar.addEventListener('click', inc);
}
</script>
</body>
</html>

14
test/index.js Normal file
View file

@ -0,0 +1,14 @@
const { app, BrowserWindow } = require('electron');
const electronDragClick = require('../index');
electronDragClick();
app.on('ready', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
frame: false,
});
win.loadFile('./index.html');
});