meta: toolchains & templates (#110)

* chore(tools): add rustfmt / toolchain

Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com>

* chore(.github)

Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com>

* chore(workflows): audit, clippy, fmt, udeps

Signed-off-by: Daniel Thompson-Yvetot <denjell@mailscript.com>

* Remove some files for simplicity

* cargo fmt

* cargo clippy

* Remove clippy ci check

There are several rules violate clippy but we use it for debug and compatibility for system api. So we have to remove it.

Co-authored-by: Ngo Iok Ui <wusyong9104@gmail.com>
This commit is contained in:
nothingismagick
2021-03-09 19:14:54 +01:00
committed by GitHub
parent d416033e08
commit fae4bbc7ab
32 changed files with 2848 additions and 2671 deletions

14
.github/CODEOWNERS vendored Normal file
View File

@@ -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

13
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -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/)

44
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -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, dont 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.

8
.github/FUNDING.yml vendored Normal file
View File

@@ -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

36
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,36 @@
<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/tauri-apps/tauri/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->
<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->
**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)
<!--
If yes, please describe the impact and migration path for existing applications in an attached issue. Filing a PR with breaking changes that has not been discussed and approved by the maintainers in an issue will be immediately closed.
-->
- [ ] 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:**

20
.github/workflows/audit.yml vendored Normal file
View File

@@ -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 }}

24
.github/workflows/fmt.yml vendored Normal file
View File

@@ -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

19
SECURITY.md Normal file
View File

@@ -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 DONT create a pull-request, DONT
file an issue on GitHub, DONT mention it on Discord and DONT 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.

View File

@@ -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")]

View File

@@ -9,20 +9,20 @@ Dropping files onto the following form is also possible:<br><br>
"#;
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(())
}

View File

