diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 57f4ea6..a389d5b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -65,13 +65,13 @@ jobs: # Linux x86_64 - os: ubuntu-latest target: x86_64-unknown-linux-gnu - sys_deps: pkg-config libwebkit2gtk-4.1-dev libsoup-3.0-dev libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libgtk-3-dev + sys_deps: pkg-config libwebkit2gtk-4.1-dev libsoup-3.0-dev libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libgtk-3-dev libxdo-dev # Linux ARM64 - os: ubuntu-latest target: aarch64-unknown-linux-gnu cross_arch: arm64 - cross_sys_deps: libglib2.0-dev:arm64 libwebkit2gtk-4.1-dev:arm64 libsoup-3.0-dev:arm64 libcairo2-dev:arm64 libpango1.0-dev:arm64 libatk1.0-dev:arm64 libgdk-pixbuf2.0-dev:arm64 libgtk-3-dev:arm64 + cross_sys_deps: libglib2.0-dev:arm64 libwebkit2gtk-4.1-dev:arm64 libsoup-3.0-dev:arm64 libcairo2-dev:arm64 libpango1.0-dev:arm64 libatk1.0-dev:arm64 libgdk-pixbuf2.0-dev:arm64 libgtk-3-dev:arm64 libxdo-dev:arm64 cross_gcc: gcc-aarch64-linux-gnu cross_gpp: g++-aarch64-linux-gnu cross_triplet: aarch64-linux-gnu @@ -91,7 +91,7 @@ jobs: - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf cross_arch: armhf - cross_sys_deps: libglib2.0-dev:armhf libwebkit2gtk-4.1-dev:armhf libsoup-3.0-dev:armhf libcairo2-dev:armhf libpango1.0-dev:armhf libatk1.0-dev:armhf libgdk-pixbuf2.0-dev:armhf libgtk-3-dev:armhf + cross_sys_deps: libglib2.0-dev:armhf libwebkit2gtk-4.1-dev:armhf libsoup-3.0-dev:armhf libcairo2-dev:armhf libpango1.0-dev:armhf libatk1.0-dev:armhf libgdk-pixbuf2.0-dev:armhf libgtk-3-dev:armhf libxdo-dev:armhf cross_gcc: gcc-arm-linux-gnueabihf cross_gpp: g++-arm-linux-gnueabihf cross_triplet: arm-linux-gnueabihf diff --git a/Cargo.toml b/Cargo.toml index 7986885..7f120d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,9 @@ napi-derive = "3.5.1" tao = { version = "0.34.5", features = ["rwh_06"] } wry = { version = "0.53.5", features = ["devtools", "fullscreen"] } +[target.'cfg(not(target_os = "android"))'.dependencies] +muda = { version = "0.17", features = ["libxdo"] } + [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18" glib = "0.18" diff --git a/README.md b/README.md index 2ac6fd1..dd8a0eb 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,20 @@ npm install @webviewjs/webview # Supported platforms -| Platform | Supported | -| ----------------------- | --------- | -| x86_64-apple-darwin | ✅ | -| x86_64-pc-windows-msvc | ✅ | -| i686-pc-windows-msvc | ✅ | -| aarch64-apple-darwin | ✅ | -| aarch64-linux-android | ✅ | -| armv7-linux-androideabi | ✅ | -| aarch64-pc-windows-msvc | ✅ | +| Platform | Supported | +| ---------------------------- | --------- | +| x86_64-pc-windows-msvc | ✅ | +| i686-pc-windows-msvc | ✅ | +| aarch64-pc-windows-msvc | ✅ | +| x86_64-apple-darwin | ✅ | +| aarch64-apple-darwin | ✅ | +| x86_64-unknown-linux-gnu | ✅ | +| i686-unknown-linux-gnu | ✅ | +| aarch64-unknown-linux-gnu | ✅ | +| armv7-unknown-linux-gnueabihf| ✅ | +| aarch64-linux-android | ✅ | +| armv7-linux-androideabi | ✅ | +| x86_64-unknown-freebsd | ✅ | # Examples @@ -45,6 +50,129 @@ webview.loadUrl('https://nodejs.org'); app.run(); ``` +## Menu System + +WebviewJS provides a cross-platform menu system that works on macOS, Windows, and Linux. + +### Basic Menu Setup + +```js +import { Application, initMenuSystem } from '@webviewjs/webview'; + +// Initialize menu system (recommended, especially for macOS) +initMenuSystem(); + +const app = new Application(); + +// Set global application menu +app.setMenu({ + items: [ + { + label: "File", + submenu: { + items: [ + { id: "new", label: "New", accelerator: "CmdOrCtrl+N" }, + { id: "open", label: "Open", accelerator: "CmdOrCtrl+O" }, + { role: "separator" }, + { id: "quit", label: "Quit", accelerator: "CmdOrCtrl+Q" } + ] + } + }, + { + label: "Edit", + submenu: { + items: [ + { role: "copy" }, + { role: "paste" }, + { role: "cut" }, + { role: "selectall" } + ] + } + } + ] +}); + +const window = app.createBrowserWindow(); +const webview = window.createWebview({ url: 'https://nodejs.org' }); + +app.run(); +``` + +### Menu Event Handling + +```js +import { Application, WebviewApplicationEvent } from '@webviewjs/webview'; + +const app = new Application(); + +// Handle menu events +app.bind((event) => { + if (event.event === WebviewApplicationEvent.CustomMenuClick) { + const menuEvent = event.customMenuEvent; + console.log(`Menu item clicked: ${menuEvent.id}`); + console.log(`From window: ${menuEvent.windowId}`); + + // Handle specific menu items + switch (menuEvent.id) { + case 'new': + console.log('Creating new document...'); + break; + case 'open': + console.log('Opening file...'); + break; + case 'quit': + app.exit(); + break; + } + } +}); + +// Set up menu... +app.setMenu({ /* ... */ }); +``` + +### Window-Specific Menus + +```js +const app = new Application(); + +// Create window with custom menu +const window = app.createBrowserWindow({ + title: "Custom Window", + menu: { + items: [ + { + id: "window-action", + label: "Window Action", + accelerator: "Ctrl+W" + } + ] + } +}); + +// Or check if window has a menu +if (window.hasMenu()) { + console.log('This window has a menu'); +} +``` + +### Menu Item Options + +- **`id`**: Unique identifier for the menu item (used in events) +- **`label`**: Display text for the menu item +- **`enabled`**: Whether the item is clickable (default: true) +- **`accelerator`**: Keyboard shortcut (e.g., "CmdOrCtrl+N", "Alt+F4") +- **`submenu`**: Nested menu items +- **`role`**: Predefined menu items with built-in behavior + +### Predefined Menu Roles + +- **`"copy"`**: Standard copy action +- **`"paste"`**: Standard paste action +- **`"cut"`**: Standard cut action +- **`"selectall"`**: Select all text action +- **`"separator"`**: Visual separator line + ## IPC ```js @@ -120,7 +248,15 @@ webview.reload(); For more details on closing applications and cleaning up resources, see the [Closing Guide](./docs/CLOSING_GUIDE.md). -Check out [examples](./examples) directory for more examples, such as serving contents from a web server to webview, etc. +Check out [examples](./examples) directory for more examples: + +- **[menu-system.mjs](./examples/menu-system.mjs)** - Comprehensive menu system demonstration with all features +- **[window-menus.mjs](./examples/window-menus.mjs)** - Window-specific vs global menu examples +- **[http/](./examples/http/)** - Serving content from a web server to webview +- **[transparent.mjs](./examples/transparent.mjs)** - Transparent window example +- **[close-example.mjs](./examples/close-example.mjs)** - Graceful application closing + +Run any example with: `node examples/menu-system.mjs` (after building the project) # Building executables @@ -141,7 +277,7 @@ You can pass `--resources ./my-resource.json` to include additional resources in - [Bun](https://bun.sh/) >= 1.3.0 - [Rust](https://www.rust-lang.org/) stable toolchain -- [Node.js](https://nodejs.org/) >= 18 (for testing) +- [Node.js](https://nodejs.org/) >= 24 (for testing) ## Setup diff --git a/examples/menu-system.mjs b/examples/menu-system.mjs new file mode 100644 index 0000000..52b7eb5 --- /dev/null +++ b/examples/menu-system.mjs @@ -0,0 +1,296 @@ +import { Application, initMenuSystem, WebviewApplicationEvent } from '../index.js'; + +// Initialize menu system (recommended, especially for macOS) +initMenuSystem(); + +const app = new Application(); + +// Set up menu event handler +app.bind((event) => { + if (event.event === WebviewApplicationEvent.CustomMenuClick) { + const menuEvent = event.customMenuEvent; + console.log(`Menu item clicked: "${menuEvent.id}" from window ${menuEvent.windowId}`); + + // Handle specific menu items + switch (menuEvent.id) { + case 'new': + console.log('📄 Creating new document...'); + break; + case 'open': + console.log('📂 Opening file...'); + break; + case 'save': + console.log('💾 Saving file...'); + break; + case 'about': + console.log('ℹ️ About this application...'); + break; + case 'preferences': + console.log('⚙️ Opening preferences...'); + break; + case 'quit': + console.log('👋 Goodbye!'); + app.exit(); + break; + case 'reload': + console.log('🔄 Reloading webview...'); + // In a real app, you would reload the webview here + break; + case 'devtools': + console.log('🔧 Opening developer tools...'); + // In a real app, you would toggle devtools here + break; + default: + console.log(`Unhandled menu item: ${menuEvent.id}`); + } + } else if (event.event === WebviewApplicationEvent.ApplicationCloseRequested) { + console.log('Application close requested'); + app.exit(); + } else if (event.event === WebviewApplicationEvent.WindowCloseRequested) { + console.log('Window close requested'); + } +}); + +// Set up comprehensive application menu +app.setMenu({ + items: [ + // File menu + { + label: "File", + submenu: { + items: [ + { + id: "new", + label: "New", + accelerator: "CmdOrCtrl+N", + enabled: true + }, + { + id: "open", + label: "Open...", + accelerator: "CmdOrCtrl+O" + }, + { role: "separator" }, + { + id: "save", + label: "Save", + accelerator: "CmdOrCtrl+S" + }, + { + id: "save-as", + label: "Save As...", + accelerator: "CmdOrCtrl+Shift+S" + }, + { role: "separator" }, + { + id: "quit", + label: "Quit", + accelerator: "CmdOrCtrl+Q" + } + ] + } + }, + + // Edit menu with predefined roles + { + label: "Edit", + submenu: { + items: [ + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectall" }, + { role: "separator" }, + { + id: "preferences", + label: "Preferences...", + accelerator: "CmdOrCtrl+," + } + ] + } + }, + + // View menu + { + label: "View", + submenu: { + items: [ + { + id: "reload", + label: "Reload", + accelerator: "CmdOrCtrl+R" + }, + { + id: "devtools", + label: "Developer Tools", + accelerator: "F12" + }, + { role: "separator" }, + { + label: "Zoom", + submenu: { + items: [ + { + id: "zoom-in", + label: "Zoom In", + accelerator: "CmdOrCtrl+Plus" + }, + { + id: "zoom-out", + label: "Zoom Out", + accelerator: "CmdOrCtrl+-" + }, + { + id: "zoom-reset", + label: "Actual Size", + accelerator: "CmdOrCtrl+0" + } + ] + } + } + ] + } + }, + + // Help menu + { + label: "Help", + submenu: { + items: [ + { + id: "about", + label: "About Menu Example" + }, + { + id: "docs", + label: "Documentation", + accelerator: "F1" + } + ] + } + } + ] +}); + +console.log('🎯 Menu System Example'); +console.log('📋 Try the following:'); +console.log(' • Click on menu items to see event handling'); +console.log(' • Use keyboard shortcuts (Ctrl+N, Ctrl+O, etc.)'); +console.log(' • Try copy/paste with Ctrl+C/Ctrl+V'); +console.log(' • Use Ctrl+Q or File > Quit to exit'); +console.log(''); + +// Create main window +const window = app.createBrowserWindow({ + title: "Menu System Example", + width: 800, + height: 600 +}); + +// Create webview with example content +const webview = window.createWebview({ + html: ` + + + Menu System Example + + + +
+

