diff --git a/Cargo.toml b/Cargo.toml index 4626962..4bd584b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,10 @@ version = "0.1.0" crate-type = ["cdylib"] [dependencies] -napi = "2" +napi = { version = "2", default-features = true, features = ["napi9"] } napi-derive = "2" tao = "0.30.2" -wry = { version = "0.45.0", features = ["devtools"] } +wry = { version = "0.45.0", features = ["devtools", "fullscreen"] } [build-dependencies] napi-build = "2" diff --git a/examples/html.js b/examples/html.js index c084c85..1f96706 100644 --- a/examples/html.js +++ b/examples/html.js @@ -1,7 +1,13 @@ -const requireScript = require('node:module').createRequire(__filename); -const { Application } = requireScript('../index.js'); +// const requireScript = require('node:module').createRequire(__filename); +// const { Application } = requireScript('../index.js'); +const { Application } = require('../index.js'); const app = new Application(); + +app.onIpcMessage((data) => { + console.log({ data }); +}); + const window = app.createBrowserWindow({ html: ` @@ -10,6 +16,12 @@ const window = app.createBrowserWindow({

Hello world!

+ + `, diff --git a/index.d.ts b/index.d.ts index 7140e0a..6f78279 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,6 +3,44 @@ /* auto-generated by NAPI-RS */ +export const enum FullscreenType { + /** Exclusive fullscreen. */ + Exclusive = 0, + /** Borderless fullscreen. */ + Borderless = 1 +} +export interface Dimensions { + /** The width of the size. */ + width: number + /** The height of the size. */ + height: number +} +export interface Position { + /** The x position. */ + x: number + /** The y position. */ + y: number +} +export interface VideoMode { + /** The size of the video mode. */ + size: Dimensions + /** The bit depth of the video mode. */ + bitDepth: number + /** The refresh rate of the video mode. */ + refreshRate: number +} +export interface Monitor { + /** The name of the monitor. */ + name?: string + /** The scale factor of the monitor. */ + scaleFactor: number + /** The size of the monitor. */ + size: Dimensions + /** The position of the monitor. */ + position: Position + /** The video modes of the monitor. */ + videoModes: Array +} export const enum JsProgressBarState { None = 0, Normal = 1, @@ -55,6 +93,32 @@ export interface BrowserWindowOptions { userAgent?: string /** The default theme. */ theme?: Theme + /** The preload script */ + preload?: string + /** Whether the window is zoomable via hotkeys or gestures. */ + hotkeysZoom?: boolean + /** Whether the clipboard access is enabled. */ + clipboard?: boolean + /** Whether the autoplay policy is enabled. */ + autoplay?: boolean + /** Indicates whether horizontal swipe gestures trigger backward and forward page navigation. */ + backForwardNavigationGestures?: boolean +} +export interface HeaderData { + key: string + value?: string +} +export interface IpcMessage { + /** The unique identifier of the window that sent the message. */ + windowId: number + /** The body of the message. */ + body: Array + /** The HTTP method of the message. */ + method: string + /** The headers of the message. */ + headers: Array + /** The URI of the message. */ + uri: string } /** Returns the version of the webview. */ export declare function getWebviewVersion(): string @@ -79,6 +143,14 @@ export interface ApplicationOptions { exitCode?: number } export declare class BrowserWindow { + /** The unique identifier of this window. */ + id(): number + /** Launch a print modal for this window's contents. */ + print(): void + /** Set webview zoom level. */ + zoom(scaleFacotr: number): void + /** Hides or shows the webview. */ + setWebviewVisibility(visible: boolean): void /** Whether the devtools is opened. */ isDevtoolsOpen(): boolean /** Opens the devtools. */ @@ -125,6 +197,8 @@ export declare class BrowserWindow { setTheme(theme: Theme): void /** Evaluates the given JavaScript code. */ evaluateScript(js: string): void + /** Evaluates the given JavaScript code with a callback. */ + evaluateScriptWithCallback(js: string, callback: (...args: any[]) => any): void /** Sets the window icon. */ setWindowIcon(icon: Array | string, width: number, height: number): void /** Removes the window icon. */ @@ -136,11 +210,39 @@ export declare class BrowserWindow { setVisible(visible: boolean): void /** Modifies the window's progress bar. */ setProgressBar(state: JsProgressBar): void + /** Maximizes the window. */ + setMaximized(value: boolean): void + /** Minimizes the window. */ + setMinimized(value: boolean): void + /** Bring the window to front and focus. */ + focus(): void + /** Get available monitors. */ + getAvailableMonitors(): Array + /** Get the current monitor. */ + getCurrentMonitor(): Monitor | null + /** Get the primary monitor. */ + getPrimaryMonitor(): Monitor | null + /** Get the monitor from the given point. */ + getMonitorFromPoint(x: number, y: number): Monitor | null + /** Prevents the window contents from being captured by other apps. */ + setContentProtection(enabled: boolean): void + /** Sets the window always on top. */ + setAlwaysOnTop(enabled: boolean): void + /** Sets always on bottom. */ + setAlwaysOnBottom(enabled: boolean): void + /** Turn window decorations on or off. */ + setDecorations(enabled: boolean): void + /** Gets the window's current fullscreen state. */ + get fullscreen(): FullscreenType | null + /** Sets the window to fullscreen or back. */ + setFullscreen(fullscreenType?: FullscreenType | undefined | null): void } /** Represents an application. */ export declare class Application { /** Creates a new application. */ constructor(options?: ApplicationOptions | undefined | null) + /** Sets the IPC handler callback. */ + onIpcMessage(handler?: (...args: any[]) => any | undefined | null): void /** Creates a new browser window. */ createBrowserWindow(options?: BrowserWindowOptions | undefined | null): BrowserWindow /** Creates a new browser window as a child window. */ diff --git a/index.js b/index.js index c175fa5..d5d5261 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,8 @@ /* auto-generated by NAPI-RS */ -const { existsSync, readFileSync } = require('node:fs') -const { join } = require('node:path') +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') const { platform, arch } = process @@ -116,7 +116,7 @@ switch (platform) { nativeBinding = require('@webviewjs/webview-darwin-universal') } break - } catch { } + } catch {} switch (arch) { case 'x64': localFileExisted = existsSync(join(__dirname, 'webview.darwin-x64.node')) @@ -310,8 +310,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { JsProgressBarState, Theme, BrowserWindow, getWebviewVersion, ControlFlow, Application } = nativeBinding +const { FullscreenType, JsProgressBarState, Theme, BrowserWindow, getWebviewVersion, ControlFlow, Application } = nativeBinding +module.exports.FullscreenType = FullscreenType module.exports.JsProgressBarState = JsProgressBarState module.exports.Theme = Theme module.exports.BrowserWindow = BrowserWindow diff --git a/src/browser_window.rs b/src/browser_window.rs index 2cf4f35..b6769d5 100644 --- a/src/browser_window.rs +++ b/src/browser_window.rs @@ -1,15 +1,67 @@ -use napi::{Either, Result}; +use napi::{ + bindgen_prelude::*, + threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}, + Either, Result, +}; use napi_derive::*; use tao::{ dpi::{LogicalPosition, LogicalSize, PhysicalSize}, event_loop::EventLoop, - window::{Icon, ProgressBarState, Window, WindowBuilder}, + window::{Fullscreen, Icon, ProgressBarState, Window, WindowBuilder}, }; -use wry::{Rect, WebView, WebViewBuilder}; +use wry::{http::Request, Rect, WebView, WebViewBuilder, WebViewBuilderExtWindows}; #[cfg(target_os = "windows")] use tao::platform::windows::IconExtWindows; +#[napi] +pub enum FullscreenType { + /// Exclusive fullscreen. + Exclusive, + /// Borderless fullscreen. + Borderless, +} + +#[napi(object)] +pub struct Dimensions { + /// The width of the size. + pub width: u32, + /// The height of the size. + pub height: u32, +} + +#[napi(object)] +pub struct Position { + /// The x position. + pub x: i32, + /// The y position. + pub y: i32, +} + +#[napi(object, js_name = "VideoMode")] +pub struct JsVideoMode { + /// The size of the video mode. + pub size: Dimensions, + /// The bit depth of the video mode. + pub bit_depth: u16, + /// The refresh rate of the video mode. + pub refresh_rate: u16, +} + +#[napi(object)] +pub struct Monitor { + /// The name of the monitor. + pub name: Option, + /// The scale factor of the monitor. + pub scale_factor: f64, + /// The size of the monitor. + pub size: Dimensions, + /// The position of the monitor. + pub position: Position, + /// The video modes of the monitor. + pub video_modes: Vec, +} + #[napi] pub enum JsProgressBarState { None, @@ -69,10 +121,21 @@ pub struct BrowserWindowOptions { pub user_agent: Option, /// The default theme. pub theme: Option, + /// The preload script + pub preload: Option, + /// Whether the window is zoomable via hotkeys or gestures. + pub hotkeys_zoom: Option, + /// Whether the clipboard access is enabled. + pub clipboard: Option, + /// Whether the autoplay policy is enabled. + pub autoplay: Option, + /// Indicates whether horizontal swipe gestures trigger backward and forward page navigation. + pub back_forward_navigation_gestures: Option, } #[napi] pub struct BrowserWindow { + id: u32, window: Window, webview: WebView, } @@ -82,7 +145,9 @@ impl BrowserWindow { pub fn new( event_loop: &EventLoop<()>, options: Option, + id: u32, child: bool, + ipc_handler: impl Fn(Request) + 'static, ) -> Result { let options = options.unwrap_or(BrowserWindowOptions { url: None, @@ -98,6 +163,11 @@ impl BrowserWindow { title: Some("WebviewJS".to_string()), user_agent: None, theme: None, + preload: None, + autoplay: None, + back_forward_navigation_gestures: None, + clipboard: None, + hotkeys_zoom: None, }); let mut window = WindowBuilder::new().with_resizable(options.resizable.unwrap_or(true)); @@ -131,6 +201,40 @@ impl BrowserWindow { }) .with_incognito(options.incognito.unwrap_or(false)); + if let Some(preload) = options.preload { + webview = webview.with_initialization_script(&preload); + } + + if let Some(transparent) = options.transparent { + webview = webview.with_transparent(transparent); + } + + if let Some(autoplay) = options.autoplay { + webview = webview.with_autoplay(autoplay); + } + + if let Some(clipboard) = options.clipboard { + webview = webview.with_clipboard(clipboard); + } + + if let Some(back_forward_navigation_gestures) = options.back_forward_navigation_gestures { + webview = webview.with_back_forward_navigation_gestures(back_forward_navigation_gestures); + } + + if let Some(hotkeys_zoom) = options.hotkeys_zoom { + webview = webview.with_hotkeys_zoom(hotkeys_zoom); + } + + if let Some(theme) = options.theme { + let theme = match theme { + JsTheme::Light => wry::Theme::Light, + JsTheme::Dark => wry::Theme::Dark, + _ => wry::Theme::Auto, + }; + + webview = webview.with_theme(theme) + } + if let Some(user_agent) = options.user_agent { webview = webview.with_user_agent(&user_agent); } @@ -143,6 +247,8 @@ impl BrowserWindow { webview = webview.with_url(&url); } + webview = webview.with_ipc_handler(ipc_handler); + let webview = webview.build().map_err(|e| { napi::Error::new( napi::Status::GenericFailure, @@ -150,7 +256,50 @@ impl BrowserWindow { ) })?; - Ok(Self { window, webview }) + Ok(Self { + window, + webview, + id, + }) + } + + #[napi] + /// The unique identifier of this window. + pub fn id(&self) -> u32 { + self.id + } + + #[napi] + /// Launch a print modal for this window's contents. + pub fn print(&self) -> Result<()> { + self.webview.print().map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to print: {}", e), + ) + }) + } + + #[napi] + /// Set webview zoom level. + pub fn zoom(&self, scale_facotr: f64) -> Result<()> { + self.webview.zoom(scale_facotr).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to zoom: {}", e), + ) + }) + } + + #[napi] + /// Hides or shows the webview. + pub fn set_webview_visibility(&self, visible: bool) -> Result<()> { + self.webview.set_visible(visible).map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to set webview visibility: {}", e), + ) + }) } #[napi] @@ -314,20 +463,33 @@ impl BrowserWindow { .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e))) } - // #[napi] - // /// Evaluates the given JavaScript code with a callback. - // pub fn evaluate_script_with_callback Result<()> + Send>( - // &self, - // js: String, - // cb: T, - // ) -> Result<()> { - // self - // .webview - // .evaluate_script_with_callback(&js, |val| { - // cb(val).unwrap_or(()); - // }) - // .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e))) - // } + #[napi] + /// Evaluates the given JavaScript code with a callback. + pub fn evaluate_script_with_callback(&self, js: String, callback: JsFunction) -> Result<()> { + let tsfn: ThreadsafeFunction = callback + .create_threadsafe_function( + 0, + |ctx: napi::threadsafe_function::ThreadSafeCallContext| { + ctx + .env + .create_string(&ctx.value.to_string()) + .map(|v| vec![v]) + }, + ) + .map_err(|e| { + napi::Error::new( + napi::Status::GenericFailure, + format!("Failed to create threadsafe function: {}", e), + ) + })?; + + self + .webview + .evaluate_script_with_callback(&js, move |val| { + tsfn.call(Ok(val), ThreadsafeFunctionCallMode::Blocking); + }) + .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!("{}", e))) + } #[napi] /// Sets the window icon. @@ -394,4 +556,205 @@ impl BrowserWindow { self.window.set_progress_bar(progress); } + + #[napi] + /// Maximizes the window. + pub fn set_maximized(&self, value: bool) { + self.window.set_maximized(value); + } + + #[napi] + /// Minimizes the window. + pub fn set_minimized(&self, value: bool) { + self.window.set_minimized(value); + } + + #[napi] + /// Bring the window to front and focus. + pub fn focus(&self) { + self.window.set_focus(); + } + + #[napi] + /// Get available monitors. + pub fn get_available_monitors(&self) -> Vec { + self + .window + .available_monitors() + .map(|m| Monitor { + name: m.name(), + scale_factor: m.scale_factor(), + size: Dimensions { + width: m.size().width, + height: m.size().height, + }, + position: Position { + x: m.position().x, + y: m.position().y, + }, + video_modes: m + .video_modes() + .map(|v| JsVideoMode { + size: Dimensions { + width: v.size().width, + height: v.size().height, + }, + bit_depth: v.bit_depth(), + refresh_rate: v.refresh_rate(), + }) + .collect(), + }) + .collect() + } + + #[napi] + /// Get the current monitor. + pub fn get_current_monitor(&self) -> Option { + match self.window.current_monitor() { + Some(monitor) => Some(Monitor { + name: monitor.name(), + scale_factor: monitor.scale_factor(), + size: Dimensions { + width: monitor.size().width, + height: monitor.size().height, + }, + position: Position { + x: monitor.position().x, + y: monitor.position().y, + }, + video_modes: monitor + .video_modes() + .map(|v| JsVideoMode { + size: Dimensions { + width: v.size().width, + height: v.size().height, + }, + bit_depth: v.bit_depth(), + refresh_rate: v.refresh_rate(), + }) + .collect(), + }), + _ => None, + } + } + + #[napi] + /// Get the primary monitor. + pub fn get_primary_monitor(&self) -> Option { + match self.window.primary_monitor() { + Some(monitor) => Some(Monitor { + name: monitor.name(), + scale_factor: monitor.scale_factor(), + size: Dimensions { + width: monitor.size().width, + height: monitor.size().height, + }, + position: Position { + x: monitor.position().x, + y: monitor.position().y, + }, + video_modes: monitor + .video_modes() + .map(|v| JsVideoMode { + size: Dimensions { + width: v.size().width, + height: v.size().height, + }, + bit_depth: v.bit_depth(), + refresh_rate: v.refresh_rate(), + }) + .collect(), + }), + _ => None, + } + } + + #[napi] + /// Get the monitor from the given point. + pub fn get_monitor_from_point(&self, x: f64, y: f64) -> Option { + match self.window.monitor_from_point(x, y) { + Some(monitor) => Some(Monitor { + name: monitor.name(), + scale_factor: monitor.scale_factor(), + size: Dimensions { + width: monitor.size().width, + height: monitor.size().height, + }, + position: Position { + x: monitor.position().x, + y: monitor.position().y, + }, + video_modes: monitor + .video_modes() + .map(|v| JsVideoMode { + size: Dimensions { + width: v.size().width, + height: v.size().height, + }, + bit_depth: v.bit_depth(), + refresh_rate: v.refresh_rate(), + }) + .collect(), + }), + _ => None, + } + } + + #[napi] + /// Prevents the window contents from being captured by other apps. + pub fn set_content_protection(&self, enabled: bool) { + self.window.set_content_protection(enabled); + } + + #[napi] + /// Sets the window always on top. + pub fn set_always_on_top(&self, enabled: bool) { + self.window.set_always_on_top(enabled); + } + + #[napi] + /// Sets always on bottom. + pub fn set_always_on_bottom(&self, enabled: bool) { + self.window.set_always_on_bottom(enabled); + } + + #[napi] + /// Turn window decorations on or off. + pub fn set_decorations(&self, enabled: bool) { + self.window.set_decorations(enabled); + } + + #[napi(getter)] + /// Gets the window's current fullscreen state. + pub fn get_fullscreen(&self) -> Option { + match self.window.fullscreen() { + None => None, + Some(Fullscreen::Borderless(None)) => Some(FullscreenType::Borderless), + _ => Some(FullscreenType::Exclusive), + } + } + + #[napi] + /// Sets the window to fullscreen or back. + pub fn set_fullscreen(&self, fullscreen_type: Option) { + let monitor = self.window.current_monitor(); + + if monitor.is_none() { + return; + }; + + let video_mode = monitor.unwrap().video_modes().next(); + + if video_mode.is_none() { + return; + }; + + let fs = match fullscreen_type { + Some(FullscreenType::Exclusive) => Some(Fullscreen::Exclusive(video_mode.unwrap())), + Some(FullscreenType::Borderless) => Some(Fullscreen::Borderless(None)), + _ => None, + }; + + self.window.set_fullscreen(fs); + } } diff --git a/src/lib.rs b/src/lib.rs index 2091e3e..0a8d556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,15 +2,37 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use browser_window::{BrowserWindow, BrowserWindowOptions}; +use napi::bindgen_prelude::*; use napi::Result; use napi_derive::napi; use tao::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; +use wry::http::Request; pub mod browser_window; +#[napi(object)] +pub struct HeaderData { + pub key: String, + pub value: Option, +} + +#[napi(object)] +pub struct IpcMessage { + /// The unique identifier of the window that sent the message. + pub window_id: u32, + /// The body of the message. + pub body: Vec, + /// The HTTP method of the message. + pub method: String, + /// The headers of the message. + pub headers: Vec, + /// The URI of the message. + pub uri: String, +} + #[napi] /// Returns the version of the webview. pub fn get_webview_version() -> Result { @@ -53,6 +75,10 @@ pub struct Application { event_loop: Option>, /// The options for creating the application. options: ApplicationOptions, + /// The unique identifier of the webviews created by this application. + id_ref: u32, + /// The ipc handler callback + ipc_handler: Option, } #[napi] @@ -69,13 +95,63 @@ impl Application { wait_time: None, exit_code: None, }), + id_ref: 0, + ipc_handler: None, }) } + #[napi] + /// Sets the IPC handler callback. + pub fn on_ipc_message(&mut self, handler: Option) { + self.ipc_handler = handler; + } + + fn handle_ipc_message(&self, req: Request, id: &u32) { + let func = &self.ipc_handler.as_ref(); + + if func.is_none() { + return; + } + + let on_ipc_msg = func.unwrap(); + + println!("Received IPC message: {:?}", req); + + let body = req.body().as_bytes().to_vec(); + let headers = req + .headers() + .iter() + .map(|(k, v)| HeaderData { + key: k.as_str().to_string(), + value: match v.to_str() { + Ok(v) => Some(v.to_string()), + Err(_) => None, + }, + }) + .collect::>(); + + let msg = IpcMessage { + window_id: id.clone(), + body, + method: req.method().to_string(), + headers, + uri: req.uri().to_string(), + }; + + match on_ipc_msg.call1::(msg) { + Ok(_) => { + println!("onIpcMessage called successfully"); + } + Err(e) => { + println!("onIpcMessage error: {:?}", e); + } + }; + } + #[napi] /// Creates a new browser window. pub fn create_browser_window( - &self, + &'static mut self, options: Option, ) -> Result { let event_loop = self.event_loop.as_ref(); @@ -87,7 +163,15 @@ impl Application { )); } - let window = BrowserWindow::new(event_loop.unwrap(), options, false)?; + self.id_ref += 1; + + let next_id = &self.id_ref; + + let cb = |req: Request| { + self.handle_ipc_message(req, next_id); + }; + + let window = BrowserWindow::new(event_loop.unwrap(), options, self.id_ref, false, cb)?; Ok(window) } @@ -95,7 +179,7 @@ impl Application { #[napi] /// Creates a new browser window as a child window. pub fn create_child_browser_window( - &self, + &'static mut self, options: Option, ) -> Result { let event_loop = self.event_loop.as_ref(); @@ -107,7 +191,15 @@ impl Application { )); } - let window = BrowserWindow::new(event_loop.unwrap(), options, true)?; + self.id_ref += 1; + + let next_id = &self.id_ref; + + let cb = |req: Request| { + self.handle_ipc_message(req, next_id); + }; + + let window = BrowserWindow::new(event_loop.unwrap(), options, self.id_ref, true, cb)?; Ok(window) }