@@ -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:

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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::<String>();
let handler = Box::new(move |_: WindowProxy, req: RpcRequest| {
if &req.method == "openWindow" {
if let Some(params) = req.params {
if let Value::String(url) = &params[0] {
let _ = window_tx.send(url.to_string());
}
}
let (window_tx, window_rx) = std::sync::mpsc::channel::<String>();
let handler = Box::new(move |_: WindowProxy, req: RpcRequest| {
if &req.method == "openWindow" {
if let Some(params) = req.params {
if let Value::String(url) = &params[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(())
}

View File

@@ -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#"
<script>
let fullscreen = false;
async function toggleFullScreen() {
@@ -31,47 +30,45 @@ async function getAsyncRpcResult() {
<div id="rpc-result"></div>
"#;
let attributes = Attributes {
url: Some(format!("data:text/html,{}", html)),
..Default::default()
};
let attributes = Attributes {
url: Some(format!("data:text/html,{}", html)),
..Default::default()
};
let handler = Box::new(|proxy: WindowProxy, mut req: RpcRequest| {
let mut response = None;
if &req.method == "fullscreen" {
if let Some(params) = req.params.take() {
if let Some(mut args) = serde_json::from_value::<Vec<bool>>(params).ok() {
if args.len() > 0 {
let flag = args.swap_remove(0);
// NOTE: in the real world we need to reply with an error
let _ = proxy.set_fullscreen(flag);
};
response = Some(RpcResponse::new_result(req.id.take(), None));
}
}
} else if &req.method == "send-parameters" {
if let Some(params) = req.params.take() {
if let Some(mut args) =
serde_json::from_value::<Vec<MessageParameters>>(params).ok()
{
let result = if args.len() > 0 {
let msg = args.swap_remove(0);
Some(Value::String(format!("Hello, {}!", msg.message)))
} else {
// NOTE: in the real-world we should send an error response here!
None
};
// Must always send a response as this is a `call()`
response = Some(RpcResponse::new_result(req.id.take(), result));
}
}
let handler = Box::new(|proxy: WindowProxy, mut req: RpcRequest| {
let mut response = None;
if &req.method == "fullscreen" {
if let Some(params) = req.params.take() {
if let Some(mut args) = serde_json::from_value::<Vec<bool>>(params).ok() {
if args.len() > 0 {
let flag = args.swap_remove(0);
// NOTE: in the real world we need to reply with an error
let _ = proxy.set_fullscreen(flag);
};
response = Some(RpcResponse::new_result(req.id.take(), None));
}
}
} else if &req.method == "send-parameters" {
if let Some(params) = req.params.take() {
if let Some(mut args) = serde_json::from_value::<Vec<MessageParameters>>(params).ok() {
let result = if args.len() > 0 {
let msg = args.swap_remove(0);
Some(Value::String(format!("Hello, {}!", msg.message)))
} else {
// NOTE: in the real-world we should send an error response here!
None
};
// Must always send a response as this is a `call()`
response = Some(RpcResponse::new_result(req.id.take(), result));
}
}
}
response
});
response
});
app.add_window_with_configs(attributes, Some(handler), None, None)?;
app.add_window_with_configs(attributes, Some(handler), None, None)?;
app.run();
Ok(())
app.run();
Ok(())
}

View File

@@ -1,14 +1,13 @@
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 {
decorations: false,
transparent: true,
url: Some(
r#"data:text/html,
let attributes = Attributes {
decorations: false,
transparent: true,
url: Some(
r#"data:text/html,
<!doctype html>
<html>
<body style="background-color:rgba(87,87,87,0.);">hello</body>
@@ -18,12 +17,12 @@ fn main() -> Result<()> {
};
</script>
</html>"#
.to_string(),
),
..Default::default()
};
.to_string(),
),
..Default::default()
};
app.add_window(attributes)?;
app.run();
Ok(())
app.add_window(attributes)?;
app.run();
Ok(())
}

View File

@@ -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(_) => {}
_ => (),
}
});
}

14
rustfmt.toml Normal file
View File

@@ -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"

View File

@@ -65,213 +65,213 @@ use std::{fs::read, path::Path};
pub type WindowRpcHandler = Box<dyn Fn(WindowProxy, RpcRequest) -> Option<RpcResponse> + Send>;
pub struct CustomProtocol {
pub name: String,
pub handler: Box<dyn Fn(&str) -> Result<Vec<u8>> + Send>,
pub name: String,
pub handler: Box<dyn Fn(&str) -> Result<Vec<u8>> + 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<u8>);
impl Icon {
/// Creates an icon from the file.
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
Ok(Self(read(path)?))
}
/// Creates an icon from raw bytes.
pub fn from_bytes<B: Into<Vec<u8>>>(bytes: B) -> Result<Self> {
Ok(Self(bytes.into()))
}
/// Creates an icon from the file.
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
Ok(Self(read(path)?))
}
/// Creates an icon from raw bytes.
pub fn from_bytes<B: Into<Vec<u8>>>(bytes: B) -> Result<Self> {
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<f64>,
/// The minimum width of the window.
///
/// The default is `None`.
pub min_width: Option<f64>,
/// The minimum height of the window.
///
/// The default is `None`.
pub min_height: Option<f64>,
/// The minimum height of the window.
///
/// The default is `None`.
pub min_height: Option<f64>,
/// The maximum width of the window.
///
/// The default is `None`.
pub max_width: Option<f64>,
/// The maximum width of the window.
///
/// The default is `None`.
pub max_width: Option<f64>,
/// The maximum height of the window.
///
/// The default is `None`.
pub max_height: Option<f64>,
/// The maximum height of the window.
///
/// The default is `None`.
pub max_height: Option<f64>,
/// The horizontal position of the window's top left corner.
///
/// The default is `None`.
pub x: Option<f64>,
/// The horizontal position of the window's top left corner.
///
/// The default is `None`.
pub x: Option<f64>,
/// The vertical position of the window's top left corner.
///
/// The default is `None`.
pub y: Option<f64>,
/// The vertical position of the window's top left corner.
///
/// The default is `None`.
pub y: Option<f64>,
/// 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<Icon>,
/// The window icon.
///
/// The default is `None`.
pub icon: Option<Icon>,
/// 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<String>,
/// The URL to be loaded in the webview window.
///
/// The default is `None`.
pub url: Option<String>,
/// Javascript Code to be initialized when loading new pages.
///
/// The default is an empty vector.
pub initialization_scripts: Vec<String>,
/// Javascript Code to be initialized when loading new pages.
///
/// The default is an empty vector.
pub initialization_scripts: Vec<String>,
}
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<f64>,
pub min_height: Option<f64>,
pub max_width: Option<f64>,
pub max_height: Option<f64>,
pub x: Option<f64>,
pub y: Option<f64>,
pub fullscreen: bool,
pub icon: Option<Icon>,
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<f64>,
pub min_height: Option<f64>,
pub max_width: Option<f64>,
pub max_height: Option<f64>,
pub x: Option<f64>,
pub y: Option<f64>,
pub fullscreen: bool,
pub icon: Option<Icon>,
pub skip_taskbar: bool,
}
pub(crate) struct InnerWebViewAttributes {
pub transparent: bool,
pub url: Option<String>,
pub initialization_scripts: Vec<String>,
pub transparent: bool,
pub url: Option<String>,
pub initialization_scripts: Vec<String>,
}

View File

@@ -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<Message>;
#[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 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<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<WindowId> {
let (sender, receiver) = channel();
self.send_message(Message::NewWindow(
fn add_window(
&self,
attributes: Attributes,
file_drop_handler: Option<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<WindowId> {
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,
};
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<WindowId, WebView>,
event_loop: EventLoop<Message>,
event_loop_proxy: EventLoopProxy,
}
impl App for InnerApplication {
type Id = WindowId;
type Proxy = InnerApplicationProxy;
fn new() -> Result<Self> {
let event_loop = EventLoop::<Message>::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<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<Self::Id> {
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 run(self) {
let proxy = self.application_proxy();
let mut windows = self.webviews;
self.event_loop.run(move |event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
for (_, w) in windows.iter() {
w.evaluate_script().unwrap();
}
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,
))?;
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())
) => {
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);
}
}
}
_ => 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()
}
}
}
pub struct InnerApplication {
webviews: HashMap<WindowId, WebView>,
event_loop: EventLoop<Message>,
event_loop_proxy: EventLoopProxy,
}
impl App for InnerApplication {
type Id = WindowId;
type Proxy = InnerApplicationProxy;
fn new() -> Result<Self> {
let event_loop = EventLoop::<Message>::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<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<Self::Id> {
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 run(self) {
let proxy = self.application_proxy();
let mut windows = self.webviews;
self.event_loop.run(move |event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
for (_, w) in windows.iter() {
w.evaluate_script().unwrap();
}
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);
}
}
}
}
},
_ => (),
}
});
}
}
},
_ => (),
}
});
}
}
fn load_icon(icon: Icon) -> crate::Result<WinitIcon> {
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<Message>,
attributes: InnerWindowAttributes,
event_loop: &EventLoopWindowTarget<Message>,
attributes: InnerWindowAttributes,
) -> Result<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)?));
}
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);
}
#[cfg(target_os = "windows")]
if attributes.skip_taskbar {
skip_taskbar(&window);
}
Ok(window)
Ok(window)
}
fn _create_webview(
proxy: InnerApplicationProxy,
window: Window,
custom_protocol: Option<CustomProtocol>,
rpc_handler: Option<WindowRpcHandler>,
file_drop_handler: Option<FileDropHandler>,
proxy: InnerApplicationProxy,
window: Window,
custom_protocol: Option<CustomProtocol>,
rpc_handler: Option<WindowRpcHandler>,
file_drop_handler: Option<FileDropHandler>,
attributes: InnerWebViewAttributes,
attributes: InnerWebViewAttributes,
) -> Result<WebView> {
let window_id = window.id();
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);
}
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(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)
}));
}
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)
}