🎯 Menu System Example

+

This example demonstrates the cross-platform menu system in WebviewJS.

+ +

📋 Available Menus:

+ + + + + + + + + +

✨ Try It Out:

+

1. Click on the menu items above to trigger events

+

2. Use keyboard shortcuts for quick access

+

3. Test copy/paste with the text area below:

+ + + +

Check the console to see menu events being handled!

+ +

🌟 Features Demonstrated:

+ +
+ + ` +}); + +// Run the application +console.log('🚀 Starting application with menu system...\n'); +app.run(); \ No newline at end of file diff --git a/examples/window-menus.mjs b/examples/window-menus.mjs new file mode 100644 index 0000000..dabe29c --- /dev/null +++ b/examples/window-menus.mjs @@ -0,0 +1,247 @@ +import { Application, initMenuSystem, WebviewApplicationEvent } from '../index.js'; + +// Initialize menu system +initMenuSystem(); + +const app = new Application(); + +// Handle menu events +app.bind((event) => { + if (event.event === WebviewApplicationEvent.CustomMenuClick) { + const menuEvent = event.customMenuEvent; + console.log(`Menu "${menuEvent.id}" clicked on window ${menuEvent.windowId}`); + + switch (menuEvent.id) { + case 'close-window': + console.log('Closing window...'); + // In a real app, you would close the specific window + break; + case 'window-1-action': + console.log('Window 1 specific action!'); + break; + case 'window-2-action': + console.log('Window 2 specific action!'); + break; + case 'global-action': + console.log('Global action from any window!'); + break; + case 'quit': + console.log('Quitting application...'); + app.exit(); + break; + } + } +}); + +// Set a global application menu +app.setMenu({ + items: [ + { + label: "App", + submenu: { + items: [ + { id: "global-action", label: "Global Action", accelerator: "CmdOrCtrl+G" }, + { role: "separator" }, + { id: "quit", label: "Quit", accelerator: "CmdOrCtrl+Q" } + ] + } + } + ] +}); + +console.log('🪟 Window-Specific Menu Example'); +console.log('Creating two windows with different menus...\n'); + +// Create first window with custom menu +const window1 = app.createBrowserWindow({ + title: "Window 1 - Custom Menu", + width: 400, + height: 300, + x: 100, + y: 100, + menu: { + items: [ + { + label: "Window 1", + submenu: { + items: [ + { id: "window-1-action", label: "Window 1 Action", accelerator: "Ctrl+1" }, + { role: "separator" }, + { id: "close-window", label: "Close Window", accelerator: "Ctrl+W" } + ] + } + }, + { + label: "Edit", + submenu: { + items: [ + { role: "copy" }, + { role: "paste" } + ] + } + } + ] + } +}); + +window1.createWebview({ + html: ` + + + Window 1 + + + +
+

