diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..97010c25e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,14 @@ +# Current WG Code Sub Teams: +# @tauri-apps/admins +# @tauri-apps/core +# @tauri-apps/testing + +# admins default to review +# Order is important; the last matching pattern takes the most precedence. +* @tauri-apps/admins + +.github @tauri-apps/admins @tauri-apps/testing + +/examples/ @tauri-apps/testing + +/src/ @tauri-apps/core diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..4ead0e1da --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..f4e8dbe99 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Tauri Contributing Guide + +Hi! We, the maintainers, are really excited that you are interested in contributing to Tauri. Before submitting your contribution though, please make sure to take a moment and read through the [Code of Conduct](CODE_OF_CONDUCT.md), as well as the appropriate section for the contribution you intend to make: + +- [Issue Reporting Guidelines](#issue-reporting-guidelines) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Development Guide](#development-guide) + +## Issue Reporting Guidelines + +- The issue list of this repo is **exclusively** for bug reports and feature requests. Non-conforming issues will be closed immediately. + +- If you have a question, you can get quick answers from the [Tauri Discord chat](https://discord.gg/SpmNs4S). + +- Try to search for your issue, it may have already been answered or even fixed in the development branch (`dev`). + +- Check if the issue is reproducible with the latest stable version of Tauri. If you are using a pre-release, please indicate the specific version you are using. + +- It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable. + +- Use only the minimum amount of code necessary to reproduce the unexpected behavior. A good bug report should isolate specific methods that exhibit unexpected behavior and precisely define how expectations were violated. What did you expect the method or methods to do, and how did the observed behavior differ? The more precisely you isolate the issue, the faster we can investigate. + +- Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed. + +- If your issue is resolved but still open, don’t hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it. + +- Most importantly, we beg your patience: the team must balance your request against many other responsibilities — fixing other bugs, answering other questions, new features, new documentation, etc. The issue list is not paid support and we cannot make guarantees about how fast your issue can be resolved. + +## Pull Request Guidelines + +- It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging. + +- If adding new feature: + + - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. + +- If fixing a bug: + - If you are resolving a special issue, add `(fix: #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `fix: update entities encoding/decoding (fix #3899)`. + - Provide detailed description of the bug in the PR, or link to an issue that does. + + +## Financial Contribution + +Tauri is an MIT-licensed open source project. Its ongoing development can be supported via [Github Sponsors](https://github.com/sponsors/nothingismagick) or [Open Collective](https://opencollective.com/tauri). We prefer Github Sponsors as donations made are doubled through the matching fund program. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..b8121e4d4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # +patreon: # +open_collective: tauri +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..3a2675caf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ + + + + + +**What kind of change does this PR introduce?** (check at least one) + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update +- [ ] Refactor +- [ ] Documentation +- [ ] Build-related changes +- [ ] Other, please describe: + +**Does this PR introduce a breaking change?** (check one) + + +- [ ] Yes. Issue #___ +- [ ] No + + +**The PR fulfills these requirements:** + +- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix: #xxx[,#xxx]`, where "xxx" is the issue number) +- [ ] A change file is added if any packages will require a version bump due to this PR per [the instructions in the readme](https://github.com/tauri-apps/tauri/blob/dev/.changes/readme.md). + +If adding a **new feature**, the PR's description includes: +- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it) + +**Other information:** diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 000000000..c105d2ed6 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,20 @@ +name: Audit + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + push: + paths: + - "Cargo.lock" + - "Cargo.toml" + +jobs: + audit-rust: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: rust audit + uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml new file mode 100644 index 000000000..3a7e5a17b --- /dev/null +++ b/.github/workflows/fmt.yml @@ -0,0 +1,24 @@ +name: fmt check + +on: + pull_request: + paths: + - 'src/**' + - 'Cargo.toml' + +jobs: + clippy_fmt_check: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cbd06ded3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| > 1.0 | :white_check_mark: | +| < 1.0 | :x: | + +## Reporting a Vulnerability + +If you have found a potential security threat, vulnerability or exploit in Tauri +or one of its upstream dependencies, please DON’T create a pull-request, DON’T +file an issue on GitHub, DON’T mention it on Discord and DON’T create a forum thread. + +We will be adding contact information to this page very soon. + +At the current time we do not have the financial ability to reward bounties, +but in extreme cases will at our discretion consider a reward. diff --git a/build.rs b/build.rs index e4eb85d9d..802995504 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,7 @@ fn main() {} #[cfg(target_os = "macos")] fn main() { - println!("cargo:rustc-link-lib=framework=WebKit"); + println!("cargo:rustc-link-lib=framework=WebKit"); } #[cfg(target_os = "windows")] diff --git a/examples/dragndrop.rs b/examples/dragndrop.rs index c71e74431..e8a56151a 100644 --- a/examples/dragndrop.rs +++ b/examples/dragndrop.rs @@ -9,20 +9,20 @@ Dropping files onto the following form is also possible:

"#; fn main() -> Result<()> { - let mut app = Application::new()?; + let mut app = Application::new()?; - app.add_window_with_configs( - Attributes { - url: Some(TEST_HTML.to_string()), - ..Default::default() - }, - None, - None, - Some(Box::new(|data| { - println!("Window 1: {:?}", data); - false // Returning true will block the OS default behaviour. - })), - )?; - app.run(); - Ok(()) + app.add_window_with_configs( + Attributes { + url: Some(TEST_HTML.to_string()), + ..Default::default() + }, + None, + None, + Some(Box::new(|data| { + println!("Window 1: {:?}", data); + false // Returning true will block the OS default behaviour. + })), + )?; + app.run(); + Ok(()) } diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index c4f477a30..635092850 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -1,20 +1,19 @@ -use wry::Result; -use wry::{Application, Attributes}; +use wry::{Application, Attributes, Result}; fn main() -> Result<()> { - let mut app = Application::new()?; + let mut app = Application::new()?; - let attributes = Attributes { - url: Some("https://www.wirple.com/".to_string()), - title: String::from("3D Render Test ^ ^"), - fullscreen: true, - transparent: true, - ..Default::default() - }; + let attributes = Attributes { + url: Some("https://www.wirple.com/".to_string()), + title: String::from("3D Render Test ^ ^"), + fullscreen: true, + transparent: true, + ..Default::default() + }; - app.add_window(attributes)?; - app.run(); - Ok(()) + app.add_window(attributes)?; + app.run(); + Ok(()) } // Test Result: diff --git a/examples/gtk.rs b/examples/gtk.rs index 1e784bd25..77dbe8ade 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -4,12 +4,12 @@ use cairo::*; use gtk::*; fn main() -> Result<()> { - gtk::init()?; - let window = Window::new(WindowType::Toplevel); + gtk::init()?; + let window = Window::new(WindowType::Toplevel); - window.show_all(); - // TODO add to webview + window.show_all(); + // TODO add to webview - gtk::main(); - Ok(()) + gtk::main(); + Ok(()) } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 49b467f2a..be99b37aa 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,15 +1,15 @@ use wry::{Application, Attributes, Result}; fn main() -> Result<()> { - let mut app = Application::new()?; + let mut app = Application::new()?; - let attributes = Attributes { - url: Some("https://tauri.studio/".to_string()), - title: String::from("Hello World!"), - ..Default::default() - }; + let attributes = Attributes { + url: Some("https://tauri.studio/".to_string()), + title: String::from("Hello World!"), + ..Default::default() + }; - app.add_window(attributes)?; - app.run(); - Ok(()) + app.add_window(attributes)?; + app.run(); + Ok(()) } diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 397efb827..5e1b022f2 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -1,59 +1,58 @@ use serde_json::Value; -use wry::Result; -use wry::{Application, Attributes, RpcRequest, WindowProxy}; +use wry::{Application, Attributes, Result, RpcRequest, WindowProxy}; fn main() -> Result<()> { - let mut app = Application::new()?; + let mut app = Application::new()?; - let attributes = Attributes { - url: Some(format!("https://tauri.studio")), - // Initialization scripts can be used to define javascript functions and variables. - initialization_scripts: vec![r#"async function openWindow() { + let attributes = Attributes { + url: Some(format!("https://tauri.studio")), + // Initialization scripts can be used to define javascript functions and variables. + initialization_scripts: vec![r#"async function openWindow() { await window.rpc.notify("openWindow", "https://i.imgur.com/x6tXcr9.gif"); }"# - .to_string()], - ..Default::default() - }; + .to_string()], + ..Default::default() + }; - let (window_tx, window_rx) = std::sync::mpsc::channel::(); - let handler = Box::new(move |_: WindowProxy, req: RpcRequest| { - if &req.method == "openWindow" { - if let Some(params) = req.params { - if let Value::String(url) = ¶ms[0] { - let _ = window_tx.send(url.to_string()); - } - } + let (window_tx, window_rx) = std::sync::mpsc::channel::(); + let handler = Box::new(move |_: WindowProxy, req: RpcRequest| { + if &req.method == "openWindow" { + if let Some(params) = req.params { + if let Value::String(url) = ¶ms[0] { + let _ = window_tx.send(url.to_string()); } - None - }); + } + } + None + }); - let window_proxy = app.add_window_with_configs(attributes, Some(handler), None, None)?; - let app_proxy = app.application_proxy(); - std::thread::spawn(move || { - let mut count = 1; - loop { - if let Ok(url) = window_rx.try_recv() { - let new_window = app_proxy - .add_window(Attributes { - width: 426., - height: 197., - title: "RODA RORA DA".into(), - url: Some(url), - ..Default::default() - }) - .unwrap(); - println!("ID of new window: {:?}", new_window.id()); - } else if count < 8 { - println!("{} seconds have passed...", count); - count += 1; - } else if count == 8 { - window_proxy.evaluate_script("openWindow()").unwrap(); - count += 1; - } - std::thread::sleep(std::time::Duration::new(1, 0)); - } - }); + let window_proxy = app.add_window_with_configs(attributes, Some(handler), None, None)?; + let app_proxy = app.application_proxy(); + std::thread::spawn(move || { + let mut count = 1; + loop { + if let Ok(url) = window_rx.try_recv() { + let new_window = app_proxy + .add_window(Attributes { + width: 426., + height: 197., + title: "RODA RORA DA".into(), + url: Some(url), + ..Default::default() + }) + .unwrap(); + println!("ID of new window: {:?}", new_window.id()); + } else if count < 8 { + println!("{} seconds have passed...", count); + count += 1; + } else if count == 8 { + window_proxy.evaluate_script("openWindow()").unwrap(); + count += 1; + } + std::thread::sleep(std::time::Duration::new(1, 0)); + } + }); - app.run(); - Ok(()) + app.run(); + Ok(()) } diff --git a/examples/rpc.rs b/examples/rpc.rs index 9c12604a9..29713ffd9 100644 --- a/examples/rpc.rs +++ b/examples/rpc.rs @@ -1,17 +1,16 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use wry::Result; -use wry::{Application, Attributes, RpcRequest, RpcResponse, WindowProxy}; +use wry::{Application, Attributes, Result, RpcRequest, RpcResponse, WindowProxy}; #[derive(Debug, Serialize, Deserialize)] struct MessageParameters { - message: String, + message: String, } fn main() -> Result<()> { - let mut app = Application::new()?; + let mut app = Application::new()?; - let html = r#" + let html = r#" "# - .to_string(), - ), - ..Default::default() - }; + .to_string(), + ), + ..Default::default() + }; - app.add_window(attributes)?; - app.run(); - Ok(()) + app.add_window(attributes)?; + app.run(); + Ok(()) } diff --git a/examples/winit.rs b/examples/winit.rs index 384dcda9a..3c8d1d4f3 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -1,53 +1,53 @@ use wry::{ - platform::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - Result, + platform::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + webview::WebViewBuilder, + Result, }; fn main() -> Result<()> { - let event_loop = EventLoop::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - let webview = WebViewBuilder::new(window) - .unwrap() - .initialize_script("menacing = 'ゴ';") - .register_protocol("wry".to_string(), |_| Ok(vec![97, 98, 99])) - .add_callback("world", |dispatcher, sequence, requests| { - dispatcher.dispatch_script("console.log(menacing);")?; - // Sequence is a number counting how many times this function being called. - if sequence < 8 { - println!("{} seconds has passed.", sequence); - } else { - // Requests is a vector of parameters passed from the caller. - println!("{:?}", requests); - } - Ok(()) - }) - .load_url("wry://tauri.studio")? - .build()?; + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + let webview = WebViewBuilder::new(window) + .unwrap() + .initialize_script("menacing = 'ゴ';") + .register_protocol("wry".to_string(), |_| Ok(vec![97, 98, 99])) + .add_callback("world", |dispatcher, sequence, requests| { + dispatcher.dispatch_script("console.log(menacing);")?; + // Sequence is a number counting how many times this function being called. + if sequence < 8 { + println!("{} seconds has passed.", sequence); + } else { + // Requests is a vector of parameters passed from the caller. + println!("{:?}", requests); + } + Ok(()) + }) + .load_url("wry://tauri.studio")? + .build()?; - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::WindowEvent { - event: WindowEvent::Resized(_), - .. - } => { - webview.resize().unwrap(); - } - Event::MainEventsCleared => { - webview.window().request_redraw(); - } - Event::RedrawRequested(_) => {} - _ => (), - } - }); + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { + event: WindowEvent::Resized(_), + .. + } => { + webview.resize().unwrap(); + } + Event::MainEventsCleared => { + webview.window().request_redraw(); + } + Event::RedrawRequested(_) => {} + _ => (), + } + }); } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..a37d2e4cc --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,14 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Unix" +use_small_heuristics = "Default" +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +edition = "2018" +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true +imports_granularity = "Crate" diff --git a/src/application/attributes.rs b/src/application/attributes.rs index 824a64cca..aebbed0b9 100644 --- a/src/application/attributes.rs +++ b/src/application/attributes.rs @@ -65,213 +65,213 @@ use std::{fs::read, path::Path}; pub type WindowRpcHandler = Box Option + Send>; pub struct CustomProtocol { - pub name: String, - pub handler: Box Result> + Send>, + pub name: String, + pub handler: Box Result> + Send>, } -/// An icon used for the window title bar, taskbar, etc. +/// An icon used for the window title bar, taskbar, etc. #[derive(Debug, Clone)] pub struct Icon(pub(crate) Vec); impl Icon { - /// Creates an icon from the file. - pub fn from_file>(path: P) -> Result { - Ok(Self(read(path)?)) - } - /// Creates an icon from raw bytes. - pub fn from_bytes>>(bytes: B) -> Result { - Ok(Self(bytes.into())) - } + /// Creates an icon from the file. + pub fn from_file>(path: P) -> Result { + Ok(Self(read(path)?)) + } + /// Creates an icon from raw bytes. + pub fn from_bytes>>(bytes: B) -> Result { + Ok(Self(bytes.into())) + } } /// Attributes to use when creating a webview window. #[derive(Debug, Clone)] pub struct Attributes { - /// Whether the window is resizable or not. - /// - /// The default is `true`. - pub resizable: bool, + /// Whether the window is resizable or not. + /// + /// The default is `true`. + pub resizable: bool, - /// The title of the window in the title bar. - /// - /// The default is `"wry"`. - pub title: String, + /// The title of the window in the title bar. + /// + /// The default is `"wry"`. + pub title: String, - /// Whether the window should be maximized upon creation. - /// - /// The default is `false`. - pub maximized: bool, + /// Whether the window should be maximized upon creation. + /// + /// The default is `false`. + pub maximized: bool, - /// Whether the window should be immediately visible upon creation. - /// - /// The default is `true`. - pub visible: bool, + /// Whether the window should be immediately visible upon creation. + /// + /// The default is `true`. + pub visible: bool, - /// Whether the WebView window should be transparent. If this is true, writing colors - /// with alpha values different than `1.0` will produce a transparent window. - /// - /// The default is `false`. - pub transparent: bool, + /// Whether the WebView window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + /// + /// The default is `false`. + pub transparent: bool, - /// Whether the window should have borders and bars. - /// - /// The default is `true`. - pub decorations: bool, + /// Whether the window should have borders and bars. + /// + /// The default is `true`. + pub decorations: bool, - /// Whether the window should always be on top of other windows. - /// - /// The default is `false`. - pub always_on_top: bool, + /// Whether the window should always be on top of other windows. + /// + /// The default is `false`. + pub always_on_top: bool, - /// The width of the window. - /// - /// The default is `800.0`. - pub width: f64, + /// The width of the window. + /// + /// The default is `800.0`. + pub width: f64, - /// The height of the window. - /// - /// The default is `600.0`. - pub height: f64, + /// The height of the window. + /// + /// The default is `600.0`. + pub height: f64, - /// The minimum width of the window. - /// - /// The default is `None`. - pub min_width: Option, + /// The minimum width of the window. + /// + /// The default is `None`. + pub min_width: Option, - /// The minimum height of the window. - /// - /// The default is `None`. - pub min_height: Option, + /// The minimum height of the window. + /// + /// The default is `None`. + pub min_height: Option, - /// The maximum width of the window. - /// - /// The default is `None`. - pub max_width: Option, + /// The maximum width of the window. + /// + /// The default is `None`. + pub max_width: Option, - /// The maximum height of the window. - /// - /// The default is `None`. - pub max_height: Option, + /// The maximum height of the window. + /// + /// The default is `None`. + pub max_height: Option, - /// The horizontal position of the window's top left corner. - /// - /// The default is `None`. - pub x: Option, + /// The horizontal position of the window's top left corner. + /// + /// The default is `None`. + pub x: Option, - /// The vertical position of the window's top left corner. - /// - /// The default is `None`. - pub y: Option, + /// The vertical position of the window's top left corner. + /// + /// The default is `None`. + pub y: Option, - /// Whether to start the window in fullscreen or not. - /// - /// The default is `false`. - pub fullscreen: bool, + /// Whether to start the window in fullscreen or not. + /// + /// The default is `false`. + pub fullscreen: bool, - /// The window icon. - /// - /// The default is `None`. - pub icon: Option, + /// The window icon. + /// + /// The default is `None`. + pub icon: Option, - /// Whether to hide the window icon in the taskbar/dock. - /// - /// The default is `false` - pub skip_taskbar: bool, + /// Whether to hide the window icon in the taskbar/dock. + /// + /// The default is `false` + pub skip_taskbar: bool, - /// The URL to be loaded in the webview window. - /// - /// The default is `None`. - pub url: Option, + /// The URL to be loaded in the webview window. + /// + /// The default is `None`. + pub url: Option, - /// Javascript Code to be initialized when loading new pages. - /// - /// The default is an empty vector. - pub initialization_scripts: Vec, + /// Javascript Code to be initialized when loading new pages. + /// + /// The default is an empty vector. + pub initialization_scripts: Vec, } impl Attributes { - pub(crate) fn split(self) -> (InnerWindowAttributes, InnerWebViewAttributes) { - ( - InnerWindowAttributes { - resizable: self.resizable, - title: self.title, - maximized: self.maximized, - visible: self.visible, - transparent: self.transparent, - decorations: self.decorations, - always_on_top: self.always_on_top, - width: self.width, - height: self.height, - min_width: self.min_width, - min_height: self.min_height, - max_width: self.max_width, - max_height: self.max_height, - x: self.x, - y: self.y, - fullscreen: self.fullscreen, - icon: self.icon, - skip_taskbar: self.skip_taskbar, - }, - InnerWebViewAttributes { - transparent: self.transparent, - url: self.url, - initialization_scripts: self.initialization_scripts, - }, - ) - } + pub(crate) fn split(self) -> (InnerWindowAttributes, InnerWebViewAttributes) { + ( + InnerWindowAttributes { + resizable: self.resizable, + title: self.title, + maximized: self.maximized, + visible: self.visible, + transparent: self.transparent, + decorations: self.decorations, + always_on_top: self.always_on_top, + width: self.width, + height: self.height, + min_width: self.min_width, + min_height: self.min_height, + max_width: self.max_width, + max_height: self.max_height, + x: self.x, + y: self.y, + fullscreen: self.fullscreen, + icon: self.icon, + skip_taskbar: self.skip_taskbar, + }, + InnerWebViewAttributes { + transparent: self.transparent, + url: self.url, + initialization_scripts: self.initialization_scripts, + }, + ) + } } impl Default for Attributes { - #[inline] - fn default() -> Self { - Self { - resizable: true, - title: "wry".to_owned(), - maximized: false, - visible: true, - transparent: false, - decorations: true, - always_on_top: false, - width: 800.0, - height: 600.0, - min_width: None, - min_height: None, - max_width: None, - max_height: None, - x: None, - y: None, - fullscreen: false, - icon: None, - skip_taskbar: false, - url: None, - initialization_scripts: vec![], - } + #[inline] + fn default() -> Self { + Self { + resizable: true, + title: "wry".to_owned(), + maximized: false, + visible: true, + transparent: false, + decorations: true, + always_on_top: false, + width: 800.0, + height: 600.0, + min_width: None, + min_height: None, + max_width: None, + max_height: None, + x: None, + y: None, + fullscreen: false, + icon: None, + skip_taskbar: false, + url: None, + initialization_scripts: vec![], } + } } pub(crate) struct InnerWindowAttributes { - pub resizable: bool, - pub title: String, - pub maximized: bool, - pub visible: bool, - pub transparent: bool, - pub decorations: bool, - pub always_on_top: bool, - pub width: f64, - pub height: f64, - pub min_width: Option, - pub min_height: Option, - pub max_width: Option, - pub max_height: Option, - pub x: Option, - pub y: Option, - pub fullscreen: bool, - pub icon: Option, - pub skip_taskbar: bool, + pub resizable: bool, + pub title: String, + pub maximized: bool, + pub visible: bool, + pub transparent: bool, + pub decorations: bool, + pub always_on_top: bool, + pub width: f64, + pub height: f64, + pub min_width: Option, + pub min_height: Option, + pub max_width: Option, + pub max_height: Option, + pub x: Option, + pub y: Option, + pub fullscreen: bool, + pub icon: Option, + pub skip_taskbar: bool, } pub(crate) struct InnerWebViewAttributes { - pub transparent: bool, - pub url: Option, - pub initialization_scripts: Vec, + pub transparent: bool, + pub url: Option, + pub initialization_scripts: Vec, } diff --git a/src/application/general.rs b/src/application/general.rs index 3b3eb7205..5b2f76643 100644 --- a/src/application/general.rs +++ b/src/application/general.rs @@ -1,34 +1,34 @@ use crate::{ - application::{App, AppProxy, InnerWebViewAttributes, InnerWindowAttributes}, - ApplicationProxy, Attributes, CustomProtocol, Error, Icon, Message, Result, WebView, - WebViewBuilder, WindowMessage, WindowProxy, WindowRpcHandler, + application::{App, AppProxy, InnerWebViewAttributes, InnerWindowAttributes}, + ApplicationProxy, Attributes, CustomProtocol, Error, Icon, Message, Result, WebView, + WebViewBuilder, WindowMessage, WindowProxy, WindowRpcHandler, }; #[cfg(target_os = "macos")] use winit::platform::macos::{ActivationPolicy, WindowBuilderExtMacOS}; pub use winit::window::WindowId; use winit::{ - dpi::{LogicalPosition, LogicalSize}, - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, - window::{Fullscreen, Icon as WinitIcon, Window, WindowAttributes, WindowBuilder}, + dpi::{LogicalPosition, LogicalSize}, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, + window::{Fullscreen, Icon as WinitIcon, Window, WindowAttributes, WindowBuilder}, }; use std::{collections::HashMap, sync::mpsc::channel}; #[cfg(target_os = "windows")] use { - libc::c_void, - std::ptr, - winapi::{ - shared::windef::HWND, - um::{ - combaseapi::{CoCreateInstance, CLSCTX_SERVER}, - shobjidl_core::{CLSID_TaskbarList, ITaskbarList}, - }, - DEFINE_GUID, + libc::c_void, + std::ptr, + winapi::{ + shared::windef::HWND, + um::{ + combaseapi::{CoCreateInstance, CLSCTX_SERVER}, + shobjidl_core::{CLSID_TaskbarList, ITaskbarList}, }, - winit::platform::windows::WindowExtWindows, + DEFINE_GUID, + }, + winit::platform::windows::WindowExtWindows, }; use crate::FileDropHandler; @@ -37,366 +37,351 @@ type EventLoopProxy = winit::event_loop::EventLoopProxy; #[derive(Clone)] pub struct InnerApplicationProxy { - proxy: EventLoopProxy, + proxy: EventLoopProxy, } impl AppProxy for InnerApplicationProxy { - fn send_message(&self, message: Message) -> Result<()> { - self.proxy - .send_event(message) - .map_err(|_| Error::MessageSender)?; - Ok(()) - } - - fn add_window( - &self, - attributes: Attributes, - file_drop_handler: Option, - rpc_handler: Option, - custom_protocol: Option, - ) -> Result { - let (sender, receiver) = channel(); - self.send_message(Message::NewWindow( - attributes, - sender, - file_drop_handler, - rpc_handler, - custom_protocol, - ))?; - Ok(receiver.recv()?) - } + fn send_message(&self, message: Message) -> Result<()> { + self + .proxy + .send_event(message) + .map_err(|_| Error::MessageSender)?; + Ok(()) + } + + fn add_window( + &self, + attributes: Attributes, + file_drop_handler: Option, + rpc_handler: Option, + custom_protocol: Option, + ) -> Result { + let (sender, receiver) = channel(); + self.send_message(Message::NewWindow( + attributes, + sender, + file_drop_handler, + rpc_handler, + custom_protocol, + ))?; + Ok(receiver.recv()?) + } } impl From<&InnerWindowAttributes> for WindowAttributes { - fn from(w: &InnerWindowAttributes) -> Self { - let min_inner_size = match (w.min_width, w.min_height) { - (Some(min_width), Some(min_height)) => { - Some(LogicalSize::new(min_width, min_height).into()) - } - _ => None, - }; + fn from(w: &InnerWindowAttributes) -> Self { + let min_inner_size = match (w.min_width, w.min_height) { + (Some(min_width), Some(min_height)) => Some(LogicalSize::new(min_width, min_height).into()), + _ => None, + }; - let max_inner_size = match (w.max_width, w.max_height) { - (Some(max_width), Some(max_height)) => { - Some(LogicalSize::new(max_width, max_height).into()) - } - _ => None, - }; - - let fullscreen = if w.fullscreen { - Some(Fullscreen::Borderless(None)) - } else { - None - }; - - Self { - resizable: w.resizable, - title: w.title.clone(), - maximized: w.maximized, - visible: w.visible, - transparent: w.transparent, - decorations: w.decorations, - always_on_top: w.always_on_top, - inner_size: Some(LogicalSize::new(w.width, w.height).into()), - min_inner_size, - max_inner_size, - fullscreen, - ..Default::default() - } + let max_inner_size = match (w.max_width, w.max_height) { + (Some(max_width), Some(max_height)) => Some(LogicalSize::new(max_width, max_height).into()), + _ => None, + }; + + let fullscreen = if w.fullscreen { + Some(Fullscreen::Borderless(None)) + } else { + None + }; + + Self { + resizable: w.resizable, + title: w.title.clone(), + maximized: w.maximized, + visible: w.visible, + transparent: w.transparent, + decorations: w.decorations, + always_on_top: w.always_on_top, + inner_size: Some(LogicalSize::new(w.width, w.height).into()), + min_inner_size, + max_inner_size, + fullscreen, + ..Default::default() } + } } pub struct InnerApplication { - webviews: HashMap, - event_loop: EventLoop, - event_loop_proxy: EventLoopProxy, + webviews: HashMap, + event_loop: EventLoop, + event_loop_proxy: EventLoopProxy, } impl App for InnerApplication { - type Id = WindowId; - type Proxy = InnerApplicationProxy; - - fn new() -> Result { - let event_loop = EventLoop::::with_user_event(); - let proxy = event_loop.create_proxy(); - Ok(Self { - webviews: HashMap::new(), - event_loop, - event_loop_proxy: proxy, - }) + type Id = WindowId; + type Proxy = InnerApplicationProxy; + + fn new() -> Result { + let event_loop = EventLoop::::with_user_event(); + let proxy = event_loop.create_proxy(); + Ok(Self { + webviews: HashMap::new(), + event_loop, + event_loop_proxy: proxy, + }) + } + + fn create_webview( + &mut self, + attributes: Attributes, + file_drop_handler: Option, + rpc_handler: Option, + custom_protocol: Option, + ) -> Result { + let (window_attrs, webview_attrs) = attributes.split(); + + let window = _create_window(&self.event_loop, window_attrs)?; + let webview = _create_webview( + self.application_proxy(), + window, + custom_protocol, + rpc_handler, + file_drop_handler, + webview_attrs, + )?; + + let id = webview.window().id(); + self.webviews.insert(id, webview); + Ok(id) + } + + fn application_proxy(&self) -> Self::Proxy { + InnerApplicationProxy { + proxy: self.event_loop_proxy.clone(), } + } - fn create_webview( - &mut self, - attributes: Attributes, - file_drop_handler: Option, - rpc_handler: Option, - custom_protocol: Option, - ) -> Result { - let (window_attrs, webview_attrs) = attributes.split(); - - let window = _create_window(&self.event_loop, window_attrs)?; - let webview = _create_webview( - self.application_proxy(), - window, - custom_protocol, - rpc_handler, - file_drop_handler, - webview_attrs, - )?; + fn run(self) { + let proxy = self.application_proxy(); + let mut windows = self.webviews; - let id = webview.window().id(); - self.webviews.insert(id, webview); - Ok(id) - } - - fn application_proxy(&self) -> Self::Proxy { - InnerApplicationProxy { - proxy: self.event_loop_proxy.clone(), - } - } + self.event_loop.run(move |event, event_loop, control_flow| { + *control_flow = ControlFlow::Wait; - fn run(self) { - let proxy = self.application_proxy(); - let mut windows = self.webviews; + for (_, w) in windows.iter() { + w.evaluate_script().unwrap(); + } + match event { + Event::WindowEvent { event, window_id } => match event { + WindowEvent::CloseRequested => { + windows.remove(&window_id); - self.event_loop.run(move |event, event_loop, control_flow| { - *control_flow = ControlFlow::Wait; - - for (_, w) in windows.iter() { - w.evaluate_script().unwrap(); + if windows.is_empty() { + *control_flow = ControlFlow::Exit; } - match event { - Event::WindowEvent { event, window_id } => match event { - WindowEvent::CloseRequested => { - windows.remove(&window_id); - - if windows.is_empty() { - *control_flow = ControlFlow::Exit; - } - } - WindowEvent::Resized(_) => { - windows[&window_id].resize().unwrap(); - } - _ => {} - }, - Event::UserEvent(message) => match message { - Message::NewWindow( - attributes, - sender, - file_drop_handler, - rpc_handler, - custom_protocol, - ) => { - let (window_attrs, webview_attrs) = attributes.split(); - let window = _create_window(&event_loop, window_attrs).unwrap(); - sender.send(window.id()).unwrap(); - let webview = _create_webview( - proxy.clone(), - window, - custom_protocol, - rpc_handler, - file_drop_handler, - webview_attrs, - ) - .unwrap(); - let id = webview.window().id(); - windows.insert(id, webview); - } - Message::Window(id, window_message) => { - if let Some(webview) = windows.get_mut(&id) { - let window = webview.window(); - match window_message { - WindowMessage::SetResizable(resizable) => { - window.set_resizable(resizable) - } - WindowMessage::SetTitle(title) => window.set_title(&title), - WindowMessage::Maximize => window.set_maximized(true), - WindowMessage::Unmaximize => window.set_maximized(false), - WindowMessage::Minimize => window.set_minimized(true), - WindowMessage::Unminimize => window.set_minimized(false), - WindowMessage::Show => window.set_visible(true), - WindowMessage::Hide => window.set_visible(false), - WindowMessage::Close => { - windows.remove(&id); - if windows.is_empty() { - *control_flow = ControlFlow::Exit; - } - } - WindowMessage::SetDecorations(decorations) => { - window.set_decorations(decorations) - } - WindowMessage::SetAlwaysOnTop(always_on_top) => { - window.set_always_on_top(always_on_top) - } - WindowMessage::SetWidth(width) => { - let mut size = - window.inner_size().to_logical(window.scale_factor()); - size.width = width; - window.set_inner_size(size); - } - WindowMessage::SetHeight(height) => { - let mut size = - window.inner_size().to_logical(window.scale_factor()); - size.height = height; - window.set_inner_size(size); - } - WindowMessage::Resize { width, height } => { - window.set_inner_size(LogicalSize::new(width, height)); - } - WindowMessage::SetMinSize { - min_width, - min_height, - } => { - window.set_min_inner_size(Some(LogicalSize::new( - min_width, min_height, - ))); - } - WindowMessage::SetMaxSize { - max_width, - max_height, - } => { - window.set_max_inner_size(Some(LogicalSize::new( - max_width, max_height, - ))); - } - WindowMessage::SetX(x) => { - if let Ok(outer_position) = window.outer_position() { - let mut outer_position = - outer_position.to_logical(window.scale_factor()); - outer_position.x = x; - window.set_outer_position(outer_position); - } - } - WindowMessage::SetY(y) => { - if let Ok(outer_position) = window.outer_position() { - let mut outer_position = - outer_position.to_logical(window.scale_factor()); - outer_position.y = y; - window.set_outer_position(outer_position); - } - } - WindowMessage::SetPosition { x, y } => { - window.set_outer_position(LogicalPosition::new(x, y)) - } - WindowMessage::SetFullscreen(fullscreen) => { - if fullscreen { - window.set_fullscreen(Some(Fullscreen::Borderless(None))) - } else { - window.set_fullscreen(None) - } - } - WindowMessage::SetIcon(icon) => { - if let Ok(icon) = load_icon(icon) { - window.set_window_icon(Some(icon)); - } - } - WindowMessage::EvaluationScript(script) => { - let _ = webview.dispatch_script(&script); - } - } - } - } - }, - _ => (), + } + WindowEvent::Resized(_) => { + windows[&window_id].resize().unwrap(); + } + _ => {} + }, + Event::UserEvent(message) => match message { + Message::NewWindow( + attributes, + sender, + file_drop_handler, + rpc_handler, + custom_protocol, + ) => { + let (window_attrs, webview_attrs) = attributes.split(); + let window = _create_window(&event_loop, window_attrs).unwrap(); + sender.send(window.id()).unwrap(); + let webview = _create_webview( + proxy.clone(), + window, + custom_protocol, + rpc_handler, + file_drop_handler, + webview_attrs, + ) + .unwrap(); + let id = webview.window().id(); + windows.insert(id, webview); + } + Message::Window(id, window_message) => { + if let Some(webview) = windows.get_mut(&id) { + let window = webview.window(); + match window_message { + WindowMessage::SetResizable(resizable) => window.set_resizable(resizable), + WindowMessage::SetTitle(title) => window.set_title(&title), + WindowMessage::Maximize => window.set_maximized(true), + WindowMessage::Unmaximize => window.set_maximized(false), + WindowMessage::Minimize => window.set_minimized(true), + WindowMessage::Unminimize => window.set_minimized(false), + WindowMessage::Show => window.set_visible(true), + WindowMessage::Hide => window.set_visible(false), + WindowMessage::Close => { + windows.remove(&id); + if windows.is_empty() { + *control_flow = ControlFlow::Exit; + } + } + WindowMessage::SetDecorations(decorations) => window.set_decorations(decorations), + WindowMessage::SetAlwaysOnTop(always_on_top) => { + window.set_always_on_top(always_on_top) + } + WindowMessage::SetWidth(width) => { + let mut size = window.inner_size().to_logical(window.scale_factor()); + size.width = width; + window.set_inner_size(size); + } + WindowMessage::SetHeight(height) => { + let mut size = window.inner_size().to_logical(window.scale_factor()); + size.height = height; + window.set_inner_size(size); + } + WindowMessage::Resize { width, height } => { + window.set_inner_size(LogicalSize::new(width, height)); + } + WindowMessage::SetMinSize { + min_width, + min_height, + } => { + window.set_min_inner_size(Some(LogicalSize::new(min_width, min_height))); + } + WindowMessage::SetMaxSize { + max_width, + max_height, + } => { + window.set_max_inner_size(Some(LogicalSize::new(max_width, max_height))); + } + WindowMessage::SetX(x) => { + if let Ok(outer_position) = window.outer_position() { + let mut outer_position = outer_position.to_logical(window.scale_factor()); + outer_position.x = x; + window.set_outer_position(outer_position); + } + } + WindowMessage::SetY(y) => { + if let Ok(outer_position) = window.outer_position() { + let mut outer_position = outer_position.to_logical(window.scale_factor()); + outer_position.y = y; + window.set_outer_position(outer_position); + } + } + WindowMessage::SetPosition { x, y } => { + window.set_outer_position(LogicalPosition::new(x, y)) + } + WindowMessage::SetFullscreen(fullscreen) => { + if fullscreen { + window.set_fullscreen(Some(Fullscreen::Borderless(None))) + } else { + window.set_fullscreen(None) + } + } + WindowMessage::SetIcon(icon) => { + if let Ok(icon) = load_icon(icon) { + window.set_window_icon(Some(icon)); + } + } + WindowMessage::EvaluationScript(script) => { + let _ = webview.dispatch_script(&script); + } + } } - }); - } + } + }, + _ => (), + } + }); + } } fn load_icon(icon: Icon) -> crate::Result { - let image = image::load_from_memory(&icon.0)?.into_rgba8(); - let (width, height) = image.dimensions(); - let rgba = image.into_raw(); - let icon = WinitIcon::from_rgba(rgba, width, height)?; - Ok(icon) + let image = image::load_from_memory(&icon.0)?.into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + let icon = WinitIcon::from_rgba(rgba, width, height)?; + Ok(icon) } #[cfg(target_os = "windows")] fn skip_taskbar(_window: &Window) { - unsafe { - let mut taskbar_list: *mut ITaskbarList = std::mem::zeroed(); - DEFINE_GUID! {IID_ITASKBAR_LIST, - 0x56FDF342, 0xfd6d, 0x11d0, 0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90} - CoCreateInstance( - &CLSID_TaskbarList, - ptr::null_mut(), - CLSCTX_SERVER, - &IID_ITASKBAR_LIST, - &mut taskbar_list as *mut *mut ITaskbarList as *mut *mut c_void, - ); - (*taskbar_list).DeleteTab(_window.hwnd() as HWND); - (*taskbar_list).Release(); - } + unsafe { + let mut taskbar_list: *mut ITaskbarList = std::mem::zeroed(); + DEFINE_GUID! {IID_ITASKBAR_LIST, + 0x56FDF342, 0xfd6d, 0x11d0, 0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90} + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_SERVER, + &IID_ITASKBAR_LIST, + &mut taskbar_list as *mut *mut ITaskbarList as *mut *mut c_void, + ); + (*taskbar_list).DeleteTab(_window.hwnd() as HWND); + (*taskbar_list).Release(); + } } fn _create_window( - event_loop: &EventLoopWindowTarget, - attributes: InnerWindowAttributes, + event_loop: &EventLoopWindowTarget, + attributes: InnerWindowAttributes, ) -> Result { - let mut window_builder = WindowBuilder::new(); - #[cfg(target_os = "macos")] - if attributes.skip_taskbar { - window_builder = window_builder.with_activation_policy(ActivationPolicy::Accessory); - } - let window_attributes = WindowAttributes::from(&attributes); - window_builder.window = window_attributes; - let window = window_builder.build(event_loop)?; - match (attributes.x, attributes.y) { - (Some(x), Some(y)) => window.set_outer_position(LogicalPosition::new(x, y)), - _ => {} - } - if let Some(icon) = attributes.icon { - window.set_window_icon(Some(load_icon(icon)?)); - } - - #[cfg(target_os = "windows")] - if attributes.skip_taskbar { - skip_taskbar(&window); - } - - Ok(window) + let mut window_builder = WindowBuilder::new(); + #[cfg(target_os = "macos")] + if attributes.skip_taskbar { + window_builder = window_builder.with_activation_policy(ActivationPolicy::Accessory); + } + let window_attributes = WindowAttributes::from(&attributes); + window_builder.window = window_attributes; + let window = window_builder.build(event_loop)?; + match (attributes.x, attributes.y) { + (Some(x), Some(y)) => window.set_outer_position(LogicalPosition::new(x, y)), + _ => {} + } + if let Some(icon) = attributes.icon { + window.set_window_icon(Some(load_icon(icon)?)); + } + + #[cfg(target_os = "windows")] + if attributes.skip_taskbar { + skip_taskbar(&window); + } + + Ok(window) } fn _create_webview( - proxy: InnerApplicationProxy, - window: Window, - custom_protocol: Option, - rpc_handler: Option, - file_drop_handler: Option, + proxy: InnerApplicationProxy, + window: Window, + custom_protocol: Option, + rpc_handler: Option, + file_drop_handler: Option, - attributes: InnerWebViewAttributes, + attributes: InnerWebViewAttributes, ) -> Result { - let window_id = window.id(); - - let mut webview = WebViewBuilder::new(window)?.transparent(attributes.transparent); - for js in attributes.initialization_scripts { - webview = webview.initialize_script(&js); - } - - if let Some(protocol) = custom_protocol { - webview = webview.register_protocol(protocol.name, protocol.handler) - } - - if let Some(rpc_handler) = rpc_handler { - webview = webview.set_rpc_handler(Box::new(move |requests| { - let proxy = WindowProxy::new( - ApplicationProxy { - inner: proxy.clone(), - }, - window_id, - ); - rpc_handler(proxy, requests) - })); - } + let window_id = window.id(); + + let mut webview = WebViewBuilder::new(window)?.transparent(attributes.transparent); + for js in attributes.initialization_scripts { + webview = webview.initialize_script(&js); + } + + if let Some(protocol) = custom_protocol { + webview = webview.register_protocol(protocol.name, protocol.handler) + } + + if let Some(rpc_handler) = rpc_handler { + webview = webview.set_rpc_handler(Box::new(move |requests| { + let proxy = WindowProxy::new( + ApplicationProxy { + inner: proxy.clone(), + }, + window_id, + ); + rpc_handler(proxy, requests) + })); + } - webview = webview.set_file_drop_handler(file_drop_handler); + webview = webview.set_file_drop_handler(file_drop_handler); - webview = match attributes.url { - Some(url) => webview.load_url(&url)?, - None => webview, - }; + webview = match attributes.url { + Some(url) => webview.load_url(&url)?, + None => webview, + }; - let webview = webview.build()?; - Ok(webview) + let webview = webview.build()?; + Ok(webview) } diff --git a/src/application/gtkrs.rs b/src/application/gtkrs.rs index 8603df2c5..4a2a2e2f8 100644 --- a/src/application/gtkrs.rs +++ b/src/application/gtkrs.rs @@ -1,21 +1,20 @@ use crate::{ - application::{App, AppProxy, InnerWebViewAttributes, InnerWindowAttributes}, - ApplicationProxy, Attributes, CustomProtocol, Error, FileDropHandler, Icon, Message, Result, - WebView, WebViewBuilder, WindowMessage, WindowProxy, WindowRpcHandler, + application::{App, AppProxy, InnerWebViewAttributes, InnerWindowAttributes}, + ApplicationProxy, Attributes, CustomProtocol, Error, FileDropHandler, Icon, Message, Result, + WebView, WebViewBuilder, WindowMessage, WindowProxy, WindowRpcHandler, }; use std::{ - cell::RefCell, - collections::HashMap, - rc::Rc, - sync::mpsc::{channel, Receiver, Sender}, + cell::RefCell, + collections::HashMap, + rc::Rc, + sync::mpsc::{channel, Receiver, Sender}, }; use cairo::Operator; use gio::{ApplicationExt as GioApplicationExt, Cancellable}; use gtk::{ - Application as GtkApp, ApplicationWindow, ApplicationWindowExt, GtkWindowExt, Inhibit, - WidgetExt, + Application as GtkApp, ApplicationWindow, ApplicationWindowExt, GtkWindowExt, Inhibit, WidgetExt, }; pub type WindowId = u32; @@ -23,416 +22,417 @@ pub type WindowId = u32; struct EventLoopProxy(Sender); impl Clone for EventLoopProxy { - fn clone(&self) -> Self { - Self(self.0.clone()) - } + fn clone(&self) -> Self { + Self(self.0.clone()) + } } #[derive(Clone)] pub struct InnerApplicationProxy { - proxy: EventLoopProxy, + proxy: EventLoopProxy, } impl AppProxy for InnerApplicationProxy { - fn send_message(&self, message: Message) -> Result<()> { - self.proxy - .0 - .send(message) - .map_err(|_| Error::MessageSender)?; - Ok(()) - } - - fn add_window( - &self, - attributes: Attributes, - file_drop_handler: Option, - rpc_handler: Option, - custom_protocol: Option, - ) -> Result { - let (sender, receiver): (Sender, Receiver) = channel(); - self.send_message(Message::NewWindow( - attributes, - sender, - file_drop_handler, - rpc_handler, - custom_protocol, - ))?; - Ok(receiver.recv()?) - } + fn send_message(&self, message: Message) -> Result<()> { + self + .proxy + .0 + .send(message) + .map_err(|_| Error::MessageSender)?; + Ok(()) + } + + fn add_window( + &self, + attributes: Attributes, + file_drop_handler: Option, + rpc_handler: Option, + custom_protocol: Option, + ) -> Result { + let (sender, receiver): (Sender, Receiver) = channel(); + self.send_message(Message::NewWindow( + attributes, + sender, + file_drop_handler, + rpc_handler, + custom_protocol, + ))?; + Ok(receiver.recv()?) + } } pub struct InnerApplication { - webviews: HashMap, - app: GtkApp, - event_loop_proxy: EventLoopProxy, - event_loop_proxy_rx: Receiver, + webviews: HashMap, + app: GtkApp, + event_loop_proxy: EventLoopProxy, + event_loop_proxy_rx: Receiver, } impl App for InnerApplication { - type Id = u32; - type Proxy = InnerApplicationProxy; - - fn new() -> Result { - let app = GtkApp::new(None, Default::default())?; - let cancellable: Option<&Cancellable> = None; - app.register(cancellable)?; - - let (event_loop_proxy_tx, event_loop_proxy_rx) = channel(); - - Ok(Self { - webviews: HashMap::new(), - app, - event_loop_proxy: EventLoopProxy(event_loop_proxy_tx), - event_loop_proxy_rx, - }) + type Id = u32; + type Proxy = InnerApplicationProxy; + + fn new() -> Result { + let app = GtkApp::new(None, Default::default())?; + let cancellable: Option<&Cancellable> = None; + app.register(cancellable)?; + + let (event_loop_proxy_tx, event_loop_proxy_rx) = channel(); + + Ok(Self { + webviews: HashMap::new(), + app, + event_loop_proxy: EventLoopProxy(event_loop_proxy_tx), + event_loop_proxy_rx, + }) + } + + fn create_webview( + &mut self, + attributes: Attributes, + file_drop_handler: Option, + rpc_handler: Option, + custom_protocol: Option, + ) -> Result { + let (window_attrs, webview_attrs) = attributes.split(); + let window = _create_window(&self.app, window_attrs)?; + + let webview = _create_webview( + self.application_proxy(), + window, + custom_protocol, + rpc_handler, + file_drop_handler, + webview_attrs, + )?; + + let id = webview.window().get_id(); + self.webviews.insert(id, webview); + + Ok(id) + } + + fn application_proxy(&self) -> Self::Proxy { + InnerApplicationProxy { + proxy: self.event_loop_proxy.clone(), } - - fn create_webview( - &mut self, - attributes: Attributes, - file_drop_handler: Option, - rpc_handler: Option, - custom_protocol: Option, - ) -> Result { - let (window_attrs, webview_attrs) = attributes.split(); - let window = _create_window(&self.app, window_attrs)?; - - let webview = _create_webview( - self.application_proxy(), - window, - custom_protocol, - rpc_handler, - file_drop_handler, - webview_attrs, - )?; - - let id = webview.window().get_id(); - self.webviews.insert(id, webview); - - Ok(id) + } + + fn run(self) { + let proxy = self.application_proxy(); + let shared_webviews = Rc::new(RefCell::new(self.webviews)); + let shared_webviews_ = shared_webviews.clone(); + + { + let webviews = shared_webviews.borrow_mut(); + for (id, w) in webviews.iter() { + let shared_webviews_ = shared_webviews_.clone(); + let id_ = *id; + w.window().connect_delete_event(move |_window, _event| { + shared_webviews_.borrow_mut().remove(&id_); + Inhibit(false) + }); + } } - fn application_proxy(&self) -> Self::Proxy { - InnerApplicationProxy { - proxy: self.event_loop_proxy.clone(), - } - } + loop { + { + let webviews = shared_webviews.borrow_mut(); - fn run(self) { - let proxy = self.application_proxy(); - let shared_webviews = Rc::new(RefCell::new(self.webviews)); - let shared_webviews_ = shared_webviews.clone(); - - { - let webviews = shared_webviews.borrow_mut(); - for (id, w) in webviews.iter() { - let shared_webviews_ = shared_webviews_.clone(); - let id_ = *id; - w.window().connect_delete_event(move |_window, _event| { - shared_webviews_.borrow_mut().remove(&id_); - Inhibit(false) - }); - } + if webviews.is_empty() { + break; } - loop { - { - let webviews = shared_webviews.borrow_mut(); + for (_, w) in webviews.iter() { + let _ = w.evaluate_script(); + } + } - if webviews.is_empty() { - break; + while let Ok(message) = self.event_loop_proxy_rx.try_recv() { + match message { + Message::NewWindow( + attributes, + sender, + file_drop_handler, + rpc_handler, + custom_protocol, + ) => { + let (window_attrs, webview_attrs) = attributes.split(); + let window = _create_window(&self.app, window_attrs).unwrap(); + sender.send(window.get_id()).unwrap(); + let webview = _create_webview( + proxy.clone(), + window, + custom_protocol, + rpc_handler, + file_drop_handler, + webview_attrs, + ) + .unwrap(); + let id = webview.window().get_id(); + let shared_webviews_ = shared_webviews_.clone(); + webview + .window() + .connect_delete_event(move |_window, _event| { + shared_webviews_.borrow_mut().remove(&id); + Inhibit(false) + }); + let mut webviews = shared_webviews.borrow_mut(); + webviews.insert(id, webview); + } + Message::Window(id, window_message) => { + if let Some(webview) = shared_webviews.borrow_mut().get_mut(&id) { + let window = webview.window(); + match window_message { + WindowMessage::SetResizable(resizable) => { + window.set_resizable(resizable); } - - for (_, w) in webviews.iter() { - let _ = w.evaluate_script(); + WindowMessage::SetTitle(title) => window.set_title(&title), + WindowMessage::Maximize => { + window.maximize(); } - } - - while let Ok(message) = self.event_loop_proxy_rx.try_recv() { - match message { - Message::NewWindow( - attributes, - sender, - file_drop_handler, - rpc_handler, - custom_protocol, - ) => { - let (window_attrs, webview_attrs) = attributes.split(); - let window = _create_window(&self.app, window_attrs).unwrap(); - sender.send(window.get_id()).unwrap(); - let webview = _create_webview( - proxy.clone(), - window, - custom_protocol, - rpc_handler, - file_drop_handler, - webview_attrs, - ) - .unwrap(); - let id = webview.window().get_id(); - let shared_webviews_ = shared_webviews_.clone(); - webview - .window() - .connect_delete_event(move |_window, _event| { - shared_webviews_.borrow_mut().remove(&id); - Inhibit(false) - }); - let mut webviews = shared_webviews.borrow_mut(); - webviews.insert(id, webview); - } - Message::Window(id, window_message) => { - if let Some(webview) = shared_webviews.borrow_mut().get_mut(&id) { - let window = webview.window(); - match window_message { - WindowMessage::SetResizable(resizable) => { - window.set_resizable(resizable); - } - WindowMessage::SetTitle(title) => window.set_title(&title), - WindowMessage::Maximize => { - window.maximize(); - } - WindowMessage::Unmaximize => { - window.unmaximize(); - } - WindowMessage::Minimize => { - window.iconify(); - } - WindowMessage::Unminimize => { - window.deiconify(); - } - WindowMessage::Show => { - window.show_all(); - } - WindowMessage::Hide => { - window.hide(); - } - WindowMessage::Close => { - window.close(); - } - WindowMessage::SetDecorations(decorations) => { - window.set_decorated(decorations); - } - WindowMessage::SetAlwaysOnTop(always_on_top) => { - window.set_keep_above(always_on_top); - } - WindowMessage::SetWidth(width) => { - window.resize(width as i32, window.get_size().1); - } - WindowMessage::SetHeight(height) => { - window.resize(window.get_size().0, height as i32); - } - WindowMessage::Resize { width, height } => { - window.resize(width as i32, height as i32); - } - WindowMessage::SetMinSize { - min_width, - min_height, - } => { - window.set_geometry_hints::( - None, - Some(&gdk::Geometry { - min_width: min_width as i32, - min_height: min_height as i32, - max_width: 0, - max_height: 0, - base_width: 0, - base_height: 0, - width_inc: 0, - height_inc: 0, - min_aspect: 0f64, - max_aspect: 0f64, - win_gravity: gdk::Gravity::Center, - }), - gdk::WindowHints::MIN_SIZE, - ); - } - WindowMessage::SetMaxSize { - max_width, - max_height, - } => { - window.set_geometry_hints::( - None, - Some(&gdk::Geometry { - min_width: 0, - min_height: 0, - max_width: max_width as i32, - max_height: max_height as i32, - base_width: 0, - base_height: 0, - width_inc: 0, - height_inc: 0, - min_aspect: 0f64, - max_aspect: 0f64, - win_gravity: gdk::Gravity::Center, - }), - gdk::WindowHints::MAX_SIZE, - ); - } - WindowMessage::SetX(x) => { - let (_, y) = window.get_position(); - window.move_(x as i32, y); - } - WindowMessage::SetY(y) => { - let (x, _) = window.get_position(); - window.move_(x, y as i32); - } - WindowMessage::SetPosition { x, y } => { - window.move_(x as i32, y as i32); - } - WindowMessage::SetFullscreen(fullscreen) => { - if fullscreen { - window.fullscreen(); - } else { - window.unfullscreen(); - } - } - WindowMessage::SetIcon(icon) => { - if let Ok(icon) = load_icon(icon) { - window.set_icon(Some(&icon)); - } - } - WindowMessage::EvaluationScript(script) => { - let _ = webview.dispatch_script(&script); - } - } - } - } + WindowMessage::Unmaximize => { + window.unmaximize(); + } + WindowMessage::Minimize => { + window.iconify(); + } + WindowMessage::Unminimize => { + window.deiconify(); + } + WindowMessage::Show => { + window.show_all(); + } + WindowMessage::Hide => { + window.hide(); } + WindowMessage::Close => { + window.close(); + } + WindowMessage::SetDecorations(decorations) => { + window.set_decorated(decorations); + } + WindowMessage::SetAlwaysOnTop(always_on_top) => { + window.set_keep_above(always_on_top); + } + WindowMessage::SetWidth(width) => { + window.resize(width as i32, window.get_size().1); + } + WindowMessage::SetHeight(height) => { + window.resize(window.get_size().0, height as i32); + } + WindowMessage::Resize { width, height } => { + window.resize(width as i32, height as i32); + } + WindowMessage::SetMinSize { + min_width, + min_height, + } => { + window.set_geometry_hints::( + None, + Some(&gdk::Geometry { + min_width: min_width as i32, + min_height: min_height as i32, + max_width: 0, + max_height: 0, + base_width: 0, + base_height: 0, + width_inc: 0, + height_inc: 0, + min_aspect: 0f64, + max_aspect: 0f64, + win_gravity: gdk::Gravity::Center, + }), + gdk::WindowHints::MIN_SIZE, + ); + } + WindowMessage::SetMaxSize { + max_width, + max_height, + } => { + window.set_geometry_hints::( + None, + Some(&gdk::Geometry { + min_width: 0, + min_height: 0, + max_width: max_width as i32, + max_height: max_height as i32, + base_width: 0, + base_height: 0, + width_inc: 0, + height_inc: 0, + min_aspect: 0f64, + max_aspect: 0f64, + win_gravity: gdk::Gravity::Center, + }), + gdk::WindowHints::MAX_SIZE, + ); + } + WindowMessage::SetX(x) => { + let (_, y) = window.get_position(); + window.move_(x as i32, y); + } + WindowMessage::SetY(y) => { + let (x, _) = window.get_position(); + window.move_(x, y as i32); + } + WindowMessage::SetPosition { x, y } => { + window.move_(x as i32, y as i32); + } + WindowMessage::SetFullscreen(fullscreen) => { + if fullscreen { + window.fullscreen(); + } else { + window.unfullscreen(); + } + } + WindowMessage::SetIcon(icon) => { + if let Ok(icon) = load_icon(icon) { + window.set_icon(Some(&icon)); + } + } + WindowMessage::EvaluationScript(script) => { + let _ = webview.dispatch_script(&script); + } + } } - gtk::main_iteration(); + } } + } + gtk::main_iteration(); } + } } fn load_icon(icon: Icon) -> Result { - let image = image::load_from_memory(&icon.0)?.into_rgba8(); - let (width, height) = image.dimensions(); - let row_stride = image.sample_layout().height_stride; - Ok(gdk_pixbuf::Pixbuf::from_mut_slice( - image.into_raw(), - gdk_pixbuf::Colorspace::Rgb, - true, - 8, - width as i32, - height as i32, - row_stride as i32, - )) + let image = image::load_from_memory(&icon.0)?.into_rgba8(); + let (width, height) = image.dimensions(); + let row_stride = image.sample_layout().height_stride; + Ok(gdk_pixbuf::Pixbuf::from_mut_slice( + image.into_raw(), + gdk_pixbuf::Colorspace::Rgb, + true, + 8, + width as i32, + height as i32, + row_stride as i32, + )) } fn _create_window(app: &GtkApp, attributes: InnerWindowAttributes) -> Result { - let window = ApplicationWindow::new(app); - - window.set_geometry_hints::( - None, - Some(&gdk::Geometry { - min_width: attributes.min_width.unwrap_or_default() as i32, - min_height: attributes.min_height.unwrap_or_default() as i32, - max_width: attributes.max_width.unwrap_or_default() as i32, - max_height: attributes.max_height.unwrap_or_default() as i32, - base_width: 0, - base_height: 0, - width_inc: 0, - height_inc: 0, - min_aspect: 0f64, - max_aspect: 0f64, - win_gravity: gdk::Gravity::Center, - }), - (if attributes.min_width.is_some() || attributes.min_height.is_some() { - gdk::WindowHints::MIN_SIZE - } else { - gdk::WindowHints::empty() - }) | (if attributes.max_width.is_some() || attributes.max_height.is_some() { - gdk::WindowHints::MAX_SIZE - } else { - gdk::WindowHints::empty() - }), - ); - - if attributes.resizable { - window.set_default_size(attributes.width as i32, attributes.height as i32); + let window = ApplicationWindow::new(app); + + window.set_geometry_hints::( + None, + Some(&gdk::Geometry { + min_width: attributes.min_width.unwrap_or_default() as i32, + min_height: attributes.min_height.unwrap_or_default() as i32, + max_width: attributes.max_width.unwrap_or_default() as i32, + max_height: attributes.max_height.unwrap_or_default() as i32, + base_width: 0, + base_height: 0, + width_inc: 0, + height_inc: 0, + min_aspect: 0f64, + max_aspect: 0f64, + win_gravity: gdk::Gravity::Center, + }), + (if attributes.min_width.is_some() || attributes.min_height.is_some() { + gdk::WindowHints::MIN_SIZE } else { - window.set_size_request(attributes.width as i32, attributes.height as i32); - } - - if attributes.transparent { - if let Some(screen) = window.get_screen() { - if let Some(visual) = screen.get_rgba_visual() { - window.set_visual(Some(&visual)); - } - } - - window.connect_draw(|_, cr| { - cr.set_source_rgba(0., 0., 0., 0.); - cr.set_operator(Operator::Source); - cr.paint(); - cr.set_operator(Operator::Over); - Inhibit(false) - }); - window.set_app_paintable(true); - } - - window.set_skip_taskbar_hint(attributes.skip_taskbar); - window.set_resizable(attributes.resizable); - window.set_title(&attributes.title); - if attributes.maximized { - window.maximize(); - } - window.set_visible(attributes.visible); - window.set_decorated(attributes.decorations); - window.set_keep_above(attributes.always_on_top); - - match (attributes.x, attributes.y) { - (Some(x), Some(y)) => window.move_(x as i32, y as i32), - _ => {} - } - - if attributes.fullscreen { - window.fullscreen(); - } - if let Some(icon) = attributes.icon { - window.set_icon(Some(&load_icon(icon)?)); + gdk::WindowHints::empty() + }) | (if attributes.max_width.is_some() || attributes.max_height.is_some() { + gdk::WindowHints::MAX_SIZE + } else { + gdk::WindowHints::empty() + }), + ); + + if attributes.resizable { + window.set_default_size(attributes.width as i32, attributes.height as i32); + } else { + window.set_size_request(attributes.width as i32, attributes.height as i32); + } + + if attributes.transparent { + if let Some(screen) = window.get_screen() { + if let Some(visual) = screen.get_rgba_visual() { + window.set_visual(Some(&visual)); + } } - Ok(window) + window.connect_draw(|_, cr| { + cr.set_source_rgba(0., 0., 0., 0.); + cr.set_operator(Operator::Source); + cr.paint(); + cr.set_operator(Operator::Over); + Inhibit(false) + }); + window.set_app_paintable(true); + } + + window.set_skip_taskbar_hint(attributes.skip_taskbar); + window.set_resizable(attributes.resizable); + window.set_title(&attributes.title); + if attributes.maximized { + window.maximize(); + } + window.set_visible(attributes.visible); + window.set_decorated(attributes.decorations); + window.set_keep_above(attributes.always_on_top); + + match (attributes.x, attributes.y) { + (Some(x), Some(y)) => window.move_(x as i32, y as i32), + _ => {} + } + + if attributes.fullscreen { + window.fullscreen(); + } + if let Some(icon) = attributes.icon { + window.set_icon(Some(&load_icon(icon)?)); + } + + Ok(window) } fn _create_webview( - proxy: InnerApplicationProxy, - window: ApplicationWindow, - custom_protocol: Option, - rpc_handler: Option, - file_drop_handler: Option, + proxy: InnerApplicationProxy, + window: ApplicationWindow, + custom_protocol: Option, + rpc_handler: Option, + file_drop_handler: Option, - attributes: InnerWebViewAttributes, + attributes: InnerWebViewAttributes, ) -> Result { - let window_id = window.get_id(); - let mut webview = WebViewBuilder::new(window)?.transparent(attributes.transparent); - for js in attributes.initialization_scripts { - webview = webview.initialize_script(&js); - } - - webview = match attributes.url { - Some(url) => webview.load_url(&url)?, - None => webview, - }; - if let Some(protocol) = custom_protocol { - webview = webview.register_protocol(protocol.name, protocol.handler); - } - - if let Some(rpc_handler) = rpc_handler { - webview = webview.set_rpc_handler(Box::new(move |requests| { - let proxy = WindowProxy::new( - ApplicationProxy { - inner: proxy.clone(), - }, - window_id, - ); - rpc_handler(proxy, requests) - })); - } - - webview = webview.set_file_drop_handler(file_drop_handler); - - let webview = webview.build()?; - Ok(webview) + let window_id = window.get_id(); + let mut webview = WebViewBuilder::new(window)?.transparent(attributes.transparent); + for js in attributes.initialization_scripts { + webview = webview.initialize_script(&js); + } + + webview = match attributes.url { + Some(url) => webview.load_url(&url)?, + None => webview, + }; + if let Some(protocol) = custom_protocol { + webview = webview.register_protocol(protocol.name, protocol.handler); + } + + if let Some(rpc_handler) = rpc_handler { + webview = webview.set_rpc_handler(Box::new(move |requests| { + let proxy = WindowProxy::new( + ApplicationProxy { + inner: proxy.clone(), + }, + window_id, + ); + rpc_handler(proxy, requests) + })); + } + + webview = webview.set_file_drop_handler(file_drop_handler); + + let webview = webview.build()?; + Ok(webview) } diff --git a/src/application/mod.rs b/src/application/mod.rs index c4539738e..0ee6e3dca 100644 --- a/src/application/mod.rs +++ b/src/application/mod.rs @@ -15,48 +15,47 @@ mod attributes; pub use attributes::{Attributes, CustomProtocol, Icon, WindowRpcHandler}; pub(crate) use attributes::{InnerWebViewAttributes, InnerWindowAttributes}; -use crate::FileDropHandler; -use crate::Result; +use crate::{FileDropHandler, Result}; use std::sync::mpsc::Sender; /// Describes a message for a WebView window. #[derive(Debug)] pub enum WindowMessage { - SetResizable(bool), - SetTitle(String), - Maximize, - Unmaximize, - Minimize, - Unminimize, - Show, - Hide, - Close, - SetDecorations(bool), - SetAlwaysOnTop(bool), - SetWidth(f64), - SetHeight(f64), - Resize { width: f64, height: f64 }, - SetMinSize { min_width: f64, min_height: f64 }, - SetMaxSize { max_width: f64, max_height: f64 }, - SetX(f64), - SetY(f64), - SetPosition { x: f64, y: f64 }, - SetFullscreen(bool), - SetIcon(Icon), - EvaluationScript(String), + SetResizable(bool), + SetTitle(String), + Maximize, + Unmaximize, + Minimize, + Unminimize, + Show, + Hide, + Close, + SetDecorations(bool), + SetAlwaysOnTop(bool), + SetWidth(f64), + SetHeight(f64), + Resize { width: f64, height: f64 }, + SetMinSize { min_width: f64, min_height: f64 }, + SetMaxSize { max_width: f64, max_height: f64 }, + SetX(f64), + SetY(f64), + SetPosition { x: f64, y: f64 }, + SetFullscreen(bool), + SetIcon(Icon), + EvaluationScript(String), } /// Describes a general message. pub enum Message { - Window(WindowId, WindowMessage), - NewWindow( - Attributes, - Sender, - Option, - Option, - Option, - ), + Window(WindowId, WindowMessage), + NewWindow( + Attributes, + Sender, + Option, + Option, + Option, + ), } /// A proxy to sent custom messages to [`Application`]. @@ -64,46 +63,46 @@ pub enum Message { /// This can be created by calling [`Application::application_proxy`]. #[derive(Clone)] pub struct ApplicationProxy { - inner: InnerApplicationProxy, + inner: InnerApplicationProxy, } impl ApplicationProxy { - /// Sends a message to the [`Application`] from which this proxy was created. - /// - /// Returns an Err if the associated EventLoop no longer exists. - pub fn send_message(&self, message: Message) -> Result<()> { - self.inner.send_message(message) - } - /// Adds another WebView window to the application. Returns its [`WindowProxy`] after created. - pub fn add_window(&self, attributes: Attributes) -> Result { - let id = self.inner.add_window(attributes, None, None, None)?; - Ok(WindowProxy::new(self.clone(), id)) - } - - /// Adds another WebView window to the application with more configuration options. Returns its [`WindowProxy`] after created. - pub fn add_window_with_configs( - &self, - attributes: Attributes, - rpc_handler: Option, - custom_protocol: Option, - file_drop_handler: Option, - ) -> Result { - let id = - self.inner - .add_window(attributes, file_drop_handler, rpc_handler, custom_protocol)?; - Ok(WindowProxy::new(self.clone(), id)) - } + /// Sends a message to the [`Application`] from which this proxy was created. + /// + /// Returns an Err if the associated EventLoop no longer exists. + pub fn send_message(&self, message: Message) -> Result<()> { + self.inner.send_message(message) + } + /// Adds another WebView window to the application. Returns its [`WindowProxy`] after created. + pub fn add_window(&self, attributes: Attributes) -> Result { + let id = self.inner.add_window(attributes, None, None, None)?; + Ok(WindowProxy::new(self.clone(), id)) + } + + /// Adds another WebView window to the application with more configuration options. Returns its [`WindowProxy`] after created. + pub fn add_window_with_configs( + &self, + attributes: Attributes, + rpc_handler: Option, + custom_protocol: Option, + file_drop_handler: Option, + ) -> Result { + let id = self + .inner + .add_window(attributes, file_drop_handler, rpc_handler, custom_protocol)?; + Ok(WindowProxy::new(self.clone(), id)) + } } trait AppProxy { - fn send_message(&self, message: Message) -> Result<()>; - fn add_window( - &self, - attributes: Attributes, - file_drop_handler: Option, - rpc_handler: Option, - custom_protocol: Option, - ) -> Result; + fn send_message(&self, message: Message) -> Result<()>; + fn add_window( + &self, + attributes: Attributes, + file_drop_handler: Option, + rpc_handler: Option, + custom_protocol: Option, + ) -> Result; } /// A proxy to customize its corresponding WebView window. @@ -113,158 +112,170 @@ trait AppProxy { /// too early. #[derive(Clone)] pub struct WindowProxy { - proxy: ApplicationProxy, - id: WindowId, + proxy: ApplicationProxy, + id: WindowId, } impl WindowProxy { - fn new(proxy: ApplicationProxy, id: WindowId) -> Self { - Self { proxy, id } - } - - /// Gets the id of the WebView window. - pub fn id(&self) -> WindowId { - self.id - } - - pub fn application_proxy(&self) -> ApplicationProxy { - self.proxy.clone() - } - - pub fn set_resizable(&self, resizable: bool) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetResizable(resizable), - )) - } - - pub fn set_title>(&self, title: S) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetTitle(title.into()), - )) - } - - pub fn maximize(&self) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::Maximize)) - } - pub fn unmaximize(&self) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::Unmaximize)) - } - - pub fn minimize(&self) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::Minimize)) - } - - pub fn unminimize(&self) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::Unminimize)) - } - - pub fn show(&self) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::Show)) - } - - pub fn hide(&self) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::Hide)) - } - - pub fn close(&self) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::Close)) - } - - pub fn set_decorations(&self, decorations: bool) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetDecorations(decorations), - )) - } - - pub fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetAlwaysOnTop(always_on_top), - )) - } - - pub fn set_width(&self, width: f64) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::SetWidth(width))) - } - - pub fn set_height(&self, height: f64) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::SetHeight(height))) - } - - pub fn resize(&self, width: f64, height: f64) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::Resize { width, height }, - )) - } - - pub fn set_min_size(&self, min_width: f64, min_height: f64) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetMinSize { - min_width, - min_height, - }, - )) - } - - pub fn set_max_size(&self, max_width: f64, max_height: f64) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetMaxSize { - max_width, - max_height, - }, - )) - } - - pub fn set_x(&self, x: f64) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::SetX(x))) - } - - pub fn set_y(&self, y: f64) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::SetY(y))) - } - - pub fn set_position(&self, x: f64, y: f64) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetPosition { x, y }, - )) - } - - pub fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::SetFullscreen(fullscreen), - )) - } - - pub fn set_icon(&self, icon: Icon) -> Result<()> { - self.proxy - .send_message(Message::Window(self.id, WindowMessage::SetIcon(icon))) - } - - pub fn evaluate_script>(&self, script: S) -> Result<()> { - self.proxy.send_message(Message::Window( - self.id, - WindowMessage::EvaluationScript(script.into()), - )) - } + fn new(proxy: ApplicationProxy, id: WindowId) -> Self { + Self { proxy, id } + } + + /// Gets the id of the WebView window. + pub fn id(&self) -> WindowId { + self.id + } + + pub fn application_proxy(&self) -> ApplicationProxy { + self.proxy.clone() + } + + pub fn set_resizable(&self, resizable: bool) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetResizable(resizable), + )) + } + + pub fn set_title>(&self, title: S) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetTitle(title.into()), + )) + } + + pub fn maximize(&self) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::Maximize)) + } + pub fn unmaximize(&self) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::Unmaximize)) + } + + pub fn minimize(&self) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::Minimize)) + } + + pub fn unminimize(&self) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::Unminimize)) + } + + pub fn show(&self) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::Show)) + } + + pub fn hide(&self) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::Hide)) + } + + pub fn close(&self) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::Close)) + } + + pub fn set_decorations(&self, decorations: bool) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetDecorations(decorations), + )) + } + + pub fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetAlwaysOnTop(always_on_top), + )) + } + + pub fn set_width(&self, width: f64) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::SetWidth(width))) + } + + pub fn set_height(&self, height: f64) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::SetHeight(height))) + } + + pub fn resize(&self, width: f64, height: f64) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::Resize { width, height }, + )) + } + + pub fn set_min_size(&self, min_width: f64, min_height: f64) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetMinSize { + min_width, + min_height, + }, + )) + } + + pub fn set_max_size(&self, max_width: f64, max_height: f64) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetMaxSize { + max_width, + max_height, + }, + )) + } + + pub fn set_x(&self, x: f64) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::SetX(x))) + } + + pub fn set_y(&self, y: f64) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::SetY(y))) + } + + pub fn set_position(&self, x: f64, y: f64) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetPosition { x, y }, + )) + } + + pub fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::SetFullscreen(fullscreen), + )) + } + + pub fn set_icon(&self, icon: Icon) -> Result<()> { + self + .proxy + .send_message(Message::Window(self.id, WindowMessage::SetIcon(icon))) + } + + pub fn evaluate_script>(&self, script: S) -> Result<()> { + self.proxy.send_message(Message::Window( + self.id, + WindowMessage::EvaluationScript(script.into()), + )) + } } /// Provides a way to create and manage WebView windows. @@ -278,93 +289,91 @@ impl WindowProxy { /// and [`Application::window_proxy`] allow you to retrieve their proxies for further management /// when running the application. pub struct Application { - inner: InnerApplication, + inner: InnerApplication, } impl Application { - /// Builds a new application. - /// - /// ***For cross-platform compatibility, the [`Application`] must be created on the main thread.*** - /// Attempting to create the application on a different thread will usually result in unexpected - /// behaviors and even panic. This restriction isn't strictly necessary on all platforms, but is - /// imposed to eliminate any nasty surprises when porting to platforms that require it. - pub fn new() -> Result { - Ok(Self { - inner: InnerApplication::new()?, - //rpc_handler: None, - }) - } - - /// Adds a WebView window to the application. Returns its [`WindowProxy`] after created. - /// - /// [`Attributes`] is the configuration struct for you to customize the window. - /// - /// To create a default window, you could just pass `.add_window(Default::default(), None)`. - pub fn add_window(&mut self, attributes: Attributes) -> Result { - let id = self.inner.create_webview(attributes, None, None, None)?; - Ok(self.window_proxy(id)) - } - - /// Adds a WebView window to the application with more configuration options. Returns its [`WindowProxy`] after created. - /// - /// [`Attributes`] is the configuration struct for you to customize the window. - /// - /// [`WindowRpcHandler`] allows you to process requests sent from Javascript side via RPC. - /// - /// [`CustomProtocol`] allows you to define custom URL scheme to handle actions like loading - /// assets. - pub fn add_window_with_configs( - &mut self, - attributes: Attributes, - rpc_handler: Option, - custom_protocol: Option, - file_drop_handler: Option, - ) -> Result { - let id = self.inner.create_webview( - attributes, - file_drop_handler, - rpc_handler, - custom_protocol, - )?; - Ok(self.window_proxy(id)) - } - - /// Returns a [`ApplicationProxy`] for you to manage the application from other threads. - pub fn application_proxy(&self) -> ApplicationProxy { - ApplicationProxy { - inner: self.inner.application_proxy(), - //rpc_handler: self.inner. - } - } - - /// Returns the [`WindowProxy`] with given `WindowId`. - pub fn window_proxy(&self, window_id: WindowId) -> WindowProxy { - WindowProxy::new(self.application_proxy(), window_id) - } - - /// Consume the application and start running it. This will hijack the main thread and iterate - /// its event loop. To further control the application after running, [`ApplicationProxy`] and - /// [`WindowProxy`] allow you to do so on other threads. - pub fn run(self) { - self.inner.run() - } + /// Builds a new application. + /// + /// ***For cross-platform compatibility, the [`Application`] must be created on the main thread.*** + /// Attempting to create the application on a different thread will usually result in unexpected + /// behaviors and even panic. This restriction isn't strictly necessary on all platforms, but is + /// imposed to eliminate any nasty surprises when porting to platforms that require it. + pub fn new() -> Result { + Ok(Self { + inner: InnerApplication::new()?, + //rpc_handler: None, + }) + } + + /// Adds a WebView window to the application. Returns its [`WindowProxy`] after created. + /// + /// [`Attributes`] is the configuration struct for you to customize the window. + /// + /// To create a default window, you could just pass `.add_window(Default::default(), None)`. + pub fn add_window(&mut self, attributes: Attributes) -> Result { + let id = self.inner.create_webview(attributes, None, None, None)?; + Ok(self.window_proxy(id)) + } + + /// Adds a WebView window to the application with more configuration options. Returns its [`WindowProxy`] after created. + /// + /// [`Attributes`] is the configuration struct for you to customize the window. + /// + /// [`WindowRpcHandler`] allows you to process requests sent from Javascript side via RPC. + /// + /// [`CustomProtocol`] allows you to define custom URL scheme to handle actions like loading + /// assets. + pub fn add_window_with_configs( + &mut self, + attributes: Attributes, + rpc_handler: Option, + custom_protocol: Option, + file_drop_handler: Option, + ) -> Result { + let id = + self + .inner + .create_webview(attributes, file_drop_handler, rpc_handler, custom_protocol)?; + Ok(self.window_proxy(id)) + } + + /// Returns a [`ApplicationProxy`] for you to manage the application from other threads. + pub fn application_proxy(&self) -> ApplicationProxy { + ApplicationProxy { + inner: self.inner.application_proxy(), + //rpc_handler: self.inner. + } + } + + /// Returns the [`WindowProxy`] with given `WindowId`. + pub fn window_proxy(&self, window_id: WindowId) -> WindowProxy { + WindowProxy::new(self.application_proxy(), window_id) + } + + /// Consume the application and start running it. This will hijack the main thread and iterate + /// its event loop. To further control the application after running, [`ApplicationProxy`] and + /// [`WindowProxy`] allow you to do so on other threads. + pub fn run(self) { + self.inner.run() + } } trait App: Sized { - type Proxy: AppProxy; - type Id: Copy; + type Proxy: AppProxy; + type Id: Copy; - fn new() -> Result; + fn new() -> Result; - fn create_webview( - &mut self, - attributes: Attributes, - file_drop_handler: Option, - rpc_handler: Option, - custom_protocol: Option, - ) -> Result; + fn create_webview( + &mut self, + attributes: Attributes, + file_drop_handler: Option, + rpc_handler: Option, + custom_protocol: Option, + ) -> Result; - fn application_proxy(&self) -> Self::Proxy; + fn application_proxy(&self) -> Self::Proxy; - fn run(self); + fn run(self); } diff --git a/src/file_drop.rs b/src/file_drop.rs index a2c687c44..e614bb228 100644 --- a/src/file_drop.rs +++ b/src/file_drop.rs @@ -2,12 +2,12 @@ use std::path::PathBuf; #[derive(Debug, Serialize, Clone)] pub enum FileDropEvent { - /// The file(s) have been dragged onto the window, but have not been dropped yet. - Hovered(Vec), - /// The file(s) have been dropped onto the window. - Dropped(Vec), - /// The file drop was aborted. - Cancelled, + /// The file(s) have been dragged onto the window, but have not been dropped yet. + Hovered(Vec), + /// The file(s) have been dropped onto the window. + Dropped(Vec), + /// The file drop was aborted. + Cancelled, } /// Initializes a new file drop handler. diff --git a/src/lib.rs b/src/lib.rs index 09db02391..29cd4aa55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,8 +76,8 @@ mod application; pub mod webview; pub use application::{ - Application, ApplicationProxy, Attributes, Icon, Message, WindowId, - WindowMessage, WindowProxy, WindowRpcHandler, + Application, ApplicationProxy, Attributes, Icon, Message, WindowId, WindowMessage, WindowProxy, + WindowRpcHandler, }; pub use serde_json::Value; pub(crate) use webview::{RpcHandler, WebView, WebViewBuilder}; @@ -96,39 +96,39 @@ pub type Result = std::result::Result; /// Errors returned by wry. #[derive(Error, Debug)] pub enum Error { - #[cfg(target_os = "linux")] - #[error(transparent)] - GlibError(#[from] glib::Error), - #[cfg(target_os = "linux")] - #[error(transparent)] - GlibBoolError(#[from] glib::BoolError), - #[error("Failed to initialize the script")] - InitScriptError, - #[error("Bad RPC request: {0} ((1))")] - RpcScriptError(String, String), - #[error(transparent)] - NulError(#[from] std::ffi::NulError), - #[cfg(not(target_os = "linux"))] - #[error(transparent)] - OsError(#[from] winit::error::OsError), - #[error(transparent)] - ReceiverError(#[from] RecvError), - #[error(transparent)] - SenderError(#[from] SendError), - #[error("Failed to send the message")] - MessageSender, - #[error(transparent)] - Json(#[from] serde_json::Error), - #[error(transparent)] - UrlError(#[from] ParseError), - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("image error: {0}")] - Image(#[from] image::ImageError), - #[cfg(not(target_os = "linux"))] - #[error("Icon error: {0}")] - Icon(#[from] BadIcon), - #[cfg(target_os = "windows")] - #[error(transparent)] - WebView2Error(#[from] webview2::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + GlibError(#[from] glib::Error), + #[cfg(target_os = "linux")] + #[error(transparent)] + GlibBoolError(#[from] glib::BoolError), + #[error("Failed to initialize the script")] + InitScriptError, + #[error("Bad RPC request: {0} ((1))")] + RpcScriptError(String, String), + #[error(transparent)] + NulError(#[from] std::ffi::NulError), + #[cfg(not(target_os = "linux"))] + #[error(transparent)] + OsError(#[from] winit::error::OsError), + #[error(transparent)] + ReceiverError(#[from] RecvError), + #[error(transparent)] + SenderError(#[from] SendError), + #[error("Failed to send the message")] + MessageSender, + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + UrlError(#[from] ParseError), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("image error: {0}")] + Image(#[from] image::ImageError), + #[cfg(not(target_os = "linux"))] + #[error("Icon error: {0}")] + Icon(#[from] BadIcon), + #[cfg(target_os = "windows")] + #[error(transparent)] + WebView2Error(#[from] webview2::Error), } diff --git a/src/webview/linux/file_drop.rs b/src/webview/linux/file_drop.rs index 91fce7be4..acc6f384b 100644 --- a/src/webview/linux/file_drop.rs +++ b/src/webview/linux/file_drop.rs @@ -6,49 +6,49 @@ use gtk::WidgetExt; use webkit2gtk::WebView; pub(crate) fn connect_drag_event(webview: Rc, handler: FileDropHandler) { - let listener = Rc::new((handler, Cell::new(None))); - - let listener_ref = listener.clone(); - webview.connect_drag_data_received(move |_, _, _, _, data, info, _| { - if info == 2 { - let uris = data - .get_uris() - .iter() - .map(|gstr| { - let path = gstr.as_str(); - PathBuf::from(path.to_string().strip_prefix("file://").unwrap_or(path)) - }) - .collect::>(); - - listener_ref.1.set(Some(uris.clone())); - listener_ref.0(FileDropEvent::Hovered(uris)); - } else { - // drag_data_received is called twice, so we can ignore this signal - } - }); - - let listener_ref = listener.clone(); - webview.connect_drag_drop(move |_, _, _, _, _| { - let uris = listener_ref.1.take(); - if let Some(uris) = uris { - gtk::Inhibit(listener_ref.0(FileDropEvent::Dropped(uris))) - } else { - gtk::Inhibit(false) - } - }); - - let listener_ref = listener.clone(); - webview.connect_drag_leave(move |_, _, time| { - if time == 0 { - // The user cancelled the drag n drop - listener_ref.0(FileDropEvent::Cancelled); - } else { - // The user dropped the file on the window, but this will be handled in connect_drag_drop instead - } - }); - - // Called when a drag "fails" - we'll just emit a Cancelled event. - let listener_ref = listener.clone(); - webview - .connect_drag_failed(move |_, _, _| gtk::Inhibit(listener_ref.0(FileDropEvent::Cancelled))); + let listener = Rc::new((handler, Cell::new(None))); + + let listener_ref = listener.clone(); + webview.connect_drag_data_received(move |_, _, _, _, data, info, _| { + if info == 2 { + let uris = data + .get_uris() + .iter() + .map(|gstr| { + let path = gstr.as_str(); + PathBuf::from(path.to_string().strip_prefix("file://").unwrap_or(path)) + }) + .collect::>(); + + listener_ref.1.set(Some(uris.clone())); + listener_ref.0(FileDropEvent::Hovered(uris)); + } else { + // drag_data_received is called twice, so we can ignore this signal + } + }); + + let listener_ref = listener.clone(); + webview.connect_drag_drop(move |_, _, _, _, _| { + let uris = listener_ref.1.take(); + if let Some(uris) = uris { + gtk::Inhibit(listener_ref.0(FileDropEvent::Dropped(uris))) + } else { + gtk::Inhibit(false) + } + }); + + let listener_ref = listener.clone(); + webview.connect_drag_leave(move |_, _, time| { + if time == 0 { + // The user cancelled the drag n drop + listener_ref.0(FileDropEvent::Cancelled); + } else { + // The user dropped the file on the window, but this will be handled in connect_drag_drop instead + } + }); + + // Called when a drag "fails" - we'll just emit a Cancelled event. + let listener_ref = listener.clone(); + webview + .connect_drag_failed(move |_, _, _| gtk::Inhibit(listener_ref.0(FileDropEvent::Cancelled))); } diff --git a/src/webview/linux/mod.rs b/src/webview/linux/mod.rs index b37726b12..60b7798ea 100644 --- a/src/webview/linux/mod.rs +++ b/src/webview/linux/mod.rs @@ -1,8 +1,9 @@ mod file_drop; -use crate::webview::mimetype::MimeType; -use crate::webview::WV; -use crate::{Error, FileDropHandler, Result, RpcHandler}; +use crate::{ + webview::{mimetype::MimeType, WV}, + Error, FileDropHandler, Result, RpcHandler, +}; use std::rc::Rc; @@ -12,169 +13,169 @@ use glib::{Bytes, FileError}; use gtk::{ApplicationWindow as Window, ContainerExt, WidgetExt}; use url::Url; use webkit2gtk::{ - SecurityManagerExt, SettingsExt, URISchemeRequestExt, UserContentInjectedFrames, - UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime, WebContext, - WebContextExt, WebView, WebViewExt, WebViewExtManual, + SecurityManagerExt, SettingsExt, URISchemeRequestExt, UserContentInjectedFrames, + UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime, WebContext, + WebContextExt, WebView, WebViewExt, WebViewExtManual, }; pub struct InnerWebView { - webview: Rc, + webview: Rc, } impl WV for InnerWebView { - type Window = Window; - - fn new Result>>( - window: &Window, - scripts: Vec, - url: Option, - transparent: bool, - custom_protocol: Option<(String, F)>, - rpc_handler: Option, - file_drop_handler: Option, - ) -> Result { - // Webview widget - let manager = UserContentManager::new(); - let context = WebContext::new(); - let webview = Rc::new(WebView::new_with_context_and_user_content_manager( - &context, &manager, - )); - - // Message handler - let wv = Rc::clone(&webview); - manager.register_script_message_handler("external"); - manager.connect_script_message_received(move |_m, msg| { - if let (Some(js), Some(context)) = (msg.get_value(), msg.get_global_context()) { - if let Some(js) = js.to_string(&context) { - if let Some(rpc_handler) = rpc_handler.as_ref() { - match super::rpc_proxy(js, rpc_handler) { - Ok(result) => { - if let Some(ref script) = result { - let cancellable: Option<&Cancellable> = None; - wv.run_javascript(script, cancellable, |_| ()); - } - } - Err(e) => { - eprintln!("{}", e); - } - } - } + type Window = Window; + + fn new Result>>( + window: &Window, + scripts: Vec, + url: Option, + transparent: bool, + custom_protocol: Option<(String, F)>, + rpc_handler: Option, + file_drop_handler: Option, + ) -> Result { + // Webview widget + let manager = UserContentManager::new(); + let context = WebContext::new(); + let webview = Rc::new(WebView::new_with_context_and_user_content_manager( + &context, &manager, + )); + + // Message handler + let wv = Rc::clone(&webview); + manager.register_script_message_handler("external"); + manager.connect_script_message_received(move |_m, msg| { + if let (Some(js), Some(context)) = (msg.get_value(), msg.get_global_context()) { + if let Some(js) = js.to_string(&context) { + if let Some(rpc_handler) = rpc_handler.as_ref() { + match super::rpc_proxy(js, rpc_handler) { + Ok(result) => { + if let Some(ref script) = result { + let cancellable: Option<&Cancellable> = None; + wv.run_javascript(script, cancellable, |_| ()); } + } + Err(e) => { + eprintln!("{}", e); + } } - }); - - window.add(&*webview); - webview.grab_focus(); - - // Enable webgl, webaudio, canvas features and others as default. - if let Some(settings) = WebViewExt::get_settings(&*webview) { - settings.set_enable_webgl(true); - settings.set_enable_webaudio(true); - settings.set_enable_accelerated_2d_canvas(true); - settings.set_javascript_can_access_clipboard(true); - - // Enable App cache - settings.set_enable_offline_web_application_cache(true); - settings.set_enable_page_cache(true); - - // Enable Smooth scrooling - settings.set_enable_smooth_scrolling(true); - - debug_assert_eq!( - { - settings.set_enable_write_console_messages_to_stdout(true); - settings.set_enable_developer_extras(true); - }, - () - ); - } - - // Transparent - if transparent { - webview.set_background_color(&RGBA { - red: 0., - green: 0., - blue: 0., - alpha: 0., - }); + } } + } + }); + + window.add(&*webview); + webview.grab_focus(); + + // Enable webgl, webaudio, canvas features and others as default. + if let Some(settings) = WebViewExt::get_settings(&*webview) { + settings.set_enable_webgl(true); + settings.set_enable_webaudio(true); + settings.set_enable_accelerated_2d_canvas(true); + settings.set_javascript_can_access_clipboard(true); + + // Enable App cache + settings.set_enable_offline_web_application_cache(true); + settings.set_enable_page_cache(true); + + // Enable Smooth scrooling + settings.set_enable_smooth_scrolling(true); + + debug_assert_eq!( + { + settings.set_enable_write_console_messages_to_stdout(true); + settings.set_enable_developer_extras(true); + }, + () + ); + } - // File drop handling - if let Some(file_drop_handler) = file_drop_handler { - file_drop::connect_drag_event(webview.clone(), file_drop_handler); - } + // Transparent + if transparent { + webview.set_background_color(&RGBA { + red: 0., + green: 0., + blue: 0., + alpha: 0., + }); + } - if window.get_visible() { - window.show_all(); - } + // File drop handling + if let Some(file_drop_handler) = file_drop_handler { + file_drop::connect_drag_event(webview.clone(), file_drop_handler); + } - let w = Self { webview }; + if window.get_visible() { + window.show_all(); + } - // Initialize scripts - w.init("window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}}")?; - for js in scripts { - w.init(&js)?; - } + let w = Self { webview }; - // Custom protocol - if let Some((name, handler)) = custom_protocol { - context - .get_security_manager() - .unwrap() - .register_uri_scheme_as_secure(&name); - context.register_uri_scheme(&name.clone(), move |request| { - if let Some(uri) = request.get_uri() { - let uri = uri.as_str(); - - match handler(uri) { - Ok(buffer) => { - let mime = MimeType::parse(&buffer, uri); - let input = gio::MemoryInputStream::from_bytes(&Bytes::from(&buffer)); - request.finish(&input, buffer.len() as i64, Some(&mime)) - } - Err(_) => request.finish_error(&mut glib::Error::new( - FileError::Exist, - "Could not get requested file.", - )), - } - } else { - request.finish_error(&mut glib::Error::new( - FileError::Exist, - "Could not get uri.", - )); - } - }); - } + // Initialize scripts + w.init("window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}}")?; + for js in scripts { + w.init(&js)?; + } - // Navigation - if let Some(url) = url { - w.webview.load_uri(url.as_str()); + // Custom protocol + if let Some((name, handler)) = custom_protocol { + context + .get_security_manager() + .unwrap() + .register_uri_scheme_as_secure(&name); + context.register_uri_scheme(&name.clone(), move |request| { + if let Some(uri) = request.get_uri() { + let uri = uri.as_str(); + + match handler(uri) { + Ok(buffer) => { + let mime = MimeType::parse(&buffer, uri); + let input = gio::MemoryInputStream::from_bytes(&Bytes::from(&buffer)); + request.finish(&input, buffer.len() as i64, Some(&mime)) + } + Err(_) => request.finish_error(&mut glib::Error::new( + FileError::Exist, + "Could not get requested file.", + )), + } + } else { + request.finish_error(&mut glib::Error::new( + FileError::Exist, + "Could not get uri.", + )); } - - Ok(w) + }); } - fn eval(&self, js: &str) -> Result<()> { - let cancellable: Option<&Cancellable> = None; - self.webview.run_javascript(js, cancellable, |_| ()); - Ok(()) + // Navigation + if let Some(url) = url { + w.webview.load_uri(url.as_str()); } + + Ok(w) + } + + fn eval(&self, js: &str) -> Result<()> { + let cancellable: Option<&Cancellable> = None; + self.webview.run_javascript(js, cancellable, |_| ()); + Ok(()) + } } impl InnerWebView { - fn init(&self, js: &str) -> Result<()> { - if let Some(manager) = self.webview.get_user_content_manager() { - let script = UserScript::new( - js, - UserContentInjectedFrames::TopFrame, - UserScriptInjectionTime::Start, - &[], - &[], - ); - manager.add_script(&script); - } else { - return Err(Error::InitScriptError); - } - Ok(()) + fn init(&self, js: &str) -> Result<()> { + if let Some(manager) = self.webview.get_user_content_manager() { + let script = UserScript::new( + js, + UserContentInjectedFrames::TopFrame, + UserScriptInjectionTime::Start, + &[], + &[], + ); + manager.add_script(&script); + } else { + return Err(Error::InitScriptError); } + Ok(()) + } } diff --git a/src/webview/macos/file_drop.rs b/src/webview/macos/file_drop.rs index d4a94834f..db7127597 100644 --- a/src/webview/macos/file_drop.rs +++ b/src/webview/macos/file_drop.rs @@ -1,151 +1,149 @@ use crate::{FileDropEvent, FileDropHandler}; use std::{ - ffi::{c_void, CStr}, - path::PathBuf, + ffi::{c_void, CStr}, + path::PathBuf, }; use once_cell::sync::Lazy; use cocoa::base::{id, BOOL, YES}; use objc::{ - declare::ClassDecl, - runtime::{Object, Sel}, + declare::ClassDecl, + runtime::{Object, Sel}, }; pub(crate) type NSDragOperation = cocoa::foundation::NSUInteger; #[allow(non_upper_case_globals)] const NSDragOperationLink: NSDragOperation = 2; -use objc::runtime::class_getInstanceMethod; -use objc::runtime::method_getImplementation; +use objc::runtime::{class_getInstanceMethod, method_getImplementation}; static OBJC_DRAGGING_ENTERED: Lazy NSDragOperation> = - Lazy::new(|| unsafe { - std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(draggingEntered:), - ))) - }); - -static OBJC_DRAGGING_EXITED: Lazy = Lazy::new(|| unsafe { + Lazy::new(|| unsafe { std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(draggingExited:), + class!(WKWebView), + sel!(draggingEntered:), ))) + }); + +static OBJC_DRAGGING_EXITED: Lazy = Lazy::new(|| unsafe { + std::mem::transmute(method_getImplementation(class_getInstanceMethod( + class!(WKWebView), + sel!(draggingExited:), + ))) }); static OBJC_PERFORM_DRAG_OPERATION: Lazy BOOL> = - Lazy::new(|| unsafe { - std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(performDragOperation:), - ))) - }); + Lazy::new(|| unsafe { + std::mem::transmute(method_getImplementation(class_getInstanceMethod( + class!(WKWebView), + sel!(performDragOperation:), + ))) + }); static OBJC_DRAGGING_UPDATED: Lazy NSDragOperation> = - Lazy::new(|| unsafe { - std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(draggingUpdated:), - ))) - }); + Lazy::new(|| unsafe { + std::mem::transmute(method_getImplementation(class_getInstanceMethod( + class!(WKWebView), + sel!(draggingUpdated:), + ))) + }); // Safety: objc runtime calls are unsafe pub(crate) unsafe fn set_file_drop_handler(webview: *mut Object, handler: FileDropHandler) { - let listener = Box::into_raw(Box::new(handler)); - (*webview).set_ivar("FileDropHandler", listener as *mut _ as *mut c_void); + let listener = Box::into_raw(Box::new(handler)); + (*webview).set_ivar("FileDropHandler", listener as *mut _ as *mut c_void); } unsafe fn get_handler(this: &Object) -> &mut FileDropHandler { - let delegate: *mut c_void = *this.get_ivar("FileDropHandler"); - &mut *(delegate as *mut FileDropHandler) + let delegate: *mut c_void = *this.get_ivar("FileDropHandler"); + &mut *(delegate as *mut FileDropHandler) } unsafe fn collect_paths(drag_info: id) -> Vec { - use cocoa::foundation::NSFastEnumeration; - use cocoa::foundation::NSString; - - let pb: id = msg_send![drag_info, draggingPasteboard]; - let mut file_drop_paths = Vec::new(); - for path in - cocoa::appkit::NSPasteboard::propertyListForType(pb, cocoa::appkit::NSFilenamesPboardType) - .iter() - { - file_drop_paths.push(PathBuf::from( - CStr::from_ptr(NSString::UTF8String(path)) - .to_string_lossy() - .into_owned(), - )); - } - file_drop_paths + use cocoa::foundation::{NSFastEnumeration, NSString}; + + let pb: id = msg_send![drag_info, draggingPasteboard]; + let mut file_drop_paths = Vec::new(); + for path in + cocoa::appkit::NSPasteboard::propertyListForType(pb, cocoa::appkit::NSFilenamesPboardType) + .iter() + { + file_drop_paths.push(PathBuf::from( + CStr::from_ptr(NSString::UTF8String(path)) + .to_string_lossy() + .into_owned(), + )); + } + file_drop_paths } extern "C" fn dragging_updated(this: &mut Object, sel: Sel, drag_info: id) -> NSDragOperation { - let os_operation = OBJC_DRAGGING_UPDATED(this, sel, drag_info); - if os_operation == 0 { - // 0 will be returned for a file drop on any arbitrary location on the webview. - // We'll override that with NSDragOperationLink. - NSDragOperationLink - } else { - // A different NSDragOperation is returned when a file is hovered over something like - // a , so we'll make sure to preserve that behaviour. - os_operation - } + let os_operation = OBJC_DRAGGING_UPDATED(this, sel, drag_info); + if os_operation == 0 { + // 0 will be returned for a file drop on any arbitrary location on the webview. + // We'll override that with NSDragOperationLink. + NSDragOperationLink + } else { + // A different NSDragOperation is returned when a file is hovered over something like + // a , so we'll make sure to preserve that behaviour. + os_operation + } } extern "C" fn dragging_entered(this: &mut Object, sel: Sel, drag_info: id) -> NSDragOperation { - let listener = unsafe { get_handler(this) }; - let paths = unsafe { collect_paths(drag_info) }; - - if !listener(FileDropEvent::Hovered(paths)) { - // Reject the Wry file drop (invoke the OS default behaviour) - OBJC_DRAGGING_ENTERED(this, sel, drag_info) - } else { - NSDragOperationLink - } + let listener = unsafe { get_handler(this) }; + let paths = unsafe { collect_paths(drag_info) }; + + if !listener(FileDropEvent::Hovered(paths)) { + // Reject the Wry file drop (invoke the OS default behaviour) + OBJC_DRAGGING_ENTERED(this, sel, drag_info) + } else { + NSDragOperationLink + } } extern "C" fn perform_drag_operation(this: &mut Object, sel: Sel, drag_info: id) -> BOOL { - let listener = unsafe { get_handler(this) }; - let paths = unsafe { collect_paths(drag_info) }; - - if !listener(FileDropEvent::Dropped(paths)) { - // Reject the Wry file drop (invoke the OS default behaviour) - OBJC_PERFORM_DRAG_OPERATION(this, sel, drag_info) - } else { - YES - } + let listener = unsafe { get_handler(this) }; + let paths = unsafe { collect_paths(drag_info) }; + + if !listener(FileDropEvent::Dropped(paths)) { + // Reject the Wry file drop (invoke the OS default behaviour) + OBJC_PERFORM_DRAG_OPERATION(this, sel, drag_info) + } else { + YES + } } extern "C" fn dragging_exited(this: &mut Object, sel: Sel, drag_info: id) { - let listener = unsafe { get_handler(this) }; - if !listener(FileDropEvent::Cancelled) { - // Reject the Wry file drop (invoke the OS default behaviour) - OBJC_DRAGGING_EXITED(this, sel, drag_info); - } + let listener = unsafe { get_handler(this) }; + if !listener(FileDropEvent::Cancelled) { + // Reject the Wry file drop (invoke the OS default behaviour) + OBJC_DRAGGING_EXITED(this, sel, drag_info); + } } pub(crate) unsafe fn add_file_drop_methods(decl: &mut ClassDecl) { - decl.add_ivar::<*mut c_void>("FileDropHandler"); - - decl.add_method( - sel!(draggingUpdated:), - dragging_updated as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation, - ); - - decl.add_method( - sel!(draggingEntered:), - dragging_entered as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation, - ); - - decl.add_method( - sel!(performDragOperation:), - perform_drag_operation as extern "C" fn(&mut Object, Sel, id) -> BOOL, - ); - - decl.add_method( - sel!(draggingExited:), - dragging_exited as extern "C" fn(&mut Object, Sel, id), - ); + decl.add_ivar::<*mut c_void>("FileDropHandler"); + + decl.add_method( + sel!(draggingUpdated:), + dragging_updated as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation, + ); + + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation, + ); + + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(draggingExited:), + dragging_exited as extern "C" fn(&mut Object, Sel, id), + ); } diff --git a/src/webview/macos/mod.rs b/src/webview/macos/mod.rs index 82335381b..62bfb068e 100644 --- a/src/webview/macos/mod.rs +++ b/src/webview/macos/mod.rs @@ -1,207 +1,209 @@ mod file_drop; -use crate::webview::mimetype::MimeType; -use crate::webview::WV; -use crate::{FileDropHandler, Result, RpcHandler}; +use crate::{ + webview::{mimetype::MimeType, WV}, + FileDropHandler, Result, RpcHandler, +}; use file_drop::{add_file_drop_methods, set_file_drop_handler}; use std::{ - ffi::{c_void, CStr}, - os::raw::c_char, - ptr::null, - slice, str, + ffi::{c_void, CStr}, + os::raw::c_char, + ptr::null, + slice, str, }; -use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable}; -use cocoa::base::id; +use cocoa::{ + appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable}, + base::id, +}; use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use objc::{ - declare::ClassDecl, - runtime::{Object, Sel}, + declare::ClassDecl, + runtime::{Object, Sel}, }; use objc_id::Id; use url::Url; use winit::{platform::macos::WindowExtMacOS, window::Window}; pub struct InnerWebView { - webview: Id, - manager: id, + webview: Id, + manager: id, } impl WV for InnerWebView { - type Window = Window; - - fn new Result>>( - window: &Window, - scripts: Vec, - url: Option, - transparent: bool, - custom_protocol: Option<(String, F)>, - rpc_handler: Option, - file_drop_handler: Option, - ) -> Result { - // Function for rpc handler - extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) { - // Safety: objc runtime calls are unsafe - unsafe { - let function = this.get_ivar::<*mut c_void>("function"); - let function: &mut RpcHandler = std::mem::transmute(*function); - let body: id = msg_send![msg, body]; - let utf8: *const c_char = msg_send![body, UTF8String]; - let js = CStr::from_ptr(utf8).to_str().expect("Invalid UTF8 string"); - - match super::rpc_proxy(js.to_string(), function) { - Ok(result) => { - if let Some(ref script) = result { - let wv: id = msg_send![msg, webView]; - let js = NSString::new(script); - let _: id = msg_send![wv, evaluateJavaScript:js completionHandler:null::<*const c_void>()]; - } - } - Err(e) => { - eprintln!("{}", e); - } - } + type Window = Window; + + fn new Result>>( + window: &Window, + scripts: Vec, + url: Option, + transparent: bool, + custom_protocol: Option<(String, F)>, + rpc_handler: Option, + file_drop_handler: Option, + ) -> Result { + // Function for rpc handler + extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) { + // Safety: objc runtime calls are unsafe + unsafe { + let function = this.get_ivar::<*mut c_void>("function"); + let function: &mut RpcHandler = std::mem::transmute(*function); + let body: id = msg_send![msg, body]; + let utf8: *const c_char = msg_send![body, UTF8String]; + let js = CStr::from_ptr(utf8).to_str().expect("Invalid UTF8 string"); + + match super::rpc_proxy(js.to_string(), function) { + Ok(result) => { + if let Some(ref script) = result { + let wv: id = msg_send![msg, webView]; + let js = NSString::new(script); + let _: id = + msg_send![wv, evaluateJavaScript:js completionHandler:null::<*const c_void>()]; } + } + Err(e) => { + eprintln!("{}", e); + } } + } + } - // Task handler for custom protocol - extern "C" fn start_task(this: &Object, _: Sel, _webview: id, task: id) { - unsafe { - let function = this.get_ivar::<*mut c_void>("function"); - let function: &mut Box Result>> = - std::mem::transmute(*function); - - // Get url request - let request: id = msg_send![task, request]; - let url: id = msg_send![request, URL]; - let nsstring = { - let s: id = msg_send![url, absoluteString]; - NSString(Id::from_ptr(s)) - }; - let uri = nsstring.to_str(); - - // Send response - if let Ok(content) = function(uri) { - let mime = MimeType::parse(&content, uri); - let nsurlresponse: id = msg_send![class!(NSURLResponse), alloc]; - let response: id = msg_send![nsurlresponse, initWithURL:url MIMEType:NSString::new(&mime) + // Task handler for custom protocol + extern "C" fn start_task(this: &Object, _: Sel, _webview: id, task: id) { + unsafe { + let function = this.get_ivar::<*mut c_void>("function"); + let function: &mut Box Result>> = std::mem::transmute(*function); + + // Get url request + let request: id = msg_send![task, request]; + let url: id = msg_send![request, URL]; + let nsstring = { + let s: id = msg_send![url, absoluteString]; + NSString(Id::from_ptr(s)) + }; + let uri = nsstring.to_str(); + + // Send response + if let Ok(content) = function(uri) { + let mime = MimeType::parse(&content, uri); + let nsurlresponse: id = msg_send![class!(NSURLResponse), alloc]; + let response: id = msg_send![nsurlresponse, initWithURL:url MIMEType:NSString::new(&mime) expectedContentLength:content.len() textEncodingName:null::()]; - let () = msg_send![task, didReceiveResponse: response]; + let () = msg_send![task, didReceiveResponse: response]; - // Send data - let bytes = content.as_ptr() as *mut c_void; - let data: id = msg_send![class!(NSData), alloc]; - let data: id = msg_send![data, initWithBytes:bytes length:content.len()]; - let () = msg_send![task, didReceiveData: data]; + // Send data + let bytes = content.as_ptr() as *mut c_void; + let data: id = msg_send![class!(NSData), alloc]; + let data: id = msg_send![data, initWithBytes:bytes length:content.len()]; + let () = msg_send![task, didReceiveData: data]; - // Finish - let () = msg_send![task, didFinish]; - } - } + // Finish + let () = msg_send![task, didFinish]; } - extern "C" fn stop_task(_: &Object, _: Sel, _webview: id, _task: id) {} - - // Safety: objc runtime calls are unsafe - unsafe { - // Config and custom protocol - let config: id = msg_send![class!(WKWebViewConfiguration), new]; - if let Some((name, function)) = custom_protocol { - let cls = ClassDecl::new("CustomURLSchemeHandler", class!(NSObject)); - let cls = match cls { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("function"); - cls.add_method( - sel!(webView:startURLSchemeTask:), - start_task as extern "C" fn(&Object, Sel, id, id), - ); - cls.add_method( - sel!(webView:stopURLSchemeTask:), - stop_task as extern "C" fn(&Object, Sel, id, id), - ); - cls.register() - } - None => class!(CustomURLSchemeHandler), - }; - let handler: id = msg_send![cls, new]; - let function: Box Result>>> = - Box::new(Box::new(function)); - - (*handler).set_ivar("function", Box::into_raw(function) as *mut _ as *mut c_void); - let () = msg_send![config, setURLSchemeHandler:handler forURLScheme:NSString::new(&name)]; - } - - // Webview and manager - let manager: id = msg_send![config, userContentController]; - let cls = match ClassDecl::new("WryWebView", class!(WKWebView)) { - Some(mut decl) => { - add_file_drop_methods(&mut decl); - decl.register() - } - _ => class!(WryWebView), - }; - let webview: id = msg_send![cls, alloc]; - let preference: id = msg_send![config, preferences]; - let yes: id = msg_send![class!(NSNumber), numberWithBool:1]; - let no: id = msg_send![class!(NSNumber), numberWithBool:0]; - - debug_assert_eq!( - { - // Equivalent Obj-C: - // [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"]; - let dev = NSString::new("developerExtrasEnabled"); - let _: id = msg_send![preference, setValue:yes forKey:dev]; - }, - () + } + } + extern "C" fn stop_task(_: &Object, _: Sel, _webview: id, _task: id) {} + + // Safety: objc runtime calls are unsafe + unsafe { + // Config and custom protocol + let config: id = msg_send![class!(WKWebViewConfiguration), new]; + if let Some((name, function)) = custom_protocol { + let cls = ClassDecl::new("CustomURLSchemeHandler", class!(NSObject)); + let cls = match cls { + Some(mut cls) => { + cls.add_ivar::<*mut c_void>("function"); + cls.add_method( + sel!(webView:startURLSchemeTask:), + start_task as extern "C" fn(&Object, Sel, id, id), ); - - if transparent { - // Equivalent Obj-C: - // [config setValue:@NO forKey:@"drawsBackground"]; - let _: id = msg_send![config, setValue:no forKey:NSString::new("drawsBackground")]; - } - - // Resize - let size = window.inner_size().to_logical(window.scale_factor()); - let rect = CGRect::new(&CGPoint::new(0., 0.), &CGSize::new(size.width, size.height)); - let _: () = msg_send![webview, initWithFrame:rect configuration:config]; - webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); - - // Message handler - if let Some(rpc_handler) = rpc_handler { - let cls = ClassDecl::new("WebViewDelegate", class!(NSObject)); - let cls = match cls { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("function"); - cls.add_method( - sel!(userContentController:didReceiveScriptMessage:), - did_receive as extern "C" fn(&Object, Sel, id, id), - ); - cls.register() - } - None => class!(WebViewDelegate), - }; - let handler: id = msg_send![cls, new]; - let function: Box = Box::new(rpc_handler); - - (*handler).set_ivar("function", Box::into_raw(function) as *mut _ as *mut c_void); - let external = NSString::new("external"); - let _: () = msg_send![manager, addScriptMessageHandler:handler name:external]; - } - - // File drop handling - if let Some(file_drop_handler) = file_drop_handler { - set_file_drop_handler(webview, file_drop_handler) - }; - - let w = Self { - webview: Id::from_ptr(webview), - manager, - }; - - // Initialize scripts - w.init( - r#"window.external = { + cls.add_method( + sel!(webView:stopURLSchemeTask:), + stop_task as extern "C" fn(&Object, Sel, id, id), + ); + cls.register() + } + None => class!(CustomURLSchemeHandler), + }; + let handler: id = msg_send![cls, new]; + let function: Box Result>>> = Box::new(Box::new(function)); + + (*handler).set_ivar("function", Box::into_raw(function) as *mut _ as *mut c_void); + let () = msg_send![config, setURLSchemeHandler:handler forURLScheme:NSString::new(&name)]; + } + + // Webview and manager + let manager: id = msg_send![config, userContentController]; + let cls = match ClassDecl::new("WryWebView", class!(WKWebView)) { + Some(mut decl) => { + add_file_drop_methods(&mut decl); + decl.register() + } + _ => class!(WryWebView), + }; + let webview: id = msg_send![cls, alloc]; + let preference: id = msg_send![config, preferences]; + let yes: id = msg_send![class!(NSNumber), numberWithBool:1]; + let no: id = msg_send![class!(NSNumber), numberWithBool:0]; + + debug_assert_eq!( + { + // Equivalent Obj-C: + // [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"]; + let dev = NSString::new("developerExtrasEnabled"); + let _: id = msg_send![preference, setValue:yes forKey:dev]; + }, + () + ); + + if transparent { + // Equivalent Obj-C: + // [config setValue:@NO forKey:@"drawsBackground"]; + let _: id = msg_send![config, setValue:no forKey:NSString::new("drawsBackground")]; + } + + // Resize + let size = window.inner_size().to_logical(window.scale_factor()); + let rect = CGRect::new(&CGPoint::new(0., 0.), &CGSize::new(size.width, size.height)); + let _: () = msg_send![webview, initWithFrame:rect configuration:config]; + webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); + + // Message handler + if let Some(rpc_handler) = rpc_handler { + let cls = ClassDecl::new("WebViewDelegate", class!(NSObject)); + let cls = match cls { + Some(mut cls) => { + cls.add_ivar::<*mut c_void>("function"); + cls.add_method( + sel!(userContentController:didReceiveScriptMessage:), + did_receive as extern "C" fn(&Object, Sel, id, id), + ); + cls.register() + } + None => class!(WebViewDelegate), + }; + let handler: id = msg_send![cls, new]; + let function: Box = Box::new(rpc_handler); + + (*handler).set_ivar("function", Box::into_raw(function) as *mut _ as *mut c_void); + let external = NSString::new("external"); + let _: () = msg_send![manager, addScriptMessageHandler:handler name:external]; + } + + // File drop handling + if let Some(file_drop_handler) = file_drop_handler { + set_file_drop_handler(webview, file_drop_handler) + }; + + let w = Self { + webview: Id::from_ptr(webview), + manager, + }; + + // Initialize scripts + w.init( + r#"window.external = { invoke: function(s) { window.webkit.messageHandlers.external.postMessage(s); }, @@ -231,68 +233,69 @@ impl WV for InnerWebView { } } }, true);"#, - ); - for js in scripts { - w.init(&js); - } - - // Navigation - if let Some(url) = url { - if url.cannot_be_a_base() { - let s = url.as_str(); - if let Some(pos) = s.find(',') { - let (_, path) = s.split_at(pos + 1); - w.navigate_to_string(path); - } - } else { - w.navigate(url.as_str()); - } - } + ); + for js in scripts { + w.init(&js); + } + + // Navigation + if let Some(url) = url { + if url.cannot_be_a_base() { + let s = url.as_str(); + if let Some(pos) = s.find(',') { + let (_, path) = s.split_at(pos + 1); + w.navigate_to_string(path); + } + } else { + w.navigate(url.as_str()); + } + } - let view = window.ns_view() as id; - view.addSubview_(webview); + let view = window.ns_view() as id; + view.addSubview_(webview); - Ok(w) - } + Ok(w) } + } - fn eval(&self, js: &str) -> Result<()> { - // Safety: objc runtime calls are unsafe - unsafe { - let _: id = msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:null::<*const c_void>()]; - } - Ok(()) + fn eval(&self, js: &str) -> Result<()> { + // Safety: objc runtime calls are unsafe + unsafe { + let _: id = msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:null::<*const c_void>()]; } + Ok(()) + } } impl InnerWebView { - fn init(&self, js: &str) { - // Safety: objc runtime calls are unsafe - // Equivalent Obj-C: - // [manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString stringWithUTF8String:js.c_str()] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]] - unsafe { - let userscript: id = msg_send![class!(WKUserScript), alloc]; - let script: id = msg_send![userscript, initWithSource:NSString::new(js) injectionTime:0 forMainFrameOnly:1]; - let _: () = msg_send![self.manager, addUserScript: script]; - } + fn init(&self, js: &str) { + // Safety: objc runtime calls are unsafe + // Equivalent Obj-C: + // [manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString stringWithUTF8String:js.c_str()] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]] + unsafe { + let userscript: id = msg_send![class!(WKUserScript), alloc]; + let script: id = + msg_send![userscript, initWithSource:NSString::new(js) injectionTime:0 forMainFrameOnly:1]; + let _: () = msg_send![self.manager, addUserScript: script]; } - - fn navigate(&self, url: &str) { - // Safety: objc runtime calls are unsafe - unsafe { - let url: id = msg_send![class!(NSURL), URLWithString: NSString::new(url)]; - let request: id = msg_send![class!(NSURLRequest), requestWithURL: url]; - let () = msg_send![self.webview, loadRequest: request]; - } + } + + fn navigate(&self, url: &str) { + // Safety: objc runtime calls are unsafe + unsafe { + let url: id = msg_send![class!(NSURL), URLWithString: NSString::new(url)]; + let request: id = msg_send![class!(NSURLRequest), requestWithURL: url]; + let () = msg_send![self.webview, loadRequest: request]; } + } - fn navigate_to_string(&self, url: &str) { - // Safety: objc runtime calls are unsafe - unsafe { - let empty: id = msg_send![class!(NSURL), URLWithString: NSString::new("")]; - let () = msg_send![self.webview, loadHTMLString:NSString::new(url) baseURL:empty]; - } + fn navigate_to_string(&self, url: &str) { + // Safety: objc runtime calls are unsafe + unsafe { + let empty: id = msg_send![class!(NSURL), URLWithString: NSString::new("")]; + let () = msg_send![self.webview, loadHTMLString:NSString::new(url) baseURL:empty]; } + } } const UTF8_ENCODING: usize = 4; @@ -300,22 +303,22 @@ const UTF8_ENCODING: usize = 4; struct NSString(Id); impl NSString { - fn new(s: &str) -> Self { - // Safety: objc runtime calls are unsafe - NSString(unsafe { - let nsstring: id = msg_send![class!(NSString), alloc]; - Id::from_ptr( - msg_send![nsstring, initWithBytes:s.as_ptr() length:s.len() encoding:UTF8_ENCODING], - ) - }) - } - - fn to_str(&self) -> &str { - unsafe { - let bytes: *const c_char = msg_send![self.0, UTF8String]; - let len = msg_send![self.0, lengthOfBytesUsingEncoding: UTF8_ENCODING]; - let bytes = slice::from_raw_parts(bytes as *const u8, len); - str::from_utf8(bytes).unwrap() - } + fn new(s: &str) -> Self { + // Safety: objc runtime calls are unsafe + NSString(unsafe { + let nsstring: id = msg_send![class!(NSString), alloc]; + Id::from_ptr( + msg_send![nsstring, initWithBytes:s.as_ptr() length:s.len() encoding:UTF8_ENCODING], + ) + }) + } + + fn to_str(&self) -> &str { + unsafe { + let bytes: *const c_char = msg_send![self.0, UTF8String]; + let len = msg_send![self.0, lengthOfBytesUsingEncoding: UTF8_ENCODING]; + let bytes = slice::from_raw_parts(bytes as *const u8, len); + str::from_utf8(bytes).unwrap() } + } } diff --git a/src/webview/mimetype.rs b/src/webview/mimetype.rs index baeb45f9d..29ca82cd0 100644 --- a/src/webview/mimetype.rs +++ b/src/webview/mimetype.rs @@ -4,118 +4,115 @@ const MIMETYPE_PLAIN: &str = "text/plain"; /// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers) pub(crate) enum MimeType { - CSS, - CSV, - HTML, - ICO, - JS, - JSON, - JSONLD, - OCTETSTREAM, - RTF, - SVG, + CSS, + CSV, + HTML, + ICO, + JS, + JSON, + JSONLD, + OCTETSTREAM, + RTF, + SVG, } impl std::fmt::Display for MimeType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mime = match self { - MimeType::CSS => "text/css", - MimeType::CSV => "text/csv", - MimeType::HTML => "text/html", - MimeType::ICO => "image/vnd.microsoft.icon", - MimeType::JS => "text/javascript", - MimeType::JSON => "application/json", - MimeType::JSONLD => "application/ld+json", - MimeType::OCTETSTREAM => "application/octet-stream", - MimeType::RTF => "application/rtf", - MimeType::SVG => "image/svg", - }; - write!(f, "{}", mime) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mime = match self { + MimeType::CSS => "text/css", + MimeType::CSV => "text/csv", + MimeType::HTML => "text/html", + MimeType::ICO => "image/vnd.microsoft.icon", + MimeType::JS => "text/javascript", + MimeType::JSON => "application/json", + MimeType::JSONLD => "application/ld+json", + MimeType::OCTETSTREAM => "application/octet-stream", + MimeType::RTF => "application/rtf", + MimeType::SVG => "image/svg", + }; + write!(f, "{}", mime) + } } impl MimeType { - /// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType. - pub fn parse_from_uri(uri: &str) -> MimeType { - let suffix = uri.split(".").last(); - match suffix { - Some("bin") => Self::OCTETSTREAM, - Some("css") => Self::CSS, - Some("csv") => Self::CSV, - Some("html") => Self::HTML, - Some("ico") => Self::ICO, - Some("js") => Self::JS, - Some("json") => Self::JSON, - Some("jsonld") => Self::JSONLD, - Some("rtf") => Self::RTF, - Some("svg") => Self::SVG, - // Assume HTML when a TLD is found for eg. `wry:://tauri.studio` | `wry://hello.com` - Some(_) => Self::HTML, - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types - // using octet stream according to this: - None => Self::OCTETSTREAM, - } + /// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType. + pub fn parse_from_uri(uri: &str) -> MimeType { + let suffix = uri.split(".").last(); + match suffix { + Some("bin") => Self::OCTETSTREAM, + Some("css") => Self::CSS, + Some("csv") => Self::CSV, + Some("html") => Self::HTML, + Some("ico") => Self::ICO, + Some("js") => Self::JS, + Some("json") => Self::JSON, + Some("jsonld") => Self::JSONLD, + Some("rtf") => Self::RTF, + Some("svg") => Self::SVG, + // Assume HTML when a TLD is found for eg. `wry:://tauri.studio` | `wry://hello.com` + Some(_) => Self::HTML, + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + // using octet stream according to this: + None => Self::OCTETSTREAM, } + } - /// infer mimetype from content (or) URI if needed. - pub fn parse(content: &Vec, uri: &str) -> String { - let mime = match infer::get(&content) { - Some(info) => info.mime_type(), - None => MIMETYPE_PLAIN, - }; - - if mime == MIMETYPE_PLAIN { - return Self::parse_from_uri(uri).to_string(); - } + /// infer mimetype from content (or) URI if needed. + pub fn parse(content: &Vec, uri: &str) -> String { + let mime = match infer::get(&content) { + Some(info) => info.mime_type(), + None => MIMETYPE_PLAIN, + }; - mime.to_string() + if mime == MIMETYPE_PLAIN { + return Self::parse_from_uri(uri).to_string(); } + + mime.to_string() + } } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn should_parse_mimetype_from_uri() { - let css = MimeType::parse_from_uri( - "https://unpkg.com/browse/bootstrap@4.1.0/dist/css/bootstrap-grid.css", - ) - .to_string(); - assert_eq!(css, "text/css".to_string()); + #[test] + fn should_parse_mimetype_from_uri() { + let css = MimeType::parse_from_uri( + "https://unpkg.com/browse/bootstrap@4.1.0/dist/css/bootstrap-grid.css", + ) + .to_string(); + assert_eq!(css, "text/css".to_string()); - let csv: String = MimeType::parse_from_uri("https://example.com/random.csv").to_string(); - assert_eq!(csv, "text/csv".to_string()); + let csv: String = MimeType::parse_from_uri("https://example.com/random.csv").to_string(); + assert_eq!(csv, "text/csv".to_string()); - let ico: String = - MimeType::parse_from_uri("https://icons.duckduckgo.com/ip3/microsoft.com.ico") - .to_string(); - assert_eq!(ico, String::from("image/vnd.microsoft.icon")); + let ico: String = + MimeType::parse_from_uri("https://icons.duckduckgo.com/ip3/microsoft.com.ico").to_string(); + assert_eq!(ico, String::from("image/vnd.microsoft.icon")); - let html: String = MimeType::parse_from_uri("https://tauri.studio/index.html").to_string(); - assert_eq!(html, String::from("text/html")); + let html: String = MimeType::parse_from_uri("https://tauri.studio/index.html").to_string(); + assert_eq!(html, String::from("text/html")); - let js: String = - MimeType::parse_from_uri("https://unpkg.com/react@17.0.1/umd/react.production.min.js") - .to_string(); - assert_eq!(js, "text/javascript".to_string()); + let js: String = + MimeType::parse_from_uri("https://unpkg.com/react@17.0.1/umd/react.production.min.js") + .to_string(); + assert_eq!(js, "text/javascript".to_string()); - let json: String = - MimeType::parse_from_uri("https://unpkg.com/browse/react@17.0.1/build-info.json") - .to_string(); - assert_eq!(json, String::from("application/json")); + let json: String = + MimeType::parse_from_uri("https://unpkg.com/browse/react@17.0.1/build-info.json").to_string(); + assert_eq!(json, String::from("application/json")); - let jsonld: String = - MimeType::parse_from_uri("https:/example.com/hello.jsonld").to_string(); - assert_eq!(jsonld, String::from("application/ld+json")); + let jsonld: String = MimeType::parse_from_uri("https:/example.com/hello.jsonld").to_string(); + assert_eq!(jsonld, String::from("application/ld+json")); - let rtf: String = MimeType::parse_from_uri("https://example.com/document.rtf").to_string(); - assert_eq!(rtf, String::from("application/rtf")); + let rtf: String = MimeType::parse_from_uri("https://example.com/document.rtf").to_string(); + assert_eq!(rtf, String::from("application/rtf")); - let svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string(); - assert_eq!(svg, String::from("image/svg")); + let svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string(); + assert_eq!(svg, String::from("image/svg")); - let custom_scheme = MimeType::parse_from_uri("wry://tauri.studio").to_string(); - assert_eq!(custom_scheme, String::from("text/html")); - } + let custom_scheme = MimeType::parse_from_uri("wry://tauri.studio").to_string(); + assert_eq!(custom_scheme, String::from("text/html")); + } } diff --git a/src/webview/mod.rs b/src/webview/mod.rs index 15f9d58da..cecdbbb54 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -45,29 +45,29 @@ pub type RpcHandler = Box Option + Send>; // Helper so all platforms handle RPC messages consistently. fn rpc_proxy(js: String, handler: &RpcHandler) -> Result> { - let req = serde_json::from_str::(&js) - .map_err(|e| Error::RpcScriptError(e.to_string(), js))?; - - let mut response = (handler)(req); - // Got a synchronous response so convert it to a script to be evaluated - if let Some(mut response) = response.take() { - if let Some(id) = response.id { - let js = if let Some(error) = response.error.take() { - RpcResponse::into_error_script(id, error)? - } else if let Some(result) = response.result.take() { - RpcResponse::into_result_script(id, result)? - } else { - // No error or result, assume a positive response - // with empty result (ACK) - RpcResponse::into_result_script(id, Value::Null)? - }; - Ok(Some(js)) - } else { - Ok(None) - } + let req = serde_json::from_str::(&js) + .map_err(|e| Error::RpcScriptError(e.to_string(), js))?; + + let mut response = (handler)(req); + // Got a synchronous response so convert it to a script to be evaluated + if let Some(mut response) = response.take() { + if let Some(id) = response.id { + let js = if let Some(error) = response.error.take() { + RpcResponse::into_error_script(id, error)? + } else if let Some(result) = response.result.take() { + RpcResponse::into_result_script(id, result)? + } else { + // No error or result, assume a positive response + // with empty result (ACK) + RpcResponse::into_result_script(id, Value::Null)? + }; + Ok(Some(js)) } else { - Ok(None) + Ok(None) } + } else { + Ok(None) + } } /// Builder type of [`WebView`]. @@ -76,69 +76,69 @@ fn rpc_proxy(js: String, handler: &RpcHandler) -> Result> { /// scripts for those who prefer to control fine grained window creation and event handling. /// [`WebViewBuilder`] privides ability to setup initialization before web engine starts. pub struct WebViewBuilder { - transparent: bool, - tx: Sender, - rx: Receiver, - initialization_scripts: Vec, - window: Window, - url: Option, - custom_protocol: Option<(String, Box Result>>)>, - rpc_handler: Option, - file_drop_handler: Option, + transparent: bool, + tx: Sender, + rx: Receiver, + initialization_scripts: Vec, + window: Window, + url: Option, + custom_protocol: Option<(String, Box Result>>)>, + rpc_handler: Option, + file_drop_handler: Option, } impl WebViewBuilder { - /// Create [`WebViewBuilder`] from provided [`Window`]. - pub fn new(window: Window) -> Result { - let (tx, rx) = channel(); - - Ok(Self { - tx, - rx, - initialization_scripts: vec![], - window, - url: None, - transparent: false, - custom_protocol: None, - rpc_handler: None, - file_drop_handler: None, - }) - } - - /// Whether the WebView window should be transparent. If this is true, writing colors - /// with alpha values different than `1.0` will produce a transparent window. - pub fn transparent(mut self, transparent: bool) -> Self { - self.transparent = transparent; - self - } - - /// Initialize javascript code when loading new pages. Everytime webview load a new page, this - /// initialization code will be executed. It is guaranteed that code is executed before - /// `window.onload`. - pub fn initialize_script(mut self, js: &str) -> Self { - self.initialization_scripts.push(js.to_string()); - self - } - - /// Create a [`Dispatcher`] to send evaluation scripts to the WebView. [`WebView`] is not thread - /// safe because it must be run on the main thread who creates it. [`Dispatcher`] can let you - /// send the scripts from other threads. - pub fn dispatcher(&self) -> Dispatcher { - Dispatcher(self.tx.clone()) - } - - /// Register custom file loading protocol - pub fn register_protocol(mut self, name: String, handler: F) -> Self - where - F: Fn(&str) -> Result> + 'static, - { - self.custom_protocol = Some((name, Box::new(handler))); - self - } - - /// Set the RPC handler. - pub fn set_rpc_handler(mut self, handler: RpcHandler) -> Self { - let js = r#" + /// Create [`WebViewBuilder`] from provided [`Window`]. + pub fn new(window: Window) -> Result { + let (tx, rx) = channel(); + + Ok(Self { + tx, + rx, + initialization_scripts: vec![], + window, + url: None, + transparent: false, + custom_protocol: None, + rpc_handler: None, + file_drop_handler: None, + }) + } + + /// Whether the WebView window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + pub fn transparent(mut self, transparent: bool) -> Self { + self.transparent = transparent; + self + } + + /// Initialize javascript code when loading new pages. Everytime webview load a new page, this + /// initialization code will be executed. It is guaranteed that code is executed before + /// `window.onload`. + pub fn initialize_script(mut self, js: &str) -> Self { + self.initialization_scripts.push(js.to_string()); + self + } + + /// Create a [`Dispatcher`] to send evaluation scripts to the WebView. [`WebView`] is not thread + /// safe because it must be run on the main thread who creates it. [`Dispatcher`] can let you + /// send the scripts from other threads. + pub fn dispatcher(&self) -> Dispatcher { + Dispatcher(self.tx.clone()) + } + + /// Register custom file loading protocol + pub fn register_protocol(mut self, name: String, handler: F) -> Self + where + F: Fn(&str) -> Result> + 'static, + { + self.custom_protocol = Some((name, Box::new(handler))); + self + } + + /// Set the RPC handler. + pub fn set_rpc_handler(mut self, handler: RpcHandler) -> Self { + let js = r#" (function() { function Rpc() { const self = this; @@ -186,41 +186,41 @@ impl WebViewBuilder { })(); "#; - self.initialization_scripts.push(js.to_string()); - self.rpc_handler = Some(handler); - self - } - - pub fn set_file_drop_handler(mut self, handler: Option) -> Self { - self.file_drop_handler = handler; - self - } - - /// Load the provided URL when the builder calling [`WebViewBuilder::build`] to create the - /// [`WebView`]. The provided URL must be valid. - pub fn load_url(mut self, url: &str) -> Result { - self.url = Some(Url::parse(url)?); - Ok(self) - } - - /// Consume the builder and create the [`WebView`]. - pub fn build(self) -> Result { - let webview = InnerWebView::new( - &self.window, - self.initialization_scripts, - self.url, - self.transparent, - self.custom_protocol, - self.rpc_handler, - self.file_drop_handler, - )?; - Ok(WebView { - window: self.window, - webview, - tx: self.tx, - rx: self.rx, - }) - } + self.initialization_scripts.push(js.to_string()); + self.rpc_handler = Some(handler); + self + } + + pub fn set_file_drop_handler(mut self, handler: Option) -> Self { + self.file_drop_handler = handler; + self + } + + /// Load the provided URL when the builder calling [`WebViewBuilder::build`] to create the + /// [`WebView`]. The provided URL must be valid. + pub fn load_url(mut self, url: &str) -> Result { + self.url = Some(Url::parse(url)?); + Ok(self) + } + + /// Consume the builder and create the [`WebView`]. + pub fn build(self) -> Result { + let webview = InnerWebView::new( + &self.window, + self.initialization_scripts, + self.url, + self.transparent, + self.custom_protocol, + self.rpc_handler, + self.file_drop_handler, + )?; + Ok(WebView { + window: self.window, + webview, + tx: self.tx, + rx: self.rx, + }) + } } /// The fundamental type to present a [`WebView`]. @@ -230,75 +230,74 @@ impl WebViewBuilder { /// [`WebView`] presents the actuall WebView window and let you still able to perform actions /// during event handling to it. [`WebView`] also contains the associate [`Window`] with it. pub struct WebView { - window: Window, - webview: InnerWebView, - tx: Sender, - rx: Receiver, + window: Window, + webview: InnerWebView, + tx: Sender, + rx: Receiver, } impl WebView { - /// Create a [`WebView`] from provided [`Window`]. Note that calling this directly loses - /// abilities to initialize scripts, add rpc handler, and many more before starting WebView. To - /// benefit from above features, create a [`WebViewBuilder`] instead. - pub fn new(window: Window) -> Result { - Self::new_with_configs(window, false) - } - - /// Create a [`WebView`] from provided [`Window`] along with several configurations. - /// Note that calling this directly loses abilities to initialize scripts, add rpc handler, and - /// many more before starting WebView. To benefit from above features, create a - /// [`WebViewBuilder`] instead. - pub fn new_with_configs(window: Window, transparent: bool) -> Result { - let picky_none: Option<(String, Box Result>>)> = None; - - let webview = - InnerWebView::new(&window, vec![], None, transparent, picky_none, None, None)?; - - let (tx, rx) = channel(); - - Ok(Self { - window, - webview, - tx, - rx, - }) - } - /// Dispatch javascript code to be evaluated later. Note this will not actually run the - /// scripts being dispatched. Users need to call [`WebView::evaluate_script`] to execute them. - pub fn dispatch_script(&mut self, js: &str) -> Result<()> { - self.tx.send(js.to_string())?; - Ok(()) - } - - /// Create a [`Dispatcher`] to send evaluation scripts to the WebView. [`WebView`] is not thread - /// safe because it must be run on the main thread who creates it. [`Dispatcher`] can let you - /// send the scripts from other threads. - pub fn dispatcher(&self) -> Dispatcher { - Dispatcher(self.tx.clone()) + /// Create a [`WebView`] from provided [`Window`]. Note that calling this directly loses + /// abilities to initialize scripts, add rpc handler, and many more before starting WebView. To + /// benefit from above features, create a [`WebViewBuilder`] instead. + pub fn new(window: Window) -> Result { + Self::new_with_configs(window, false) + } + + /// Create a [`WebView`] from provided [`Window`] along with several configurations. + /// Note that calling this directly loses abilities to initialize scripts, add rpc handler, and + /// many more before starting WebView. To benefit from above features, create a + /// [`WebViewBuilder`] instead. + pub fn new_with_configs(window: Window, transparent: bool) -> Result { + let picky_none: Option<(String, Box Result>>)> = None; + + let webview = InnerWebView::new(&window, vec![], None, transparent, picky_none, None, None)?; + + let (tx, rx) = channel(); + + Ok(Self { + window, + webview, + tx, + rx, + }) + } + /// Dispatch javascript code to be evaluated later. Note this will not actually run the + /// scripts being dispatched. Users need to call [`WebView::evaluate_script`] to execute them. + pub fn dispatch_script(&mut self, js: &str) -> Result<()> { + self.tx.send(js.to_string())?; + Ok(()) + } + + /// Create a [`Dispatcher`] to send evaluation scripts to the WebView. [`WebView`] is not thread + /// safe because it must be run on the main thread who creates it. [`Dispatcher`] can let you + /// send the scripts from other threads. + pub fn dispatcher(&self) -> Dispatcher { + Dispatcher(self.tx.clone()) + } + + /// Get the [`Window`] associate with the [`WebView`]. This can let you perform window related + /// actions. + pub fn window(&self) -> &Window { + &self.window + } + + /// Evaluate the scripts sent from [`Dispatcher`]s. + pub fn evaluate_script(&self) -> Result<()> { + while let Ok(js) = self.rx.try_recv() { + self.webview.eval(&js)?; } - /// Get the [`Window`] associate with the [`WebView`]. This can let you perform window related - /// actions. - pub fn window(&self) -> &Window { - &self.window - } - - /// Evaluate the scripts sent from [`Dispatcher`]s. - pub fn evaluate_script(&self) -> Result<()> { - while let Ok(js) = self.rx.try_recv() { - self.webview.eval(&js)?; - } - - Ok(()) - } + Ok(()) + } - /// Resize the WebView manually. This is required on Windows because its WebView API doesn't - /// provide a way to resize automatically. - pub fn resize(&self) -> Result<()> { - #[cfg(target_os = "windows")] - self.webview.resize(self.window.hwnd())?; - Ok(()) - } + /// Resize the WebView manually. This is required on Windows because its WebView API doesn't + /// provide a way to resize automatically. + pub fn resize(&self) -> Result<()> { + #[cfg(target_os = "windows")] + self.webview.resize(self.window.hwnd())?; + Ok(()) + } } #[derive(Clone)] @@ -309,28 +308,28 @@ impl WebView { pub struct Dispatcher(Sender); impl Dispatcher { - /// Dispatch javascript code to be evaluated later. Note this will not actually run the - /// scripts being dispatched. Users need to call [`WebView::evaluate_script`] to execute them. - pub fn dispatch_script(&self, js: &str) -> Result<()> { - self.0.send(js.to_string())?; - Ok(()) - } + /// Dispatch javascript code to be evaluated later. Note this will not actually run the + /// scripts being dispatched. Users need to call [`WebView::evaluate_script`] to execute them. + pub fn dispatch_script(&self, js: &str) -> Result<()> { + self.0.send(js.to_string())?; + Ok(()) + } } pub(crate) trait WV: Sized { - type Window; - - fn new Result>>( - window: &Self::Window, - scripts: Vec, - url: Option, - transparent: bool, - custom_protocol: Option<(String, F)>, - rpc_handler: Option, - file_drop_handler: Option, - ) -> Result; - - fn eval(&self, js: &str) -> Result<()>; + type Window; + + fn new Result>>( + window: &Self::Window, + scripts: Vec, + url: Option, + transparent: bool, + custom_protocol: Option<(String, F)>, + rpc_handler: Option, + file_drop_handler: Option, + ) -> Result; + + fn eval(&self, js: &str) -> Result<()>; } const RPC_VERSION: &str = "2.0"; @@ -341,59 +340,59 @@ const RPC_VERSION: &str = "2.0"; /// the parameter. You don't create this by yourself. #[derive(Debug, Serialize, Deserialize)] pub struct RpcRequest { - jsonrpc: String, - pub id: Option, - pub method: String, - pub params: Option, + jsonrpc: String, + pub id: Option, + pub method: String, + pub params: Option, } /// RPC response message which being sent back to the Javascript side. #[derive(Debug, Serialize, Deserialize)] pub struct RpcResponse { - jsonrpc: String, - pub(crate) id: Option, - pub(crate) result: Option, - pub(crate) error: Option, + jsonrpc: String, + pub(crate) id: Option, + pub(crate) result: Option, + pub(crate) error: Option, } impl RpcResponse { - /// Create a new result response. - pub fn new_result(id: Option, result: Option) -> Self { - Self { - jsonrpc: RPC_VERSION.to_string(), - id, - result, - error: None, - } + /// Create a new result response. + pub fn new_result(id: Option, result: Option) -> Self { + Self { + jsonrpc: RPC_VERSION.to_string(), + id, + result, + error: None, } - - /// Create a new error response. - pub fn new_error(id: Option, error: Option) -> Self { - Self { - jsonrpc: RPC_VERSION.to_string(), - id, - error, - result: None, - } - } - - /// Get a script that resolves the promise with a result. - pub fn into_result_script(id: Value, result: Value) -> Result { - let retval = serde_json::to_string(&result)?; - Ok(format!( - "window.external.rpc._result({}, {})", - id.to_string(), - retval - )) - } - - /// Get a script that rejects the promise with an error. - pub fn into_error_script(id: Value, result: Value) -> Result { - let retval = serde_json::to_string(&result)?; - Ok(format!( - "window.external.rpc._error({}, {})", - id.to_string(), - retval - )) + } + + /// Create a new error response. + pub fn new_error(id: Option, error: Option) -> Self { + Self { + jsonrpc: RPC_VERSION.to_string(), + id, + error, + result: None, } + } + + /// Get a script that resolves the promise with a result. + pub fn into_result_script(id: Value, result: Value) -> Result { + let retval = serde_json::to_string(&result)?; + Ok(format!( + "window.external.rpc._result({}, {})", + id.to_string(), + retval + )) + } + + /// Get a script that rejects the promise with an error. + pub fn into_error_script(id: Value, result: Value) -> Result { + let retval = serde_json::to_string(&result)?; + Ok(format!( + "window.external.rpc._error({}, {})", + id.to_string(), + retval + )) + } } diff --git a/src/webview/win/file_drop.rs b/src/webview/win/file_drop.rs index 84c7392e2..4fce661ed 100644 --- a/src/webview/win/file_drop.rs +++ b/src/webview/win/file_drop.rs @@ -6,63 +6,63 @@ use crate::{FileDropEvent, FileDropHandler}; // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2experimentalcompositioncontroller3?view=webview2-1.0.721-prerelease&preserve-view=true use std::{ - ffi::OsString, - os::{raw::c_void, windows::ffi::OsStringExt}, - path::PathBuf, - ptr, - rc::Rc, - sync::atomic::{AtomicUsize, Ordering}, + ffi::OsString, + os::{raw::c_void, windows::ffi::OsStringExt}, + path::PathBuf, + ptr, + rc::Rc, + sync::atomic::{AtomicUsize, Ordering}, }; use winapi::shared::windef::HWND; pub(crate) struct FileDropController { - drop_targets: Vec<*mut IDropTarget>, + drop_targets: Vec<*mut IDropTarget>, } impl Drop for FileDropController { - fn drop(&mut self) { - // Safety: this could dereference a null ptr. - // This should never be a null ptr unless something goes wrong in Windows. - unsafe { - for ptr in &self.drop_targets { - Box::from_raw(*ptr); - } - } + fn drop(&mut self) { + // Safety: this could dereference a null ptr. + // This should never be a null ptr unless something goes wrong in Windows. + unsafe { + for ptr in &self.drop_targets { + Box::from_raw(*ptr); + } } + } } impl FileDropController { - pub(crate) fn new() -> Self { - FileDropController { - drop_targets: Vec::new(), - } + pub(crate) fn new() -> Self { + FileDropController { + drop_targets: Vec::new(), } - - pub(crate) fn listen(&mut self, hwnd: HWND, handler: FileDropHandler) { - let listener = Rc::new(handler); - - // Enumerate child windows to find the WebView2 "window" and override! - enumerate_child_windows(hwnd, |hwnd| self.inject(hwnd, listener.clone())); + } + + pub(crate) fn listen(&mut self, hwnd: HWND, handler: FileDropHandler) { + let listener = Rc::new(handler); + + // Enumerate child windows to find the WebView2 "window" and override! + enumerate_child_windows(hwnd, |hwnd| self.inject(hwnd, listener.clone())); + } + + fn inject(&mut self, hwnd: HWND, listener: Rc) -> bool { + // Safety: WinAPI calls are unsafe + unsafe { + let file_drop_handler = IDropTarget::new(hwnd, listener); + let handler_interface_ptr = + &mut (*file_drop_handler.data).interface as winapi::um::oleidl::LPDROPTARGET; + + if winapi::um::ole2::RevokeDragDrop(hwnd) != winapi::shared::winerror::DRAGDROP_E_INVALIDHWND + && winapi::um::ole2::RegisterDragDrop(hwnd, handler_interface_ptr) == S_OK + { + // Not a great solution. But there is no reliable way to get the window handle of the webview, for whatever reason... + self + .drop_targets + .push(Box::into_raw(Box::new(file_drop_handler))); + } } - fn inject(&mut self, hwnd: HWND, listener: Rc) -> bool { - // Safety: WinAPI calls are unsafe - unsafe { - let file_drop_handler = IDropTarget::new(hwnd, listener); - let handler_interface_ptr = - &mut (*file_drop_handler.data).interface as winapi::um::oleidl::LPDROPTARGET; - - if winapi::um::ole2::RevokeDragDrop(hwnd) - != winapi::shared::winerror::DRAGDROP_E_INVALIDHWND - && winapi::um::ole2::RegisterDragDrop(hwnd, handler_interface_ptr) == S_OK - { - // Not a great solution. But there is no reliable way to get the window handle of the webview, for whatever reason... - self.drop_targets - .push(Box::into_raw(Box::new(file_drop_handler))); - } - } - - true - } + true + } } // https://gist.github.com/application-developer-DA/5a460d9ca02948f1d2bfa53100c941da @@ -70,25 +70,25 @@ impl FileDropController { fn enumerate_child_windows(hwnd: HWND, mut callback: F) where - F: FnMut(HWND) -> bool, + F: FnMut(HWND) -> bool, { - let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback; - let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) }; + let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback; + let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) }; - let lparam = closure_pointer_pointer as winapi::shared::minwindef::LPARAM; - unsafe { winapi::um::winuser::EnumChildWindows(hwnd, Some(enumerate_callback), lparam) }; + let lparam = closure_pointer_pointer as winapi::shared::minwindef::LPARAM; + unsafe { winapi::um::winuser::EnumChildWindows(hwnd, Some(enumerate_callback), lparam) }; } unsafe extern "system" fn enumerate_callback( - hwnd: HWND, - lparam: winapi::shared::minwindef::LPARAM, + hwnd: HWND, + lparam: winapi::shared::minwindef::LPARAM, ) -> winapi::shared::minwindef::BOOL { - let closure: &mut &mut dyn FnMut(HWND) -> bool = std::mem::transmute(lparam as *mut c_void); - if closure(hwnd) { - winapi::shared::minwindef::TRUE - } else { - winapi::shared::minwindef::FALSE - } + let closure: &mut &mut dyn FnMut(HWND) -> bool = std::mem::transmute(lparam as *mut c_void); + if closure(hwnd) { + winapi::shared::minwindef::TRUE + } else { + winapi::shared::minwindef::FALSE + } } // The below code has been ripped from Winit - if only they'd `pub use` this! @@ -96,235 +96,233 @@ unsafe extern "system" fn enumerate_callback( // Safety: WinAPI calls are unsafe use winapi::{ - shared::{ - guiddef::REFIID, - minwindef::{DWORD, UINT, ULONG}, - windef::POINTL, - winerror::S_OK, - }, - um::{ - objidl::IDataObject, - oleidl::{ - IDropTarget as NativeIDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_NONE, - }, - shellapi, unknwnbase, - winnt::HRESULT, - }, + shared::{ + guiddef::REFIID, + minwindef::{DWORD, UINT, ULONG}, + windef::POINTL, + winerror::S_OK, + }, + um::{ + objidl::IDataObject, + oleidl::{IDropTarget as NativeIDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_NONE}, + shellapi, unknwnbase, + winnt::HRESULT, + }, }; #[allow(non_camel_case_types)] #[repr(C)] struct IDropTargetData { - pub interface: NativeIDropTarget, - listener: Rc, - refcount: AtomicUsize, - window: HWND, - cursor_effect: DWORD, - hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ + pub interface: NativeIDropTarget, + listener: Rc, + refcount: AtomicUsize, + window: HWND, + cursor_effect: DWORD, + hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ } #[allow(non_camel_case_types)] pub struct IDropTarget { - data: *mut IDropTargetData, + data: *mut IDropTargetData, } #[allow(non_snake_case)] impl IDropTarget { - fn new(window: HWND, listener: Rc) -> IDropTarget { - let data = Box::new(IDropTargetData { - listener, - interface: NativeIDropTarget { - lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl, - }, - refcount: AtomicUsize::new(1), - window, - cursor_effect: DROPEFFECT_NONE, - hovered_is_valid: false, - }); - IDropTarget { - data: Box::into_raw(data), - } - } - - // Implement IUnknown - pub unsafe extern "system" fn QueryInterface( - _this: *mut unknwnbase::IUnknown, - _riid: REFIID, - _ppvObject: *mut *mut c_void, - ) -> HRESULT { - // This function doesn't appear to be required for an `IDropTarget`. - // An implementation would be nice however. - unimplemented!(); - } - - pub unsafe extern "system" fn AddRef(this: *mut unknwnbase::IUnknown) -> ULONG { - let drop_handler_data = Self::from_interface(this); - let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; - count as ULONG - } - - pub unsafe extern "system" fn Release(this: *mut unknwnbase::IUnknown) -> ULONG { - let drop_handler = Self::from_interface(this); - let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; - if count == 0 { - // Destroy the underlying data - Box::from_raw(drop_handler as *mut IDropTargetData); - } - count as ULONG - } - - pub unsafe extern "system" fn DragEnter( - this: *mut NativeIDropTarget, - pDataObj: *const IDataObject, - _grfKeyState: DWORD, - _pt: *const POINTL, - pdwEffect: *mut DWORD, - ) -> HRESULT { - let mut paths = Vec::new(); - - let drop_handler = Self::from_interface(this); - let hdrop = Self::collect_paths(pDataObj, &mut paths); - drop_handler.hovered_is_valid = hdrop.is_some(); - drop_handler.cursor_effect = if drop_handler.hovered_is_valid { - DROPEFFECT_COPY - } else { - DROPEFFECT_NONE - }; - *pdwEffect = drop_handler.cursor_effect; - - (drop_handler.listener)(FileDropEvent::Hovered(paths)); - - S_OK - } - - pub unsafe extern "system" fn DragOver( - this: *mut NativeIDropTarget, - _grfKeyState: DWORD, - _pt: *const POINTL, - pdwEffect: *mut DWORD, - ) -> HRESULT { - let drop_handler = Self::from_interface(this); - *pdwEffect = drop_handler.cursor_effect; - - S_OK + fn new(window: HWND, listener: Rc) -> IDropTarget { + let data = Box::new(IDropTargetData { + listener, + interface: NativeIDropTarget { + lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl, + }, + refcount: AtomicUsize::new(1), + window, + cursor_effect: DROPEFFECT_NONE, + hovered_is_valid: false, + }); + IDropTarget { + data: Box::into_raw(data), } - - pub unsafe extern "system" fn DragLeave(this: *mut NativeIDropTarget) -> HRESULT { - let drop_handler = Self::from_interface(this); - if drop_handler.hovered_is_valid { - (drop_handler.listener)(FileDropEvent::Cancelled); - } - - S_OK + } + + // Implement IUnknown + pub unsafe extern "system" fn QueryInterface( + _this: *mut unknwnbase::IUnknown, + _riid: REFIID, + _ppvObject: *mut *mut c_void, + ) -> HRESULT { + // This function doesn't appear to be required for an `IDropTarget`. + // An implementation would be nice however. + unimplemented!(); + } + + pub unsafe extern "system" fn AddRef(this: *mut unknwnbase::IUnknown) -> ULONG { + let drop_handler_data = Self::from_interface(this); + let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; + count as ULONG + } + + pub unsafe extern "system" fn Release(this: *mut unknwnbase::IUnknown) -> ULONG { + let drop_handler = Self::from_interface(this); + let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + // Destroy the underlying data + Box::from_raw(drop_handler as *mut IDropTargetData); } - - pub unsafe extern "system" fn Drop( - this: *mut NativeIDropTarget, - pDataObj: *const IDataObject, - _grfKeyState: DWORD, - _pt: *const POINTL, - _pdwEffect: *mut DWORD, - ) -> HRESULT { - let mut paths = Vec::new(); - - let drop_handler = Self::from_interface(this); - let hdrop = Self::collect_paths(pDataObj, &mut paths); - if let Some(hdrop) = hdrop { - shellapi::DragFinish(hdrop); - } - - (drop_handler.listener)(FileDropEvent::Dropped(paths)); - - S_OK + count as ULONG + } + + pub unsafe extern "system" fn DragEnter( + this: *mut NativeIDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: DWORD, + _pt: *const POINTL, + pdwEffect: *mut DWORD, + ) -> HRESULT { + let mut paths = Vec::new(); + + let drop_handler = Self::from_interface(this); + let hdrop = Self::collect_paths(pDataObj, &mut paths); + drop_handler.hovered_is_valid = hdrop.is_some(); + drop_handler.cursor_effect = if drop_handler.hovered_is_valid { + DROPEFFECT_COPY + } else { + DROPEFFECT_NONE + }; + *pdwEffect = drop_handler.cursor_effect; + + (drop_handler.listener)(FileDropEvent::Hovered(paths)); + + S_OK + } + + pub unsafe extern "system" fn DragOver( + this: *mut NativeIDropTarget, + _grfKeyState: DWORD, + _pt: *const POINTL, + pdwEffect: *mut DWORD, + ) -> HRESULT { + let drop_handler = Self::from_interface(this); + *pdwEffect = drop_handler.cursor_effect; + + S_OK + } + + pub unsafe extern "system" fn DragLeave(this: *mut NativeIDropTarget) -> HRESULT { + let drop_handler = Self::from_interface(this); + if drop_handler.hovered_is_valid { + (drop_handler.listener)(FileDropEvent::Cancelled); } - unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut IDropTargetData { - &mut *(this as *mut _) + S_OK + } + + pub unsafe extern "system" fn Drop( + this: *mut NativeIDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: DWORD, + _pt: *const POINTL, + _pdwEffect: *mut DWORD, + ) -> HRESULT { + let mut paths = Vec::new(); + + let drop_handler = Self::from_interface(this); + let hdrop = Self::collect_paths(pDataObj, &mut paths); + if let Some(hdrop) = hdrop { + shellapi::DragFinish(hdrop); } - unsafe fn collect_paths( - data_obj: *const IDataObject, - paths: &mut Vec, - ) -> Option { - use winapi::{ - shared::{ - winerror::{DV_E_FORMATETC, SUCCEEDED}, - wtypes::{CLIPFORMAT, DVASPECT_CONTENT}, - }, - um::{ - objidl::{FORMATETC, TYMED_HGLOBAL}, - shellapi::DragQueryFileW, - winuser::CF_HDROP, - }, - }; - - let mut drop_format = FORMATETC { - cfFormat: CF_HDROP as CLIPFORMAT, - ptd: ptr::null(), - dwAspect: DVASPECT_CONTENT, - lindex: -1, - tymed: TYMED_HGLOBAL, - }; - - let mut medium = std::mem::zeroed(); - let get_data_result = (*data_obj).GetData(&mut drop_format, &mut medium); - if SUCCEEDED(get_data_result) { - let hglobal = (*medium.u).hGlobal(); - let hdrop = (*hglobal) as shellapi::HDROP; - - // The second parameter (0xFFFFFFFF) instructs the function to return the item count - let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, ptr::null_mut(), 0); - - for i in 0..item_count { - // Get the length of the path string NOT including the terminating null character. - // Previously, this was using a fixed size array of MAX_PATH length, but the - // Windows API allows longer paths under certain circumstances. - let character_count = DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize; - let str_len = character_count + 1; - - // Fill path_buf with the null-terminated file name - let mut path_buf = Vec::with_capacity(str_len); - DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as UINT); - path_buf.set_len(str_len); - - paths.push(OsString::from_wide(&path_buf[0..character_count]).into()); - } - - return Some(hdrop); - } else if get_data_result == DV_E_FORMATETC { - // If the dropped item is not a file this error will occur. - // In this case it is OK to return without taking further action. - debug_assert!( - false, - "Error occured while processing dropped/hovered item: item is not a file." - ); - return None; - } else { - debug_assert!( - false, - "Unexpected error occured while processing dropped/hovered item." - ); - return None; - } + (drop_handler.listener)(FileDropEvent::Dropped(paths)); + + S_OK + } + + unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut IDropTargetData { + &mut *(this as *mut _) + } + + unsafe fn collect_paths( + data_obj: *const IDataObject, + paths: &mut Vec, + ) -> Option { + use winapi::{ + shared::{ + winerror::{DV_E_FORMATETC, SUCCEEDED}, + wtypes::{CLIPFORMAT, DVASPECT_CONTENT}, + }, + um::{ + objidl::{FORMATETC, TYMED_HGLOBAL}, + shellapi::DragQueryFileW, + winuser::CF_HDROP, + }, + }; + + let mut drop_format = FORMATETC { + cfFormat: CF_HDROP as CLIPFORMAT, + ptd: ptr::null(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL, + }; + + let mut medium = std::mem::zeroed(); + let get_data_result = (*data_obj).GetData(&mut drop_format, &mut medium); + if SUCCEEDED(get_data_result) { + let hglobal = (*medium.u).hGlobal(); + let hdrop = (*hglobal) as shellapi::HDROP; + + // The second parameter (0xFFFFFFFF) instructs the function to return the item count + let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, ptr::null_mut(), 0); + + for i in 0..item_count { + // Get the length of the path string NOT including the terminating null character. + // Previously, this was using a fixed size array of MAX_PATH length, but the + // Windows API allows longer paths under certain circumstances. + let character_count = DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize; + let str_len = character_count + 1; + + // Fill path_buf with the null-terminated file name + let mut path_buf = Vec::with_capacity(str_len); + DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as UINT); + path_buf.set_len(str_len); + + paths.push(OsString::from_wide(&path_buf[0..character_count]).into()); + } + + return Some(hdrop); + } else if get_data_result == DV_E_FORMATETC { + // If the dropped item is not a file this error will occur. + // In this case it is OK to return without taking further action. + debug_assert!( + false, + "Error occured while processing dropped/hovered item: item is not a file." + ); + return None; + } else { + debug_assert!( + false, + "Unexpected error occured while processing dropped/hovered item." + ); + return None; } + } } impl Drop for IDropTarget { - fn drop(&mut self) { - unsafe { - IDropTarget::Release(self.data as *mut unknwnbase::IUnknown); - } + fn drop(&mut self) { + unsafe { + IDropTarget::Release(self.data as *mut unknwnbase::IUnknown); } + } } static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { - parent: unknwnbase::IUnknownVtbl { - QueryInterface: IDropTarget::QueryInterface, - AddRef: IDropTarget::AddRef, - Release: IDropTarget::Release, - }, - DragEnter: IDropTarget::DragEnter, - DragOver: IDropTarget::DragOver, - DragLeave: IDropTarget::DragLeave, - Drop: IDropTarget::Drop, + parent: unknwnbase::IUnknownVtbl { + QueryInterface: IDropTarget::QueryInterface, + AddRef: IDropTarget::AddRef, + Release: IDropTarget::Release, + }, + DragEnter: IDropTarget::DragEnter, + DragOver: IDropTarget::DragOver, + DragLeave: IDropTarget::DragLeave, + Drop: IDropTarget::Drop, }; diff --git a/src/webview/win/mod.rs b/src/webview/win/mod.rs index 93711c233..822bfe4f6 100644 --- a/src/webview/win/mod.rs +++ b/src/webview/win/mod.rs @@ -1,8 +1,9 @@ mod file_drop; -use crate::webview::mimetype::MimeType; -use crate::webview::WV; -use crate::{FileDropHandler, Result, RpcHandler}; +use crate::{ + webview::{mimetype::MimeType, WV}, + FileDropHandler, Result, RpcHandler, +}; use file_drop::FileDropController; @@ -15,198 +16,198 @@ use winapi::{shared::windef::HWND, um::winuser::GetClientRect}; use winit::{platform::windows::WindowExtWindows, window::Window}; pub struct InnerWebView { - controller: Rc>, + controller: Rc>, - // Store FileDropController in here to make sure it gets dropped when - // the webview gets dropped, otherwise we'll have a memory leak - #[allow(dead_code)] - file_drop_controller: Rc>, + // Store FileDropController in here to make sure it gets dropped when + // the webview gets dropped, otherwise we'll have a memory leak + #[allow(dead_code)] + file_drop_controller: Rc>, } impl WV for InnerWebView { - type Window = Window; - - fn new Result>>( - window: &Window, - scripts: Vec, - url: Option, - // TODO default background color option just adds to webview2 recently and it requires - // canary build. Implement this once it's in official release. - transparent: bool, - custom_protocol: Option<(String, F)>, - rpc_handler: Option, - file_drop_handler: Option, - ) -> Result { - let hwnd = window.hwnd() as HWND; - - let controller: Rc> = Rc::new(OnceCell::new()); - let controller_clone = controller.clone(); - - let file_drop_controller: Rc> = Rc::new(OnceCell::new()); - let file_drop_controller_clone = file_drop_controller.clone(); - - // Webview controller - webview2::EnvironmentBuilder::new().build(move |env| { - let env = env?; - let env_ = env.clone(); - env.create_controller(hwnd, move |controller| { - let controller = controller?; - let w = controller.get_webview()?; - - // Enable sensible defaults - let settings = w.get_settings()?; - settings.put_is_status_bar_enabled(false)?; - settings.put_are_default_context_menus_enabled(true)?; - settings.put_is_zoom_control_enabled(false)?; - settings.put_are_dev_tools_enabled(false)?; - debug_assert_eq!(settings.put_are_dev_tools_enabled(true)?, ()); - - // Safety: System calls are unsafe - unsafe { - let mut rect = std::mem::zeroed(); - GetClientRect(hwnd, &mut rect); - controller.put_bounds(rect)?; - } + type Window = Window; + + fn new Result>>( + window: &Window, + scripts: Vec, + url: Option, + // TODO default background color option just adds to webview2 recently and it requires + // canary build. Implement this once it's in official release. + transparent: bool, + custom_protocol: Option<(String, F)>, + rpc_handler: Option, + file_drop_handler: Option, + ) -> Result { + let hwnd = window.hwnd() as HWND; + + let controller: Rc> = Rc::new(OnceCell::new()); + let controller_clone = controller.clone(); + + let file_drop_controller: Rc> = Rc::new(OnceCell::new()); + let file_drop_controller_clone = file_drop_controller.clone(); + + // Webview controller + webview2::EnvironmentBuilder::new().build(move |env| { + let env = env?; + let env_ = env.clone(); + env.create_controller(hwnd, move |controller| { + let controller = controller?; + let w = controller.get_webview()?; + + // Enable sensible defaults + let settings = w.get_settings()?; + settings.put_is_status_bar_enabled(false)?; + settings.put_are_default_context_menus_enabled(true)?; + settings.put_is_zoom_control_enabled(false)?; + settings.put_are_dev_tools_enabled(false)?; + debug_assert_eq!(settings.put_are_dev_tools_enabled(true)?, ()); - // Initialize scripts - w.add_script_to_execute_on_document_created( - "window.external={invoke:s=>window.chrome.webview.postMessage(s)}", - |_| (Ok(())), - )?; - for js in scripts { - w.add_script_to_execute_on_document_created(&js, |_| (Ok(())))?; - } - - // Message handler - w.add_web_message_received(move |webview, args| { - let js = args.try_get_web_message_as_string()?; - if let Some(rpc_handler) = rpc_handler.as_ref() { - match super::rpc_proxy(js, rpc_handler) { - Ok(result) => { - if let Some(ref script) = result { - webview.execute_script(script, |_| (Ok(())))?; - } - } - Err(e) => { - eprintln!("{}", e); - } - } - } - Ok(()) - })?; - - let mut custom_protocol_name = None; - if let Some((name, function)) = custom_protocol { - // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround - // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 - custom_protocol_name = Some(name.clone()); - w.add_web_resource_requested_filter( - &format!("file://custom-protocol-{}*", name), - webview2::WebResourceContext::All, - )?; - w.add_web_resource_requested(move |_, args| { - let uri = args.get_request()?.get_uri()?; - // Undo the protocol workaround when giving path to resolver - let path = &uri.replace( - &format!("file://custom-protocol-{}", name), - &format!("{}://", name), - ); - - match function(path) { - Ok(content) => { - let mime = MimeType::parse(&content, &uri); - let stream = webview2::Stream::from_bytes(&content); - let response = env_.create_web_resource_response( - stream, - 200, - "OK", - &format!("Content-Type: {}", mime), - )?; - args.put_response(response)?; - Ok(()) - } - Err(_) => Err(webview2::Error::from(std::io::Error::new( - std::io::ErrorKind::Other, - "Error loading requested file", - ))), - } - })?; - } - - // Enable clipboard - w.add_permission_requested(|_, args| { - let kind = args.get_permission_kind()?; - if kind == PermissionKind::ClipboardRead { - args.put_state(PermissionState::Allow)?; - } - Ok(()) - })?; - - // Navigation - if let Some(url) = url { - if url.cannot_be_a_base() { - let s = url.as_str(); - if let Some(pos) = s.find(',') { - let (_, path) = s.split_at(pos + 1); - w.navigate_to_string(path)?; - } - } else { - let mut url_string = String::from(url.as_str()); - if let Some(name) = custom_protocol_name { - if name == url.scheme() { - // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround - // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 - url_string = url.as_str().replace( - &format!("{}://", name), - &format!("file://custom-protocol-{}", name), - ) - } - } - w.navigate(&url_string)?; - } - } + // Safety: System calls are unsafe + unsafe { + let mut rect = std::mem::zeroed(); + GetClientRect(hwnd, &mut rect); + controller.put_bounds(rect)?; + } - let _ = controller_clone.set(controller); + // Initialize scripts + w.add_script_to_execute_on_document_created( + "window.external={invoke:s=>window.chrome.webview.postMessage(s)}", + |_| (Ok(())), + )?; + for js in scripts { + w.add_script_to_execute_on_document_created(&js, |_| (Ok(())))?; + } - if let Some(file_drop_handler) = file_drop_handler { - let mut file_drop_controller = FileDropController::new(); - file_drop_controller.listen(hwnd, file_drop_handler); - let _ = file_drop_controller_clone.set(file_drop_controller); + // Message handler + w.add_web_message_received(move |webview, args| { + let js = args.try_get_web_message_as_string()?; + if let Some(rpc_handler) = rpc_handler.as_ref() { + match super::rpc_proxy(js, rpc_handler) { + Ok(result) => { + if let Some(ref script) = result { + webview.execute_script(script, |_| (Ok(())))?; } + } + Err(e) => { + eprintln!("{}", e); + } + } + } + Ok(()) + })?; + let mut custom_protocol_name = None; + if let Some((name, function)) = custom_protocol { + // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround + // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 + custom_protocol_name = Some(name.clone()); + w.add_web_resource_requested_filter( + &format!("file://custom-protocol-{}*", name), + webview2::WebResourceContext::All, + )?; + w.add_web_resource_requested(move |_, args| { + let uri = args.get_request()?.get_uri()?; + // Undo the protocol workaround when giving path to resolver + let path = &uri.replace( + &format!("file://custom-protocol-{}", name), + &format!("{}://", name), + ); + + match function(path) { + Ok(content) => { + let mime = MimeType::parse(&content, &uri); + let stream = webview2::Stream::from_bytes(&content); + let response = env_.create_web_resource_response( + stream, + 200, + "OK", + &format!("Content-Type: {}", mime), + )?; + args.put_response(response)?; Ok(()) - }) + } + Err(_) => Err(webview2::Error::from(std::io::Error::new( + std::io::ErrorKind::Other, + "Error loading requested file", + ))), + } + })?; + } + + // Enable clipboard + w.add_permission_requested(|_, args| { + let kind = args.get_permission_kind()?; + if kind == PermissionKind::ClipboardRead { + args.put_state(PermissionState::Allow)?; + } + Ok(()) })?; - Ok(Self { - controller, + // Navigation + if let Some(url) = url { + if url.cannot_be_a_base() { + let s = url.as_str(); + if let Some(pos) = s.find(',') { + let (_, path) = s.split_at(pos + 1); + w.navigate_to_string(path)?; + } + } else { + let mut url_string = String::from(url.as_str()); + if let Some(name) = custom_protocol_name { + if name == url.scheme() { + // WebView2 doesn't support non-standard protocols yet, so we have to use this workaround + // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/73 + url_string = url.as_str().replace( + &format!("{}://", name), + &format!("file://custom-protocol-{}", name), + ) + } + } + w.navigate(&url_string)?; + } + } - file_drop_controller, - }) - } + let _ = controller_clone.set(controller); - fn eval(&self, js: &str) -> Result<()> { - if let Some(c) = self.controller.get() { - let webview = c.get_webview()?; - webview.execute_script(js, |_| (Ok(())))?; + if let Some(file_drop_handler) = file_drop_handler { + let mut file_drop_controller = FileDropController::new(); + file_drop_controller.listen(hwnd, file_drop_handler); + let _ = file_drop_controller_clone.set(file_drop_controller); } + Ok(()) + }) + })?; + + Ok(Self { + controller, + + file_drop_controller, + }) + } + + fn eval(&self, js: &str) -> Result<()> { + if let Some(c) = self.controller.get() { + let webview = c.get_webview()?; + webview.execute_script(js, |_| (Ok(())))?; } + Ok(()) + } } impl InnerWebView { - pub fn resize(&self, hwnd: *mut c_void) -> Result<()> { - let hwnd = hwnd as HWND; - - // Safety: System calls are unsafe - unsafe { - let mut rect = std::mem::zeroed(); - GetClientRect(hwnd, &mut rect); - if let Some(c) = self.controller.get() { - c.put_bounds(rect)?; - } - } - - Ok(()) + pub fn resize(&self, hwnd: *mut c_void) -> Result<()> { + let hwnd = hwnd as HWND; + + // Safety: System calls are unsafe + unsafe { + let mut rect = std::mem::zeroed(); + GetClientRect(hwnd, &mut rect); + if let Some(c) = self.controller.get() { + c.put_bounds(rect)?; + } } + + Ok(()) + } }