View File

@@ -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<Message>);
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 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<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<WindowId> {
let (sender, receiver): (Sender<WindowId>, Receiver<WindowId>) = channel();
self.send_message(Message::NewWindow(
attributes,
sender,
file_drop_handler,
rpc_handler,
custom_protocol,
))?;
Ok(receiver.recv()?)
}
}
pub struct InnerApplication {
webviews: HashMap<u32, WebView>,
app: GtkApp,
event_loop_proxy: EventLoopProxy,
event_loop_proxy_rx: Receiver<Message>,
}
impl App for InnerApplication {
type Id = u32;
type Proxy = InnerApplicationProxy;
fn new() -> Result<Self> {
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<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<Self::Id> {
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 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 add_window(
&self,
attributes: Attributes,
file_drop_handler: Option<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<WindowId> {
let (sender, receiver): (Sender<WindowId>, Receiver<WindowId>) = channel();
self.send_message(Message::NewWindow(
loop {
{
let webviews = shared_webviews.borrow_mut();
if webviews.is_empty() {
break;
}
for (_, w) in webviews.iter() {
let _ = w.evaluate_script();
}
}
while let Ok(message) = self.event_loop_proxy_rx.try_recv() {
match message {
Message::NewWindow(
attributes,
sender,
file_drop_handler,
rpc_handler,
custom_protocol,
))?;
Ok(receiver.recv()?)
}
}
pub struct InnerApplication {
webviews: HashMap<u32, WebView>,
app: GtkApp,
event_loop_proxy: EventLoopProxy,
event_loop_proxy_rx: Receiver<Message>,
}
impl App for InnerApplication {
type Id = u32;
type Proxy = InnerApplicationProxy;
fn new() -> Result<Self> {
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<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<Self::Id> {
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 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)
});
}
}
loop {
{
let webviews = shared_webviews.borrow_mut();
if webviews.is_empty() {
break;
) => {
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::<ApplicationWindow>(
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::<ApplicationWindow>(
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::<ApplicationWindow>(
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::<ApplicationWindow>(
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<gdk_pixbuf::Pixbuf> {
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<ApplicationWindow> {
let window = ApplicationWindow::new(app);
let window = ApplicationWindow::new(app);
window.set_geometry_hints::<ApplicationWindow>(
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);
window.set_geometry_hints::<ApplicationWindow>(
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);
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));
}
}
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.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);
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),
_ => {}
}
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)?));
}
if attributes.fullscreen {
window.fullscreen();
}
if let Some(icon) = attributes.icon {
window.set_icon(Some(&load_icon(icon)?));
}
Ok(window)
Ok(window)
}
fn _create_webview(
proxy: InnerApplicationProxy,
window: ApplicationWindow,
custom_protocol: Option<CustomProtocol>,
rpc_handler: Option<WindowRpcHandler>,
file_drop_handler: Option<FileDropHandler>,
proxy: InnerApplicationProxy,
window: ApplicationWindow,
custom_protocol: Option<CustomProtocol>,
rpc_handler: Option<WindowRpcHandler>,
file_drop_handler: Option<FileDropHandler>,
attributes: InnerWebViewAttributes,
attributes: InnerWebViewAttributes,
) -> Result<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);
}
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);
}
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)
}));
}
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);
let webview = webview.build()?;
Ok(webview)
let webview = webview.build()?;
Ok(webview)
}