🪟 Window 1

+

Custom Menu:

+

• Window 1 → Window 1 Action (Ctrl+1)

+

• Window 1 → Close Window (Ctrl+W)

+

• Edit → Copy, Paste

+
+

This window has its own menu!

+

Try the menu items above 👆

+
+ + ` +}); + +// Create second window with different custom menu +const window2 = app.createBrowserWindow({ + title: "Window 2 - Different Menu", + width: 400, + height: 300, + x: 520, + y: 100, + menu: { + items: [ + { + label: "Window 2", + submenu: { + items: [ + { id: "window-2-action", label: "Window 2 Action", accelerator: "Ctrl+2" }, + { role: "separator" }, + { id: "close-window", label: "Close Window", accelerator: "Ctrl+W" } + ] + } + }, + { + label: "Tools", + submenu: { + items: [ + { role: "selectall" }, + { role: "separator" }, + { id: "tool-action", label: "Special Tool" } + ] + } + } + ] + } +}); + +window2.createWebview({ + html: ` + + + Window 2 + + + +
+

🪟 Window 2

+

Different Menu:

+

• Window 2 → Window 2 Action (Ctrl+2)

+

• Window 2 → Close Window (Ctrl+W)

+

• Tools → Select All, Special Tool

+
+

This window has a different menu!