View File

@@ -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<WindowId>,
Option<FileDropHandler>,
Option<WindowRpcHandler>,
Option<CustomProtocol>,
),
Window(WindowId, WindowMessage),
NewWindow(
Attributes,
Sender<WindowId>,
Option<FileDropHandler>,
Option<WindowRpcHandler>,
Option<CustomProtocol>,
),
}
/// 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<WindowProxy> {
let id = self.inner.add_window(attributes, None, None, None)?;
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<WindowProxy> {
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<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<WindowProxy> {
let id =
self.inner
.add_window(attributes, file_drop_handler, rpc_handler, custom_protocol)?;
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<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<WindowProxy> {
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<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<WindowId>;
fn send_message(&self, message: Message) -> Result<()>;
fn add_window(
&self,
attributes: Attributes,
file_drop_handler: Option<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<WindowId>;
}
/// 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 }
}
fn new(proxy: ApplicationProxy, id: WindowId) -> Self {
Self { proxy, id }
}
/// Gets the id of the WebView window.
pub fn id(&self) -> WindowId {
self.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 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_resizable(&self, resizable: bool) -> Result<()> {
self.proxy.send_message(Message::Window(
self.id,
WindowMessage::SetResizable(resizable),
))
}
pub fn set_title<S: Into<String>>(&self, title: S) -> Result<()> {
self.proxy.send_message(Message::Window(
self.id,
WindowMessage::SetTitle(title.into()),
))
}
pub fn set_title<S: Into<String>>(&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 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 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 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 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 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 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_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_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_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 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 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_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_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_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_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_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_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 set_icon(&self, icon: Icon) -> Result<()> {
self
.proxy
.send_message(Message::Window(self.id, WindowMessage::SetIcon(icon)))
}
pub fn evaluate_script<S: Into<String>>(&self, script: S) -> Result<()> {
self.proxy.send_message(Message::Window(
self.id,
WindowMessage::EvaluationScript(script.into()),
))
}
pub fn evaluate_script<S: Into<String>>(&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<Self> {
Ok(Self {
inner: InnerApplication::new()?,
//rpc_handler: None,
})
}
/// 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<Self> {
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<WindowProxy> {
let id = self.inner.create_webview(attributes, None, None, None)?;
Ok(self.window_proxy(id))
}
/// 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<WindowProxy> {
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<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<WindowProxy> {
let id = self.inner.create_webview(
attributes,
file_drop_handler,
rpc_handler,
custom_protocol,
)?;
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<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<WindowProxy> {
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 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)
}
/// 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()
}
/// 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<Self>;
fn new() -> Result<Self>;
fn create_webview(
&mut self,
attributes: Attributes,
file_drop_handler: Option<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<Self::Id>;
fn create_webview(
&mut self,
attributes: Attributes,
file_drop_handler: Option<FileDropHandler>,
rpc_handler: Option<WindowRpcHandler>,
custom_protocol: Option<CustomProtocol>,
) -> Result<Self::Id>;
fn application_proxy(&self) -> Self::Proxy;
fn application_proxy(&self) -> Self::Proxy;
fn run(self);
fn run(self);
}

View File

@@ -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<PathBuf>),
/// The file(s) have been dropped onto the window.
Dropped(Vec<PathBuf>),
/// The file drop was aborted.
Cancelled,
/// The file(s) have been dragged onto the window, but have not been dropped yet.
Hovered(Vec<PathBuf>),
/// The file(s) have been dropped onto the window.
Dropped(Vec<PathBuf>),
/// The file drop was aborted.
Cancelled,
}
/// Initializes a new file drop handler.

View File

@@ -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<T> = std::result::Result<T, Error>;
/// 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<String>),
#[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<String>),
#[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),
}

View File

@@ -6,49 +6,49 @@ use gtk::WidgetExt;
use webkit2gtk::WebView;
pub(crate) fn connect_drag_event(webview: Rc<WebView>, handler: FileDropHandler) {
let listener = Rc::new((handler, Cell::new(None)));
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::<Vec<PathBuf>>();
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::<Vec<PathBuf>>();
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
}
});
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_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
}
});
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)));
// 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)));
}

View File

@@ -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>,
webview: Rc<WebView>,
}
impl WV for InnerWebView {
type Window = Window;
type Window = Window;
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Window,
scripts: Vec<String>,
url: Option<Url>,
transparent: bool,
custom_protocol: Option<(String, F)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self> {
// Webview widget
let manager = UserContentManager::new();
let context = WebContext::new();
let webview = Rc::new(WebView::new_with_context_and_user_content_manager(
&context, &manager,
));
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Window,
scripts: Vec<String>,
url: Option<Url>,
transparent: bool,
custom_protocol: Option<(String, F)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self> {
// 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);
}
}
}
// 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();
// File drop handling
if let Some(file_drop_handler) = file_drop_handler {
file_drop::connect_drag_event(webview.clone(), file_drop_handler);
}
// 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);
if window.get_visible() {
window.show_all();
}
// Enable App cache
settings.set_enable_offline_web_application_cache(true);
settings.set_enable_page_cache(true);
let w = Self { webview };
// Enable Smooth scrooling
settings.set_enable_smooth_scrolling(true);
// Initialize scripts
w.init("window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}}")?;
for js in scripts {
w.init(&js)?;
}
// 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.",
));
}
});
}
// Navigation
if let Some(url) = url {
w.webview.load_uri(url.as_str());
}
Ok(w)
debug_assert_eq!(
{
settings.set_enable_write_console_messages_to_stdout(true);
settings.set_enable_developer_extras(true);
},
()
);
}
fn eval(&self, js: &str) -> Result<()> {
let cancellable: Option<&Cancellable> = None;
self.webview.run_javascript(js, cancellable, |_| ());
Ok(())
// Transparent
if transparent {
webview.set_background_color(&RGBA {
red: 0.,
green: 0.,
blue: 0.,
alpha: 0.,
});
}
// File drop handling
if let Some(file_drop_handler) = file_drop_handler {
file_drop::connect_drag_event(webview.clone(), file_drop_handler);
}
if window.get_visible() {
window.show_all();
}
let w = Self { webview };
// Initialize scripts
w.init("window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}}")?;
for js in scripts {
w.init(&js)?;
}
// 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.",
));
}
});
}
// 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(())
}
}