+

Compare with Window 1 👈

+
+ + ` +}); + +// Create third window that uses the global menu (no custom menu specified) +const window3 = app.createBrowserWindow({ + title: "Window 3 - Global Menu", + width: 400, + height: 300, + x: 310, + y: 420, + show_menu: true // Uses global menu +}); + +window3.createWebview({ + html: ` + + + Window 3 + + + +
+

🌍 Window 3

+

Global Menu:

+

• App → Global Action (Ctrl+G)

+

• App → Quit (Ctrl+Q)

+
+

This window uses the global menu!

+

Set with app.setMenu()

+
+ + ` +}); + +// Log menu status for each window +console.log(`Window 1 has menu: ${window1.hasMenu()}`); +console.log(`Window 2 has menu: ${window2.hasMenu()}`); +console.log(`Window 3 has menu: ${window3.hasMenu()}`); + +console.log('\n🎯 Try different menu items in each window!'); +console.log('Notice how each window responds to different menu items.'); +console.log('Use Ctrl+Q to quit the application.\n'); + +// Run the application +app.run(); \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 36f04c1..9cbefce 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,12 +8,14 @@ export declare class Application { onEvent(handler?: ((arg: ApplicationEvent) => void) | undefined | null): void /** Alias for on_event() - binds an event handler callback. */ bind(handler?: ((arg: ApplicationEvent) => void) | undefined | null): void + /** Exits the application gracefully. This will trigger the close event and clean up resources. */ + exit(): void /** Creates a new browser window. */ createBrowserWindow(options?: BrowserWindowOptions | undefined | null): BrowserWindow /** Creates a new browser window as a child window. */ createChildBrowserWindow(options?: BrowserWindowOptions | undefined | null): BrowserWindow - /** Exits the application gracefully. This will trigger the close event and clean up resources. */ - exit(): void + /** Sets the global menu for the application (cross-platform) */ + setMenu(menuOptions?: MenuOptions | undefined | null): void /** Runs the application. This method will block the current thread. */ run(): void } @@ -53,6 +55,10 @@ export declare class BrowserWindow { setMinimizable(minimizable: boolean): void /** Sets resizable. */ setResizable(resizable: boolean): void + /** Gets the window ID. */ + id(): number + /** Gets whether the window has a menu. */ + hasMenu(): boolean /** Gets the window theme. */ get theme(): Theme /** Sets the window theme. */ @@ -94,6 +100,12 @@ export declare class BrowserWindow { get fullscreen(): FullscreenType | null /** Sets the window to fullscreen or back. */ setFullscreen(fullscreenType?: FullscreenType | undefined | null): void + /** + * Closes the window by hiding it. Note: This hides the window rather than closing it completely, + * as tao requires the event loop to handle window closing. Use this when you want to + * close a specific window (like a login window) and potentially reopen it later. + */ + close(): void /** Hides the window without destroying it. */ hide(): void /** Shows the window if it was hidden. */ @@ -132,11 +144,13 @@ export type JsWebview = Webview export interface ApplicationEvent { /** The event type. */ event: WebviewApplicationEvent + /** Custom menu event data */ + customMenuEvent?: CustomMenuEvent } /** Represents the options for creating an application. */ export interface ApplicationOptions { - /** The control flow of the application. Default is `Poll`. */ + /** The control flow of the application. Default is `Wait` (recommended for low CPU usage). */ controlFlow?: ControlFlow /** The waiting time in ms for the application (only applicable if control flow is set to `WaitUntil`). */ waitTime?: number @@ -145,6 +159,10 @@ export interface ApplicationOptions { } export interface BrowserWindowOptions { + /** The window menu */ + menu?: MenuOptions + /** Whether to show the menu bar */ + showMenu?: boolean /** Whether the window is resizable. Default is `true`. */ resizable?: boolean /** The window title. */ @@ -185,14 +203,23 @@ export interface BrowserWindowOptions { /** Represents the control flow of the application. */ export declare const enum ControlFlow { - /** The application will continue running. */ + /** The application will continuously poll for events (high CPU usage). */ Poll = 0, + /** The application will wait for events (recommended, low CPU usage). */ + Wait = 1, /** The application will wait until the specified time. */ - WaitUntil = 1, + WaitUntil = 2, /** The application will exit. */ - Exit = 2, + Exit = 3, /** The application will exit with the given exit code. */ - ExitWithCode = 3 + ExitWithCode = 4 +} + +export interface CustomMenuEvent { + /** The menu item identifier */ + id: string + /** The window identifier */ + windowId: number } export interface Dimensions { @@ -219,6 +246,9 @@ export interface HeaderData { value?: string } +/** Initialize menu system from worker thread (cross-platform) */ +export declare function initMenuSystem(): void + export interface IpcMessage { /** The body of the message. */ body: Buffer @@ -237,6 +267,21 @@ export interface JsProgressBar { progress?: number } +/** Represents menu item options from JavaScript */ +export interface MenuItemOptions { + id?: string + label?: string + enabled?: boolean + accelerator?: string + submenu?: MenuOptions + role?: string +} + +/** Represents menu options from JavaScript */ +export interface MenuOptions { + items: Array +} + export interface Monitor { /** The name of the monitor. */ name?: string @@ -292,7 +337,9 @@ export declare const enum WebviewApplicationEvent { /** Window close event. */ WindowCloseRequested = 0, /** Application close event. */ - ApplicationCloseRequested = 1 + ApplicationCloseRequested = 1, + /** Custom menu click event. */ + CustomMenuClick = 2 } export interface WebviewOptions { @@ -331,3 +378,13 @@ export interface WebviewOptions { /** Indicates whether horizontal swipe gestures trigger backward and forward page navigation. */ backForwardNavigationGestures?: boolean } + +/** Window commands that can be sent from JavaScript */ +export declare const enum WindowCommand { + /** Close the window */ + Close = 0, + /** Show the window */ + Show = 1, + /** Hide the window */ + Hide = 2 +} diff --git a/index.js b/index.js index a588951..c39c609 100644 --- a/index.js +++ b/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-android-arm64') const bindingPackageVersion = require('@webviewjs/webview-android-arm64/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-android-arm-eabi') const bindingPackageVersion = require('@webviewjs/webview-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-win32-x64-gnu') const bindingPackageVersion = require('@webviewjs/webview-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-win32-x64-msvc') const bindingPackageVersion = require('@webviewjs/webview-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-win32-ia32-msvc') const bindingPackageVersion = require('@webviewjs/webview-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-win32-arm64-msvc') const bindingPackageVersion = require('@webviewjs/webview-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-darwin-universal') const bindingPackageVersion = require('@webviewjs/webview-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-darwin-x64') const bindingPackageVersion = require('@webviewjs/webview-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-darwin-arm64') const bindingPackageVersion = require('@webviewjs/webview-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-freebsd-x64') const bindingPackageVersion = require('@webviewjs/webview-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-freebsd-arm64') const bindingPackageVersion = require('@webviewjs/webview-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-x64-musl') const bindingPackageVersion = require('@webviewjs/webview-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-x64-gnu') const bindingPackageVersion = require('@webviewjs/webview-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-arm64-musl') const bindingPackageVersion = require('@webviewjs/webview-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-arm64-gnu') const bindingPackageVersion = require('@webviewjs/webview-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-arm-musleabihf') const bindingPackageVersion = require('@webviewjs/webview-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-arm-gnueabihf') const bindingPackageVersion = require('@webviewjs/webview-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-loong64-musl') const bindingPackageVersion = require('@webviewjs/webview-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-loong64-gnu') const bindingPackageVersion = require('@webviewjs/webview-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-riscv64-musl') const bindingPackageVersion = require('@webviewjs/webview-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-riscv64-gnu') const bindingPackageVersion = require('@webviewjs/webview-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-ppc64-gnu') const bindingPackageVersion = require('@webviewjs/webview-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-linux-s390x-gnu') const bindingPackageVersion = require('@webviewjs/webview-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-openharmony-arm64') const bindingPackageVersion = require('@webviewjs/webview-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-openharmony-x64') const bindingPackageVersion = require('@webviewjs/webview-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('@webviewjs/webview-openharmony-arm') const bindingPackageVersion = require('@webviewjs/webview-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.1.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.1.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -584,7 +584,9 @@ module.exports.ControlFlow = nativeBinding.ControlFlow module.exports.JsControlFlow = nativeBinding.JsControlFlow module.exports.FullscreenType = nativeBinding.FullscreenType module.exports.getWebviewVersion = nativeBinding.getWebviewVersion +module.exports.initMenuSystem = nativeBinding.initMenuSystem module.exports.ProgressBarState = nativeBinding.ProgressBarState module.exports.JsProgressBarState = nativeBinding.JsProgressBarState module.exports.Theme = nativeBinding.Theme module.exports.WebviewApplicationEvent = nativeBinding.WebviewApplicationEvent +module.exports.WindowCommand = nativeBinding.WindowCommand diff --git a/src/browser_window.rs b/src/browser_window.rs index 64a6268..50da422 100644 --- a/src/browser_window.rs +++ b/src/browser_window.rs @@ -1,12 +1,20 @@ use napi::{Either, Env, Result}; use napi_derive::*; +use std::sync::{Arc, Mutex}; +use std::hash::{Hash, Hasher}; +use std::collections::hash_map::DefaultHasher; use tao::{ dpi::{LogicalPosition, PhysicalSize}, event_loop::EventLoop, - window::{Fullscreen, ProgressBarState, Window, WindowBuilder}, + window::{Fullscreen, ProgressBarState, Window, WindowBuilder, WindowId}, }; +#[cfg(not(target_os = "android"))] +use muda::Menu; use crate::webview::{JsWebview, Theme, WebviewOptions}; +use crate::MenuOptions; +#[cfg(not(target_os = "android"))] +use crate::create_menu_from_options; // #[cfg(target_os = "windows")] // use tao::platform::windows::IconExtWindows; @@ -81,6 +89,10 @@ pub struct JsProgressBar { #[napi(object)] pub struct BrowserWindowOptions { + /// The window menu + pub menu: Option, + /// Whether to show the menu bar + pub show_menu: Option, /// Whether the window is resizable. Default is `true`. pub resizable: Option, /// The window title. @@ -122,6 +134,8 @@ pub struct BrowserWindowOptions { impl Default for BrowserWindowOptions { fn default() -> Self { Self { + menu: None, + show_menu: Some(true), resizable: Some(true), title: Some("WebviewJS".to_owned()), width: Some(800.0), @@ -148,6 +162,9 @@ impl Default for BrowserWindowOptions { pub struct BrowserWindow { is_child_window: bool, window: Window, + window_id: u32, + #[cfg(not(target_os = "android"))] + window_menu: Option, } #[napi] @@ -156,6 +173,10 @@ impl BrowserWindow { event_loop: &EventLoop<()>, options: Option, child: bool, + #[cfg(not(target_os = "android"))] + global_menu: Arc>>, + #[cfg(target_os = "android")] + _global_menu: Arc>>, ) -> Result { let options = options.unwrap_or_default(); @@ -239,9 +260,78 @@ impl BrowserWindow { ) })?; + // Generate a window ID by hashing the WindowId + let mut hasher = DefaultHasher::new(); + window.id().hash(&mut hasher); + let window_id = hasher.finish() as u32; + + // Handle menu for this window + #[cfg(not(target_os = "android"))] + let window_menu = if let Some(menu_options) = options.menu { + // Create window-specific menu + let menu = create_menu_from_options(menu_options)?; + #[cfg(target_os = "windows")] + { + use tao::platform::windows::WindowExtWindows; + unsafe { + menu.init_for_hwnd(window.hwnd() as isize).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to set window menu: {}", e), + ) + })? + }; + } + #[cfg(target_os = "linux")] + { + use tao::platform::unix::WindowExtUnix; + menu.init_for_gtk_window(window.gtk_window(), window.default_vbox()).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to set window menu: {}", e), + ) + })?; + } + Some(menu) + } else if options.show_menu.unwrap_or(false) { + // Use global menu if available and show_menu is true + if let Ok(global_menu) = global_menu.lock() { + if let Some(_menu) = global_menu.as_ref() { + #[cfg(target_os = "windows")] + { + use tao::platform::windows::WindowExtWindows; + unsafe { + _menu.init_for_hwnd(window.hwnd() as isize).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to set global menu: {}", e), + ) + })? + }; + } + #[cfg(target_os = "linux")] + { + use tao::platform::unix::WindowExtUnix; + _menu.init_for_gtk_window(window.gtk_window(), window.default_vbox()).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to set global menu: {}", e), + ) + })?; + } + } + } + None + } else { + None + }; + Ok(Self { window, is_child_window: child, + window_id, + #[cfg(not(target_os = "android"))] + window_menu, }) } @@ -348,6 +438,26 @@ impl BrowserWindow { self.window.set_resizable(resizable); } + #[napi] + /// Gets the window ID. + pub fn id(&self) -> u32 { + self.window_id + } + + #[napi] + /// Gets whether the window has a menu. + pub fn has_menu(&self) -> bool { + #[cfg(not(target_os = "android"))] + { self.window_menu.is_some() } + #[cfg(target_os = "android")] + { false } + } + + /// Gets the tao window ID (for internal use). + pub fn tao_window_id(&self) -> WindowId { + self.window.id() + } + #[napi(getter)] /// Gets the window theme. pub fn get_theme(&self) -> Theme { diff --git a/src/lib.rs b/src/lib.rs index 7394ec9..33e1e03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use std::cell::RefCell; use std::rc::Rc; +use std::sync::{Arc, Mutex}; use browser_window::{BrowserWindow, BrowserWindowOptions}; use napi::bindgen_prelude::*; @@ -12,6 +13,12 @@ use tao::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; +use std::collections::HashMap; +#[cfg(not(target_os = "android"))] +use muda::{ + accelerator::Accelerator, + Menu, MenuItem, PredefinedMenuItem, Submenu, +}; pub mod browser_window; pub mod webview; @@ -34,6 +41,35 @@ pub enum WebviewApplicationEvent { WindowCloseRequested, /// Application close event. ApplicationCloseRequested, + /// Custom menu click event. + CustomMenuClick, +} + +#[napi(object)] +pub struct CustomMenuEvent { + /// The menu item identifier + pub id: String, + /// The window identifier + pub window_id: u32, +} + +#[napi(object)] +/// Represents menu item options from JavaScript +#[derive(Clone)] +pub struct MenuItemOptions { + pub id: Option, + pub label: Option, + pub enabled: Option, + pub accelerator: Option, + pub submenu: Option, + pub role: Option, // For predefined roles like copy, paste, etc +} + +#[napi(object)] +/// Represents menu options from JavaScript +#[derive(Clone)] +pub struct MenuOptions { + pub items: Vec, } #[napi(object)] @@ -70,8 +106,10 @@ pub fn get_webview_version() -> Result { #[napi(js_name = "ControlFlow")] /// Represents the control flow of the application. pub enum JsControlFlow { - /// The application will continue running. + /// The application will continuously poll for events (high CPU usage). Poll, + /// The application will wait for events (recommended, low CPU usage). + Wait, /// The application will wait until the specified time. WaitUntil, /// The application will exit. @@ -83,7 +121,7 @@ pub enum JsControlFlow { #[napi(object)] /// Represents the options for creating an application. pub struct ApplicationOptions { - /// The control flow of the application. Default is `Poll`. + /// The control flow of the application. Default is `Wait` (recommended for low CPU usage). pub control_flow: Option, /// The waiting time in ms for the application (only applicable if control flow is set to `WaitUntil`). pub wait_time: Option, @@ -96,6 +134,8 @@ pub struct ApplicationOptions { pub struct ApplicationEvent { /// The event type. pub event: WebviewApplicationEvent, + /// Custom menu event data + pub custom_menu_event: Option, } #[napi] @@ -111,6 +151,14 @@ pub struct Application { env: Env, /// Whether the application should exit should_exit: Rc>, + /// The global menu + #[cfg(not(target_os = "android"))] + global_menu: Arc>>, + /// Menu event receiver + #[cfg(not(target_os = "android"))] + menu_event_receiver: Arc>>, + /// Window ID mapping (using string for simplicity) + window_ids: Arc>>, } #[napi] @@ -123,13 +171,18 @@ impl Application { Ok(Self { event_loop: Some(event_loop), options: options.unwrap_or(ApplicationOptions { - control_flow: Some(JsControlFlow::Poll), + control_flow: Some(JsControlFlow::Wait), wait_time: None, exit_code: None, }), handler: Rc::new(RefCell::new(None::>)), env, should_exit: Rc::new(RefCell::new(false)), + #[cfg(not(target_os = "android"))] + global_menu: Arc::new(Mutex::new(None)), + #[cfg(not(target_os = "android"))] + menu_event_receiver: Arc::new(Mutex::new(None)), + window_ids: Arc::new(Mutex::new(HashMap::new())), }) } @@ -145,6 +198,12 @@ impl Application { *self.handler.borrow_mut() = handler; } + #[napi] + /// Exits the application gracefully. This will trigger the close event and clean up resources. + pub fn exit(&self) { + *self.should_exit.borrow_mut() = true; + } + #[napi] /// Creates a new browser window. pub fn create_browser_window( @@ -160,7 +219,27 @@ impl Application { )); } - let window = BrowserWindow::new(event_loop.unwrap(), options, false)?; + // Pass the global menu to the window if no custom menu is provided + let mut window_options = options.unwrap_or_default(); + #[cfg(not(target_os = "android"))] + if window_options.menu.is_none() { + if let Ok(global_menu) = self.global_menu.lock() { + if global_menu.as_ref().is_some() { + window_options.show_menu = Some(true); + } + } + } + + #[cfg(not(target_os = "android"))] + let window = BrowserWindow::new(event_loop.unwrap(), Some(window_options), false, self.global_menu.clone())?; + #[cfg(target_os = "android")] + let window = BrowserWindow::new(event_loop.unwrap(), Some(window_options), false, Arc::new(Mutex::new(None)))?; + // Store window ID for menu events + if let Ok(mut ids) = self.window_ids.lock() { + let window_id = window.id(); + let tao_id = window.tao_window_id(); + ids.insert(format!("{:?}", tao_id), window_id); + } Ok(window) } @@ -180,23 +259,53 @@ impl Application { )); } - let window = BrowserWindow::new(event_loop.unwrap(), options, true)?; + #[cfg(not(target_os = "android"))] + let window = BrowserWindow::new(event_loop.unwrap(), options, true, self.global_menu.clone())?; + #[cfg(target_os = "android")] + let window = BrowserWindow::new(event_loop.unwrap(), options, true, Arc::new(Mutex::new(None)))?; Ok(window) } #[napi] - /// Exits the application gracefully. This will trigger the close event and clean up resources. - pub fn exit(&self) { - *self.should_exit.borrow_mut() = true; + /// Sets the global menu for the application (cross-platform) + pub fn set_menu(&mut self, menu_options: Option) -> Result<()> { + #[cfg(not(target_os = "android"))] + { + if let Some(options) = menu_options { + let menu = create_menu_from_options(options)?; + + #[cfg(target_os = "macos")] + { + // On macOS, set as application menu + menu.init_for_nsapp(); + } + + // Set up menu event receiver + if let Ok(mut receiver) = self.menu_event_receiver.lock() { + *receiver = Some(muda::MenuEvent::receiver().clone()); + } + + // Store the menu for use with new windows on other platforms + *self.global_menu.lock().unwrap() = Some(menu); + } else { + *self.global_menu.lock().unwrap() = None; + *self.menu_event_receiver.lock().unwrap() = None; + } + } + #[cfg(target_os = "android")] + let _ = menu_options; + + Ok(()) } #[napi] /// Runs the application. This method will block the current thread. pub fn run(&mut self) -> Result<()> { let ctrl = match self.options.control_flow { - None => ControlFlow::Poll, + None => ControlFlow::Wait, Some(JsControlFlow::Poll) => ControlFlow::Poll, + Some(JsControlFlow::Wait) => ControlFlow::Wait, Some(JsControlFlow::WaitUntil) => { let wait_time = self.options.wait_time.unwrap_or(0); ControlFlow::WaitUntil( @@ -214,10 +323,46 @@ impl Application { let handler = self.handler.clone(); let env = self.env; let should_exit = self.should_exit.clone(); + #[cfg(not(target_os = "android"))] + let menu_event_receiver = self.menu_event_receiver.clone(); + let _window_ids = self.window_ids.clone(); event_loop.run(move |event, _, control_flow| { *control_flow = ctrl; + // Only check for menu events when we have actual events or when using Poll control flow + #[cfg(not(target_os = "android"))] + { + let should_check_menu = matches!(event, Event::WindowEvent { .. } | Event::MainEventsCleared | Event::NewEvents(_)) + || matches!(ctrl, ControlFlow::Poll); + + if should_check_menu { + // Check for menu events - batch process all available events + if let Ok(receiver) = menu_event_receiver.lock() { + if let Some(receiver) = receiver.as_ref() { + // Process all available menu events in one go to reduce overhead + while let Ok(menu_event) = receiver.try_recv() { + let callback = handler.borrow(); + if let Some(callback) = callback.as_ref() { + if let Ok(on_event) = callback.borrow_back(&env) { + // Get window ID for the menu event + let window_id = 0; // Menu events are global, window ID not directly available + + let _ = on_event.call(ApplicationEvent { + event: WebviewApplicationEvent::CustomMenuClick, + custom_menu_event: Some(CustomMenuEvent { + id: menu_event.id().0.clone(), + window_id, + }), + }); + } + } + } + } + } + } + } + // Check if exit was requested if *should_exit.borrow() { let callback = handler.borrow(); @@ -225,6 +370,7 @@ impl Application { if let Ok(on_exit) = callback.borrow_back(&env) { let _ = on_exit.call(ApplicationEvent { event: WebviewApplicationEvent::ApplicationCloseRequested, + custom_menu_event: None, }); } } @@ -242,6 +388,7 @@ impl Application { if let Ok(on_ipc_msg) = callback.borrow_back(&env) { let _ = on_ipc_msg.call(ApplicationEvent { event: WebviewApplicationEvent::WindowCloseRequested, + custom_menu_event: None, }); } } @@ -254,3 +401,167 @@ impl Application { Ok(()) } } + +#[napi] +/// Initialize menu system from worker thread (cross-platform) +pub fn init_menu_system() -> Result<()> { + #[cfg(target_os = "macos")] + { + // Initialize the menu system for macOS + // This can be called from a worker thread + muda::Menu::new().init_for_nsapp(); + } + Ok(()) +} + +/// Creates a menu from JavaScript options +#[cfg(not(target_os = "android"))] +pub fn create_menu_from_options(options: MenuOptions) -> Result { + let menu = Menu::new(); + + // -------- App Menu -------- + let app = Submenu::new("App", true); + + let about = PredefinedMenuItem::about(None, None); + let hide = PredefinedMenuItem::hide(None); + let hide_others = PredefinedMenuItem::hide_others(None); + let show_all = PredefinedMenuItem::show_all(None); + let quit = PredefinedMenuItem::quit(None); + + app.append_items(&[ + &about, + &PredefinedMenuItem::separator(), + &hide, + &hide_others, + &show_all, + &PredefinedMenuItem::separator(), + &quit, + ]) + .ok(); + + menu.append(&app).ok(); + + for item in options.items { + add_menu_item_to_menu(&menu, item)?; + } + + Ok(menu) +} + +/// Adds a menu item to a menu or submenu +#[cfg(not(target_os = "android"))] +fn add_menu_item_to_menu(menu: &Menu, item: MenuItemOptions) -> Result<()> { + if let Some(submenu_options) = item.submenu { + // Create submenu + let submenu = Submenu::new(&item.label.unwrap_or_default(), true); + for sub_item in submenu_options.items { + add_menu_item_to_submenu(&submenu, sub_item)?; + } + menu.append(&submenu).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to append submenu: {}", e), + ) + })?; + } else if let Some(role) = &item.role { + // Handle predefined menu items + let predefined_item = match role.as_str() { + "copy" => PredefinedMenuItem::copy(None), + "paste" => PredefinedMenuItem::paste(None), + "cut" => PredefinedMenuItem::cut(None), + "selectall" => PredefinedMenuItem::select_all(None), + "separator" => PredefinedMenuItem::separator(), + _ => { + return Err(napi::Error::new( + napi::Status::InvalidArg, + format!("Unknown menu role: {}", role), + )) + } + }; + menu.append(&predefined_item).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to append predefined item: {}", e), + ) + })?; + } else if item.id.is_some() || item.label.is_some() { + // Create custom menu item + let menu_item = MenuItem::with_id( + muda::MenuId(item.id.clone().unwrap_or_else(|| { + item.label.clone().unwrap_or("item".to_string()) + })), + &item.label.unwrap_or_default(), + item.enabled.unwrap_or(true), + item.accelerator + .as_ref() + .and_then(|acc| acc.parse::().ok()), + ); + menu.append(&menu_item).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to append menu item: {}", e), + ) + })?; + } + + Ok(()) +} + +/// Adds a menu item to a submenu +#[cfg(not(target_os = "android"))] +fn add_menu_item_to_submenu(submenu: &Submenu, item: MenuItemOptions) -> Result<()> { + if let Some(nested_submenu_options) = item.submenu { + // Create nested submenu + let nested_submenu = Submenu::new(&item.label.unwrap_or_default(), true); + for sub_item in nested_submenu_options.items { + add_menu_item_to_submenu(&nested_submenu, sub_item)?; + } + submenu.append(&nested_submenu).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to append nested submenu: {}", e), + ) + })?; + } else if let Some(role) = &item.role { + // Handle predefined menu items in submenu + let predefined_item = match role.as_str() { + "copy" => PredefinedMenuItem::copy(None), + "paste" => PredefinedMenuItem::paste(None), + "cut" => PredefinedMenuItem::cut(None), + "selectall" => PredefinedMenuItem::select_all(None), + "separator" => PredefinedMenuItem::separator(), + _ => { + return Err(napi::Error::new( + napi::Status::InvalidArg, + format!("Unknown menu role: {}", role), + )) + } + }; + submenu.append(&predefined_item).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to append predefined item to submenu: {}", e), + ) + })?; + } else if item.id.is_some() || item.label.is_some() { + // Create custom menu item in submenu + let menu_item = MenuItem::with_id( + muda::MenuId(item.id.clone().unwrap_or_else(|| { + item.label.clone().unwrap_or("item".to_string()) + })), + &item.label.unwrap_or_default(), + item.enabled.unwrap_or(true), + item.accelerator + .as_ref() + .and_then(|acc| acc.parse::().ok()), + ); + submenu.append(&menu_item).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to append menu item to submenu: {}", e), + ) + })?; + } + + Ok(()) +}