View File

@@ -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<extern "C" fn(*const Object, Sel, id) -> NSDragOperation> =
Lazy::new(|| unsafe {
std::mem::transmute(method_getImplementation(class_getInstanceMethod(
class!(WKWebView),
sel!(draggingEntered:),
)))
});
Lazy::new(|| unsafe {
std::mem::transmute(method_getImplementation(class_getInstanceMethod(
class!(WKWebView),
sel!(draggingEntered:),
)))
});
static OBJC_DRAGGING_EXITED: Lazy<extern "C" fn(*const Object, Sel, id)> = Lazy::new(|| unsafe {
std::mem::transmute(method_getImplementation(class_getInstanceMethod(
class!(WKWebView),
sel!(draggingExited:),
)))
std::mem::transmute(method_getImplementation(class_getInstanceMethod(
class!(WKWebView),
sel!(draggingExited:),
)))
});
static OBJC_PERFORM_DRAG_OPERATION: Lazy<extern "C" fn(*const Object, Sel, id) -> 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<extern "C" fn(*const Object, Sel, id) -> 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<PathBuf> {
use cocoa::foundation::NSFastEnumeration;
use cocoa::foundation::NSString;
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
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 <input type="file">, 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 <input type="file">, 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) };
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
}
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) };
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
}
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_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!(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!(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!(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_method(
sel!(draggingExited:),
dragging_exited as extern "C" fn(&mut Object, Sel, id),
);
}

View File

@@ -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<Object>,
manager: id,
webview: Id<Object>,
manager: id,
}
impl WV for InnerWebView {
type Window = Window;
type Window = Window;
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Window,
scripts: Vec<String>,
url: Option<Url>,
transparent: bool,
custom_protocol: Option<(String, F)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self> {
// 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");
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Window,
scripts: Vec<String>,
url: Option<Url>,
transparent: bool,
custom_protocol: Option<(String, F)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self> {
// 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);
}
}
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<dyn Fn(&str) -> Result<Vec<u8>>> =
std::mem::transmute(*function);
// 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<dyn Fn(&str) -> Result<Vec<u8>>> = 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();
// 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)
// 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::<c_void>()];
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) {}
}
}
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<Box<dyn Fn(&str) -> Result<Vec<u8>>>> =
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];
},
()
// 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<Box<dyn Fn(&str) -> Result<Vec<u8>>>> = Box::new(Box::new(function));
if transparent {
// Equivalent Obj-C:
// [config setValue:@NO forKey:@"drawsBackground"];
let _: id = msg_send![config, setValue:no forKey:NSString::new("drawsBackground")];
}
(*handler).set_ivar("function", Box::into_raw(function) as *mut _ as *mut c_void);
let () = msg_send![config, setURLSchemeHandler:handler forURLScheme:NSString::new(&name)];
}
// 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);
// 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];
// 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<RpcHandler> = Box::new(rpc_handler);
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];
},
()
);
(*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];
}
if transparent {
// Equivalent Obj-C:
// [config setValue:@NO forKey:@"drawsBackground"];
let _: id = msg_send![config, setValue:no forKey:NSString::new("drawsBackground")];
}
// File drop handling
if let Some(file_drop_handler) = file_drop_handler {
set_file_drop_handler(webview, file_drop_handler)
};
// 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);
let w = Self {
webview: Id::from_ptr(webview),
manager,
};
// 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<RpcHandler> = Box::new(rpc_handler);
// Initialize scripts
w.init(
r#"window.external = {
(*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);
}
);
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);
Ok(w)
// 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());
}
}
}
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(())
let view = window.ns_view() as id;
view.addSubview_(webview);
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(())
}
}
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<Object>);
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 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 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()
}
}
}

View File

@@ -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<u8>, 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<u8>, 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();
}
mime.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",
)
#[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 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 js: String =
MimeType::parse_from_uri("https://unpkg.com/react@17.0.1/umd/react.production.min.js")
.to_string();
assert_eq!(css, "text/css".to_string());
assert_eq!(js, "text/javascript".to_string());
let csv: String = MimeType::parse_from_uri("https://example.com/random.csv").to_string();
assert_eq!(csv, "text/csv".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 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 jsonld: String = MimeType::parse_from_uri("https:/example.com/hello.jsonld").to_string();
assert_eq!(jsonld, String::from("application/ld+json"));
let html: String = MimeType::parse_from_uri("https://tauri.studio/index.html").to_string();
assert_eq!(html, String::from("text/html"));
let rtf: String = MimeType::parse_from_uri("https://example.com/document.rtf").to_string();
assert_eq!(rtf, String::from("application/rtf"));
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 svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string();
assert_eq!(svg, String::from("image/svg"));
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 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 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"));
}
}

View File

@@ -45,29 +45,29 @@ pub type RpcHandler = Box<dyn Fn(RpcRequest) -> Option<RpcResponse> + Send>;
// Helper so all platforms handle RPC messages consistently.
fn rpc_proxy(js: String, handler: &RpcHandler) -> Result<Option<String>> {
let req = serde_json::from_str::<RpcRequest>(&js)
.map_err(|e| Error::RpcScriptError(e.to_string(), js))?;
let req = serde_json::from_str::<RpcRequest>(&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 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<Option<String>> {
/// 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<String>,
rx: Receiver<String>,
initialization_scripts: Vec<String>,
window: Window,
url: Option<Url>,
custom_protocol: Option<(String, Box<dyn Fn(&str) -> Result<Vec<u8>>>)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
transparent: bool,
tx: Sender<String>,
rx: Receiver<String>,
initialization_scripts: Vec<String>,
window: Window,
url: Option<Url>,
custom_protocol: Option<(String, Box<dyn Fn(&str) -> Result<Vec<u8>>>)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
}
impl WebViewBuilder {
/// Create [`WebViewBuilder`] from provided [`Window`].
pub fn new(window: Window) -> Result<Self> {
let (tx, rx) = channel();
/// Create [`WebViewBuilder`] from provided [`Window`].
pub fn new(window: Window) -> Result<Self> {
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,
})
}
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
}
/// 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
}
/// 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())
}
/// 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<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(&str) -> Result<Vec<u8>> + 'static,
{
self.custom_protocol = Some((name, Box::new(handler)));
self
}
/// Register custom file loading protocol
pub fn register_protocol<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(&str) -> Result<Vec<u8>> + '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#"
/// 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
}
self.initialization_scripts.push(js.to_string());
self.rpc_handler = Some(handler);
self
}
pub fn set_file_drop_handler(mut self, handler: Option<FileDropHandler>) -> Self {
self.file_drop_handler = handler;
self
}
pub fn set_file_drop_handler(mut self, handler: Option<FileDropHandler>) -> 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> {
self.url = Some(Url::parse(url)?);
Ok(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> {
self.url = Some(Url::parse(url)?);
Ok(self)
}
/// Consume the builder and create the [`WebView`].
pub fn build(self) -> Result<WebView> {
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,
})
}
/// Consume the builder and create the [`WebView`].
pub fn build(self) -> Result<WebView> {
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<String>,
rx: Receiver<String>,
window: Window,
webview: InnerWebView,
tx: Sender<String>,
rx: Receiver<String>,
}
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> {
Self::new_with_configs(window, false)
/// 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> {
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<Self> {
let picky_none: Option<(String, Box<dyn Fn(&str) -> Result<Vec<u8>>>)> = 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)?;
}
/// 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<Self> {
let picky_none: Option<(String, Box<dyn Fn(&str) -> Result<Vec<u8>>>)> = None;
Ok(())
}
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)?;
}
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<String>);
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;
type Window;
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Self::Window,
scripts: Vec<String>,
url: Option<Url>,
transparent: bool,
custom_protocol: Option<(String, F)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self>;
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Self::Window,
scripts: Vec<String>,
url: Option<Url>,
transparent: bool,
custom_protocol: Option<(String, F)>,
rpc_handler: Option<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self>;
fn eval(&self, js: &str) -> 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<Value>,
pub method: String,
pub params: Option<Value>,
jsonrpc: String,
pub id: Option<Value>,
pub method: String,
pub params: Option<Value>,
}
/// RPC response message which being sent back to the Javascript side.
#[derive(Debug, Serialize, Deserialize)]
pub struct RpcResponse {
jsonrpc: String,
pub(crate) id: Option<Value>,
pub(crate) result: Option<Value>,
pub(crate) error: Option<Value>,
jsonrpc: String,
pub(crate) id: Option<Value>,
pub(crate) result: Option<Value>,
pub(crate) error: Option<Value>,
}
impl RpcResponse {
/// Create a new result response.
pub fn new_result(id: Option<Value>, result: Option<Value>) -> Self {
Self {
jsonrpc: RPC_VERSION.to_string(),
id,
result,
error: None,
}
/// Create a new result response.
pub fn new_result(id: Option<Value>, result: Option<Value>) -> Self {
Self {
jsonrpc: RPC_VERSION.to_string(),
id,
result,
error: None,
}
}
/// Create a new error response.
pub fn new_error(id: Option<Value>, error: Option<Value>) -> Self {
Self {
jsonrpc: RPC_VERSION.to_string(),
id,
error,
result: None,
}
/// Create a new error response.
pub fn new_error(id: Option<Value>, error: Option<Value>) -> 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<String> {
let retval = serde_json::to_string(&result)?;
Ok(format!(
"window.external.rpc._result({}, {})",
id.to_string(),
retval
))
}
/// Get a script that resolves the promise with a result.
pub fn into_result_script(id: Value, result: Value) -> Result<String> {
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<String> {
let retval = serde_json::to_string(&result)?;
Ok(format!(
"window.external.rpc._error({}, {})",
id.to_string(),
retval
))
}
/// Get a script that rejects the promise with an error.
pub fn into_error_script(id: Value, result: Value) -> Result<String> {
let retval = serde_json::to_string(&result)?;
Ok(format!(
"window.external.rpc._error({}, {})",
id.to_string(),
retval
))
}
}

View File

@@ -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()));
}
fn inject(&mut self, hwnd: HWND, listener: Rc<FileDropHandler>) -> 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)));
}
}
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<FileDropHandler>) -> 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<F>(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<FileDropHandler>,
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<FileDropHandler>,
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<FileDropHandler>) -> 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),
}
fn new(window: HWND, listener: Rc<FileDropHandler>) -> 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
}
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);
}
// 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!();
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);
}
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
}
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
}
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
}
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<PathBuf>,
) -> Option<shellapi::HDROP> {
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<PathBuf>,
) -> Option<shellapi::HDROP> {
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,
};

View File

@@ -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<OnceCell<Controller>>,
controller: Rc<OnceCell<Controller>>,
// 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<OnceCell<FileDropController>>,
// 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<OnceCell<FileDropController>>,
}
impl WV for InnerWebView {
type Window = Window;
type Window = Window;
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Window,
scripts: Vec<String>,
url: Option<Url>,
// 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<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self> {
let hwnd = window.hwnd() as HWND;
fn new<F: 'static + Fn(&str) -> Result<Vec<u8>>>(
window: &Window,
scripts: Vec<String>,
url: Option<Url>,
// 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<RpcHandler>,
file_drop_handler: Option<FileDropHandler>,
) -> Result<Self> {
let hwnd = window.hwnd() as HWND;
let controller: Rc<OnceCell<Controller>> = Rc::new(OnceCell::new());
let controller_clone = controller.clone();
let controller: Rc<OnceCell<Controller>> = Rc::new(OnceCell::new());
let controller_clone = controller.clone();
let file_drop_controller: Rc<OnceCell<FileDropController>> = Rc::new(OnceCell::new());
let file_drop_controller_clone = file_drop_controller.clone();
let file_drop_controller: Rc<OnceCell<FileDropController>> = 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()?;
// 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)?;
}
// 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)?;
}
}
let _ = controller_clone.set(controller);
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;
// 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);
if let Some(c) = self.controller.get() {
c.put_bounds(rect)?;
let mut rect = std::mem::zeroed();
GetClientRect(hwnd, &mut rect);
controller.put_bounds(rect)?;
}
// 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)?;
}
}
let _ = controller_clone.set(controller);
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(())
}
}