mirror of
https://github.com/tauri-apps/web-view.git
synced 2026-02-04 02:11:18 +01:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
113
.github/workflows/ci.yml
vendored
Normal file
113
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
name: rustfmt
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
override: true
|
||||
|
||||
- name: install rustfmt
|
||||
run: rustup component add rustfmt
|
||||
|
||||
- name: cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
test-stable:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macOS-latest, windows-2019, ubuntu-latest]
|
||||
name: cargo test stable
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: install libgtk-dev libwebkit2gtk-4.0
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libwebkit2gtk-4.0-dev
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
|
||||
- name: install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: cargo test --all
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
test-nightly:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macOS-latest, windows-2019, ubuntu-latest]
|
||||
name: cargo test nightly
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: install libgtk-dev libwebkit2gtk-4.0
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libwebkit2gtk-4.0-dev
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
|
||||
- name: install nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: cargo test --all
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
check-docs:
|
||||
name: Docs
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macOS-latest, windows-2019, ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: install libgtk-dev libwebkit2gtk-4.0
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libwebkit2gtk-4.0-dev
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
|
||||
- name: install nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: check docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --document-private-items
|
||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +0,0 @@
|
||||
sudo: required
|
||||
language: rust
|
||||
cache: cargo
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libwebkit2gtk-4.0-dev
|
||||
sources:
|
||||
- sourceline: 'ppa:webkit-team/ppa'
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "web-view"
|
||||
version = "0.5.4"
|
||||
version = "0.6.2"
|
||||
authors = ["Boscop", "zxey <r.hozak@seznam.cz>", "Sam Green <sam.green81@gmail.com>"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -12,8 +12,9 @@ exclude = ["examples/todo-ps/dist/**/*", "examples/elm-counter/index.html"]
|
||||
|
||||
[dependencies]
|
||||
urlencoding = "1.0"
|
||||
webview-sys = { path = "webview-sys", version = "0.3.1" }
|
||||
webview-sys = { path = "webview-sys", version = "0.5" }
|
||||
boxfnonce = "0.1"
|
||||
tinyfiledialogs = "3.3.9"
|
||||
|
||||
[features]
|
||||
edge = ["webview-sys/edge"]
|
||||
@@ -27,3 +28,5 @@ actix-rt = "0.2"
|
||||
rust-embed = "5.1.0"
|
||||
mime_guess = "2.0.1"
|
||||
futures = "0.1"
|
||||
grep = "0.2.4"
|
||||
walkdir = "2.3.1"
|
||||
|
||||
33
README.md
33
README.md
@@ -1,4 +1,4 @@
|
||||
# web-view   [![Build Status]][travis] [![Latest Version]][crates.io] <!-- omit in toc -->
|
||||
# web-view    [![Latest Version]][crates.io] <!-- omit in toc -->
|
||||
|
||||
[Build Status]: https://api.travis-ci.org/Boscop/web-view.svg?branch=master
|
||||
[travis]: https://travis-ci.org/Boscop/web-view
|
||||
@@ -91,7 +91,7 @@ As this library can be found as a crate on the [Rust Community Registry](https:/
|
||||
web-view = { version = "0.5.4" }
|
||||
```
|
||||
|
||||
If you want to make use of **Edge** on Windows environments then you'll need to use the following syntax instead:
|
||||
If you want to make use of **Edge** on Windows environments, you will need to have Windows 10 SDK installed through Visual Studio Installer and you'll need to use the following syntax instead:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
@@ -126,20 +126,19 @@ fn main() {
|
||||
```
|
||||
|
||||
You should now be able to run `cargo build` and see something similar to the output below:
|
||||
|
||||
```text
|
||||
$ cargo build
|
||||
Updating crates.io index
|
||||
Compiling pkg-config v0.3.17
|
||||
Compiling bitflags v1.2.1
|
||||
Compiling cc v1.0.47
|
||||
Compiling boxfnonce v0.1.1
|
||||
Compiling urlencoding v1.0.0
|
||||
Compiling webview-sys v0.3.3
|
||||
Compiling web-view v0.5.4
|
||||
Compiling my-project v0.1.0 (C:\Users\Username\source\rust-projects\my-project)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 8.36s
|
||||
```
|
||||
|
||||
```text
|
||||
$ cargo build
|
||||
Updating crates.io index
|
||||
Compiling pkg-config v0.3.17
|
||||
Compiling cc v1.0.47
|
||||
Compiling boxfnonce v0.1.1
|
||||
Compiling urlencoding v1.0.0
|
||||
Compiling webview-sys v0.3.3
|
||||
Compiling web-view v0.5.4
|
||||
Compiling my-project v0.1.0 (C:\Users\Username\source\rust-projects\my-project)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 8.36s
|
||||
```
|
||||
|
||||
Assuming you get a successful build all you have to do now is run it with: `cargo run`. Hopefully you'll see the same as below:
|
||||
|
||||
@@ -149,7 +148,7 @@ For more usage info please check out the [examples](https://github.com/Boscop/we
|
||||
|
||||
## Known Issues and Limitations
|
||||
|
||||
* `Edge` feature switch not working on Windows 10. As of version `0.5.4` the Edge feature switch seems to have regressed and will not render content reliably. Trying to run the application with `cargo run` will always display an empty window before faulting, but if you run the binary created during the build process directly it will work as expected. For more information [see issue #96](https://github.com/Boscop/web-view/issues/96).
|
||||
* `Edge` feature switch not working on Windows 10 if run as `Administrator`. This was the root cause of the issue raised in [#96](https://github.com/Boscop/web-view/issues/96) and is the result of a bug in `Microsoft.Toolkit.Win32` which is [tracked here](https://github.com/windows-toolkit/Microsoft.Toolkit.Win32/issues/50).
|
||||
* `Edge` sandbox restrictions. If you decide to make use of an embedded Web Server to return your content you will need to run the following command to bypass the restriction that prevents communication with `localhost`:
|
||||
|
||||
``` powershell
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
extern crate actix_web;
|
||||
extern crate actix_rt;
|
||||
extern crate rust_embed;
|
||||
extern crate mime_guess;
|
||||
extern crate web_view;
|
||||
extern crate actix_web;
|
||||
extern crate futures;
|
||||
extern crate mime_guess;
|
||||
extern crate rust_embed;
|
||||
extern crate web_view;
|
||||
|
||||
use actix_web::{web, App, HttpRequest, HttpServer, HttpResponse, body::Body};
|
||||
use rust_embed::RustEmbed;
|
||||
use actix_web::{body::Body, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
use futures::future::Future;
|
||||
use mime_guess::from_path;
|
||||
use std::{borrow::Cow, thread, sync::mpsc};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, sync::mpsc, thread};
|
||||
use web_view::*;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@@ -19,7 +19,7 @@ use web_view::*;
|
||||
struct Asset;
|
||||
|
||||
fn assets(req: HttpRequest) -> HttpResponse {
|
||||
let path = if req.path() == "/" {
|
||||
let path = if req.path() == "/" {
|
||||
// if there is no path, return default file
|
||||
"index.html"
|
||||
} else {
|
||||
@@ -34,7 +34,9 @@ fn assets(req: HttpRequest) -> HttpResponse {
|
||||
Cow::Borrowed(bytes) => bytes.into(),
|
||||
Cow::Owned(bytes) => bytes.into(),
|
||||
};
|
||||
HttpResponse::Ok().content_type(from_path(path).first_or_octet_stream().as_ref()).body(body)
|
||||
HttpResponse::Ok()
|
||||
.content_type(from_path(path).first_or_octet_stream().as_ref())
|
||||
.body(body)
|
||||
}
|
||||
None => HttpResponse::NotFound().body("404 Not Found"),
|
||||
}
|
||||
@@ -48,12 +50,10 @@ fn main() {
|
||||
thread::spawn(move || {
|
||||
let sys = actix_rt::System::new("actix-example");
|
||||
|
||||
let server = HttpServer::new(|| {
|
||||
App::new().route("*", web::get().to(assets))
|
||||
})
|
||||
let server = HttpServer::new(|| App::new().route("*", web::get().to(assets)))
|
||||
.bind("127.0.0.1:0")
|
||||
.unwrap();
|
||||
|
||||
|
||||
// we specified the port to be 0,
|
||||
// meaning the operating system
|
||||
// will choose some available port
|
||||
@@ -62,12 +62,12 @@ fn main() {
|
||||
// so we know where to point webview at
|
||||
let port = server.addrs().first().unwrap().port();
|
||||
let server = server.start();
|
||||
|
||||
|
||||
let _ = port_tx.send(port);
|
||||
let _ = server_tx.send(server);
|
||||
let _ = sys.run();
|
||||
});
|
||||
|
||||
|
||||
let port = port_rx.recv().unwrap();
|
||||
let server = server_rx.recv().unwrap();
|
||||
|
||||
@@ -86,7 +86,5 @@ fn main() {
|
||||
.unwrap();
|
||||
|
||||
// gracefully shutdown actix web server
|
||||
let _ = server
|
||||
.stop(true)
|
||||
.wait();
|
||||
}
|
||||
let _ = server.stop(true).wait();
|
||||
}
|
||||
|
||||
36
examples/color_change.rs
Normal file
36
examples/color_change.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
extern crate web_view;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
web_view::builder()
|
||||
.title("Change background color")
|
||||
.content(Content::Html(HTML))
|
||||
.size(200, 100)
|
||||
.resizable(true)
|
||||
.debug(true)
|
||||
.user_data("")
|
||||
.invoke_handler(|webview, arg| {
|
||||
match arg {
|
||||
"red" => webview.set_color((255, 0, 0)),
|
||||
"green" => webview.set_color((0, 255, 0)),
|
||||
"blue" => webview.set_color((0, 0, 255)),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
const HTML: &str = r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<button onclick="external.invoke('red')">red</button>
|
||||
<button onclick="external.invoke('green')">green</button>
|
||||
<button onclick="external.invoke('blue')">blue</button>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
@@ -1,7 +1,9 @@
|
||||
//#![windows_subsystem = "windows"]
|
||||
|
||||
extern crate tinyfiledialogs as tfd;
|
||||
extern crate web_view;
|
||||
|
||||
use tfd::MessageBoxIcon;
|
||||
use web_view::*;
|
||||
|
||||
fn main() -> WVResult {
|
||||
@@ -14,15 +16,34 @@ fn main() -> WVResult {
|
||||
.user_data(())
|
||||
.invoke_handler(|webview, arg| {
|
||||
match arg {
|
||||
"open" => match webview.dialog().open_file("Please choose a file...", "")? {
|
||||
Some(path) => webview.dialog().info("File chosen", path.to_string_lossy()),
|
||||
None => webview
|
||||
.dialog()
|
||||
.warning("Warning", "You didn't choose a file."),
|
||||
}?,
|
||||
"exit" => {
|
||||
webview.terminate();
|
||||
"open" => match tfd::open_file_dialog("Please choose a file...", "", None) {
|
||||
Some(path) => tfd::message_box_ok("File chosen", &path, MessageBoxIcon::Info),
|
||||
None => tfd::message_box_ok(
|
||||
"Warning",
|
||||
"You didn't choose a file.",
|
||||
MessageBoxIcon::Warning,
|
||||
),
|
||||
},
|
||||
"save" => match tfd::save_file_dialog("Save file...", "") {
|
||||
Some(path) => tfd::message_box_ok("File chosen", &path, MessageBoxIcon::Info),
|
||||
None => tfd::message_box_ok(
|
||||
"Warning",
|
||||
"You didn't choose a file.",
|
||||
MessageBoxIcon::Warning,
|
||||
),
|
||||
},
|
||||
"info" => {
|
||||
tfd::message_box_ok("Info", "This is a info dialog", MessageBoxIcon::Info)
|
||||
}
|
||||
"warning" => tfd::message_box_ok(
|
||||
"Warning",
|
||||
"This is a warning dialog",
|
||||
MessageBoxIcon::Warning,
|
||||
),
|
||||
"error" => {
|
||||
tfd::message_box_ok("Error", "This is a error dialog", MessageBoxIcon::Error)
|
||||
}
|
||||
"exit" => webview.exit(),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
Ok(())
|
||||
@@ -35,9 +56,13 @@ fn main() -> WVResult {
|
||||
const HTML: &str = r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<button onclick="external.invoke('open')">Open</button>
|
||||
<button onclick="external.invoke('exit')">Exit</button>
|
||||
</body>
|
||||
<body>
|
||||
<button onclick="external.invoke('open')">Open</button>
|
||||
<button onclick="external.invoke('save')">Save</button>
|
||||
<button onclick="external.invoke('info')">Info</button>
|
||||
<button onclick="external.invoke('warning')">Warning</button>
|
||||
<button onclick="external.invoke('error')">Error</button>
|
||||
<button onclick="external.invoke('exit')">Exit</button>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
31
examples/frameless.rs
Normal file
31
examples/frameless.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
extern crate web_view;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
web_view::builder()
|
||||
.title("Frameless example")
|
||||
.content(Content::Html(HTML))
|
||||
.size(150, 150)
|
||||
.frameless(true)
|
||||
.debug(true)
|
||||
.user_data("")
|
||||
.invoke_handler(|webview, arg| {
|
||||
match arg {
|
||||
"exit" => webview.exit(),
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
const HTML: &str = r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<button onclick="external.invoke('exit')" style="display:block;width:100px;height:100px;font-size:24pt;margin:25px auto;">exit</button>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
33
examples/fullscreen.rs
Normal file
33
examples/fullscreen.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
extern crate web_view;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
web_view::builder()
|
||||
.title("Fullscreen example")
|
||||
.content(Content::Html(HTML))
|
||||
.size(800, 100)
|
||||
.resizable(true)
|
||||
.debug(true)
|
||||
.user_data("")
|
||||
.invoke_handler(|webview, arg| {
|
||||
match arg {
|
||||
"enter" => webview.set_fullscreen(true),
|
||||
"exit" => webview.set_fullscreen(false),
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
const HTML: &str = r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<button onclick="external.invoke('enter')">enter fullscreen</button>
|
||||
<button onclick="external.invoke('exit')">exit fullscreen</button>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
31
examples/graceful_exit.rs
Normal file
31
examples/graceful_exit.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
extern crate web_view;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
let res = web_view::builder()
|
||||
.title("Graceful Exit Example")
|
||||
.content(Content::Html(include_str!("graceful_exit/index.html")))
|
||||
.size(800, 600)
|
||||
.resizable(true)
|
||||
.debug(true)
|
||||
.user_data(0)
|
||||
.invoke_handler(invoke_handler)
|
||||
.run()
|
||||
.unwrap();
|
||||
println!("res: {:?}", res)
|
||||
}
|
||||
|
||||
fn invoke_handler(wv: &mut WebView<usize>, arg: &str) -> WVResult {
|
||||
if arg == "init" {
|
||||
wv.eval("init()")?;
|
||||
} else if arg == "update" {
|
||||
*wv.user_data_mut() += 1;
|
||||
let js = format!("setCurrentCount({})", wv.user_data());
|
||||
wv.eval(&js)?;
|
||||
} else if arg == "exit" {
|
||||
println!("exiting!");
|
||||
wv.exit();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
43
examples/graceful_exit/index.html
Normal file
43
examples/graceful_exit/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<style>
|
||||
#counter-message.preload {
|
||||
display: none;
|
||||
}
|
||||
#counter-message {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="counter-message" class="preload"></h1>
|
||||
<script>
|
||||
var t;
|
||||
function init() {
|
||||
var h1 = document.getElementById('counter-message');
|
||||
h1.setAttribute('class', '');
|
||||
setCurrentCount(0);
|
||||
t = setTimeout(tick, 500);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
window.external.invoke('update');
|
||||
t = setTimeout(tick, 500);
|
||||
}
|
||||
|
||||
function setCurrentCount(count) {
|
||||
if (count > 10) {
|
||||
return window.external.invoke('exit');
|
||||
}
|
||||
var h1 = document.getElementById('counter-message');
|
||||
var rem = 11 - count;
|
||||
var suffix = rem < 2 ? ' second' : ' seconds';
|
||||
h1.innerText = 'Exiting in ' + rem + suffix;
|
||||
document.body.appendChild(h1);
|
||||
}
|
||||
external.invoke('init');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
39
examples/multi_window.rs
Normal file
39
examples/multi_window.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
extern crate web_view;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
let mut webview1 = web_view::builder()
|
||||
.title("Multi window example - Window 1")
|
||||
.content(Content::Url("https://en.m.wikipedia.org/wiki/Main_Page"))
|
||||
.size(800, 600)
|
||||
.resizable(true)
|
||||
.debug(true)
|
||||
.user_data(())
|
||||
.invoke_handler(|_webview, _arg| Ok(()))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut webview2 = web_view::builder()
|
||||
.title("Multi window example - Window 2")
|
||||
.content(Content::Url("https://en.m.wikipedia.org/wiki/Main_Page"))
|
||||
.size(800, 600)
|
||||
.resizable(true)
|
||||
.debug(true)
|
||||
.user_data(())
|
||||
.invoke_handler(|_webview, _arg| Ok(()))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
if webview1.step().is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
if webview2.step().is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
187
examples/simple_grep.rs
Normal file
187
examples/simple_grep.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
extern crate grep;
|
||||
extern crate walkdir;
|
||||
extern crate web_view;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate tinyfiledialogs as tfd;
|
||||
|
||||
use grep::regex::RegexMatcher;
|
||||
use grep::searcher::sinks::UTF8;
|
||||
use grep::searcher::{BinaryDetection, SearcherBuilder};
|
||||
use std::error::Error;
|
||||
use std::ffi::OsString;
|
||||
use tfd::MessageBoxIcon;
|
||||
use walkdir::WalkDir;
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
web_view::builder()
|
||||
.title("Simple Grep Example")
|
||||
.content(Content::Html(HTML))
|
||||
.size(825, 625)
|
||||
.resizable(true)
|
||||
.debug(true)
|
||||
.user_data(())
|
||||
.invoke_handler(|webview, arg| {
|
||||
use Cmd::*;
|
||||
|
||||
match serde_json::from_str(arg).unwrap() {
|
||||
Search { pattern, path } => {
|
||||
let result = match search(&pattern, OsString::from(path)) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
let err_str = format!("{}", err);
|
||||
tfd::message_box_ok("Error", &err_str, MessageBoxIcon::Error);
|
||||
OsString::from("")
|
||||
}
|
||||
};
|
||||
if result.is_empty() {
|
||||
tfd::message_box_ok(
|
||||
"Information",
|
||||
"No results were found!",
|
||||
MessageBoxIcon::Info,
|
||||
);
|
||||
} else {
|
||||
let eval_str = format!("LoadTextArea({:?});", result);
|
||||
webview.eval(&eval_str)?;
|
||||
}
|
||||
}
|
||||
|
||||
Browse {} => match tfd::open_file_dialog("Please choose a file...", "", None) {
|
||||
Some(path_selected) => {
|
||||
let eval_str = format!("SetPath({:?});", path_selected);
|
||||
webview.eval(&eval_str)?;
|
||||
}
|
||||
None => {
|
||||
tfd::message_box_ok(
|
||||
"Warning",
|
||||
"You didn't choose a file.",
|
||||
MessageBoxIcon::Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Error { msg } => tfd::message_box_ok("Error", &msg, MessageBoxIcon::Error),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "camelCase")]
|
||||
pub enum Cmd {
|
||||
Search { pattern: String, path: String },
|
||||
Browse {},
|
||||
Error { msg: String },
|
||||
}
|
||||
|
||||
fn search(pattern: &str, path: OsString) -> Result<OsString, Box<dyn Error>> {
|
||||
let matcher = RegexMatcher::new_line_matcher(&pattern)?;
|
||||
let mut matches: OsString = OsString::new();
|
||||
let mut searcher = SearcherBuilder::new()
|
||||
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||
.line_number(true)
|
||||
.build();
|
||||
|
||||
let mut matched_line = OsString::new();
|
||||
|
||||
for result in WalkDir::new(path) {
|
||||
let entry = match result {
|
||||
Ok(entry) => entry,
|
||||
Err(err) => {
|
||||
let err_str = format!("{}", err);
|
||||
tfd::message_box_ok("Error", &err_str, MessageBoxIcon::Error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match searcher.search_path(
|
||||
&matcher,
|
||||
entry.path(),
|
||||
UTF8(|lnum, line| {
|
||||
matched_line = OsString::from(format!(
|
||||
"{:?}\t {}:\t {}",
|
||||
entry.path(),
|
||||
lnum.to_string(),
|
||||
line.to_string()
|
||||
));
|
||||
matches.push(&matched_line);
|
||||
matches.push("\n");
|
||||
Ok(true)
|
||||
}),
|
||||
) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
let err_str = format!("{}: {:?}", err, entry.path());
|
||||
tfd::message_box_ok("Error", &err_str, MessageBoxIcon::Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
const HTML: &str = r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 30em;
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
'use strict';
|
||||
var rpc = {
|
||||
invoke : function(arg) { window.external.invoke(JSON.stringify(arg)); },
|
||||
search : function() {
|
||||
var pattern = document.getElementById("pattern");
|
||||
var path = document.getElementById("path");
|
||||
if (pattern.value.trim().length === 0) {
|
||||
rpc.error("No pattern entered!");
|
||||
return;
|
||||
}
|
||||
if (path.value.trim().length === 0) {
|
||||
rpc.error("No path entered!");
|
||||
return;
|
||||
}
|
||||
var textArea = document.getElementById("text_box");
|
||||
textArea.value = "";
|
||||
rpc.invoke({cmd : 'search', path : path.value, pattern : pattern.value});
|
||||
},
|
||||
browse : function() { rpc.invoke({cmd : 'browse'}); },
|
||||
error : function(msg) { rpc.invoke({cmd : 'error', msg : msg}); },
|
||||
};
|
||||
|
||||
function LoadTextArea(data) {
|
||||
var textArea = document.getElementById("text_box");
|
||||
textArea.value = data;
|
||||
}
|
||||
function SetPath(path_selected) {
|
||||
var path = document.getElementById("path");
|
||||
path.value = path_selected;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<label for="pattern">Pattern to search for:</label>
|
||||
<input style="font-size:16px" id="pattern" type="text" size="35" />
|
||||
<label style="font-size:14px"> (prefix text with "(?i)" to ignore case)</label><br><br>
|
||||
<label for="path">Path (directory or file):</label>
|
||||
<input id="path" type="text" size="70" />
|
||||
<button onclick="rpc.browse()">Browse</button>
|
||||
<button onclick="rpc.search()">Search</button>
|
||||
<textarea class="textarea" id="text_box"></textarea>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
@@ -29,7 +29,7 @@ fn main() {
|
||||
render(webview, *counter)?;
|
||||
}
|
||||
"exit" => {
|
||||
webview.terminate();
|
||||
webview.exit();
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
32
examples/title_change.rs
Normal file
32
examples/title_change.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
extern crate web_view;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
web_view::builder()
|
||||
.title("Change title with input")
|
||||
.content(Content::Html(HTML))
|
||||
.size(800, 100)
|
||||
.resizable(true)
|
||||
.debug(true)
|
||||
.user_data("")
|
||||
.invoke_handler(|webview, arg| webview.set_title(arg))
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
const HTML: &str = r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<input id="title" type="text" placeholder="Title" style="width: 100%; padding: none; margin: none; font-size: large;"/>
|
||||
<script type="text/javascript">
|
||||
function updateTitle(e) {
|
||||
external.invoke(e.target.value);
|
||||
}
|
||||
var el = document.getElementById("title");
|
||||
el.addEventListener("input", updateTitle, false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
@@ -8,7 +8,8 @@ extern crate web_view;
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
let html = format!(r#"<!doctype html>
|
||||
let html = format!(
|
||||
r#"<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
@@ -27,9 +28,10 @@ fn main() {
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
styles = inline_style(include_str!("todo-elm/styles.css")),
|
||||
scripts = inline_script(include_str!("todo-elm/elm.js")) + &inline_script(include_str!("todo-elm/app.js")),
|
||||
);
|
||||
styles = inline_style(include_str!("todo-elm/styles.css")),
|
||||
scripts = inline_script(include_str!("todo-elm/elm.js"))
|
||||
+ &inline_script(include_str!("todo-elm/app.js")),
|
||||
);
|
||||
|
||||
let mut webview = web_view::builder()
|
||||
.title("Rust Todo App")
|
||||
@@ -46,13 +48,11 @@ fn main() {
|
||||
|
||||
match serde_json::from_str(arg).unwrap() {
|
||||
Init => {
|
||||
*tasks = vec![
|
||||
Task {
|
||||
name: "Create Elm example".to_string(),
|
||||
done: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
*tasks = vec![Task {
|
||||
name: "Create Elm example".to_string(),
|
||||
done: true,
|
||||
}];
|
||||
}
|
||||
Log { text } => println!("{}", text),
|
||||
AddTask { name } => tasks.push(Task { name, done: false }),
|
||||
MarkTask { index, done } => tasks[index].done = done,
|
||||
@@ -79,7 +79,10 @@ fn render(webview: &mut WebView<Vec<Task>>) -> WVResult {
|
||||
let render_tasks = {
|
||||
let tasks = webview.user_data();
|
||||
println!("{:#?}", tasks);
|
||||
format!("app.ports.fromRust.send({})", serde_json::to_string(tasks).unwrap())
|
||||
format!(
|
||||
"app.ports.fromRust.send({})",
|
||||
serde_json::to_string(tasks).unwrap()
|
||||
)
|
||||
};
|
||||
webview.eval(&render_tasks)
|
||||
}
|
||||
|
||||
104
examples/todo-vue.rs
Normal file
104
examples/todo-vue.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
//#![windows_subsystem = "windows"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate web_view;
|
||||
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
let html = format!(
|
||||
r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{styles}
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 9]>
|
||||
<div class="ie-upgrade-container">
|
||||
<p class="ie-upgrade-message">Please, upgrade Internet Explorer to continue using this software.</p>
|
||||
<a class="ie-upgrade-link" target="_blank" href="https://www.microsoft.com/en-us/download/internet-explorer.aspx">Upgrade</a>
|
||||
</div>
|
||||
<![endif]-->
|
||||
<!--[if gte IE 9 | !IE ]> <!-->
|
||||
<div id="app"></div>
|
||||
{scripts}
|
||||
<![endif]-->
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
styles = inline_style(include_str!("todo-vue/build/app.css")),
|
||||
scripts = inline_script(include_str!("todo-vue/build/app.js"))
|
||||
);
|
||||
|
||||
let mut webview = web_view::builder()
|
||||
.title("Rust Todo App")
|
||||
.content(Content::Html(html))
|
||||
.size(320, 480)
|
||||
.resizable(false)
|
||||
.debug(true)
|
||||
.user_data(vec![])
|
||||
.invoke_handler(|webview, arg| {
|
||||
use Cmd::*;
|
||||
|
||||
let tasks_len = {
|
||||
let tasks = webview.user_data_mut();
|
||||
|
||||
match serde_json::from_str(arg).unwrap() {
|
||||
Init => (),
|
||||
Log { text } => println!("{}", text),
|
||||
AddTask { name } => tasks.push(Task { name, done: false }),
|
||||
MarkTask { index, done } => tasks[index].done = done,
|
||||
ClearDoneTasks => tasks.retain(|t| !t.done),
|
||||
}
|
||||
|
||||
tasks.len()
|
||||
};
|
||||
|
||||
webview.set_title(&format!("Rust Todo App ({} Tasks)", tasks_len))?;
|
||||
render(webview)
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
webview.set_color((156, 39, 176));
|
||||
|
||||
let res = webview.run().unwrap();
|
||||
|
||||
println!("final state: {:?}", res);
|
||||
}
|
||||
|
||||
fn render(webview: &mut WebView<Vec<Task>>) -> WVResult {
|
||||
let render_tasks = {
|
||||
let tasks = webview.user_data();
|
||||
println!("{:#?}", tasks);
|
||||
format!("app.fromRust({})", serde_json::to_string(tasks).unwrap())
|
||||
};
|
||||
webview.eval(&render_tasks)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Task {
|
||||
name: String,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "camelCase")]
|
||||
pub enum Cmd {
|
||||
Init,
|
||||
Log { text: String },
|
||||
AddTask { name: String },
|
||||
MarkTask { index: usize, done: bool },
|
||||
ClearDoneTasks,
|
||||
}
|
||||
|
||||
fn inline_style(s: &str) -> String {
|
||||
format!(r#"<style type="text/css">{}</style>"#, s)
|
||||
}
|
||||
|
||||
fn inline_script(s: &str) -> String {
|
||||
format!(r#"<script type="text/javascript">{}</script>"#, s)
|
||||
}
|
||||
4
examples/todo-vue/.gitignore
vendored
Normal file
4
examples/todo-vue/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.cache
|
||||
node_modules
|
||||
dist
|
||||
build/*.map
|
||||
1
examples/todo-vue/build/app.css
Normal file
1
examples/todo-vue/build/app.css
Normal file
@@ -0,0 +1 @@
|
||||
.text-input-wrapper[data-v-84f9f2]{padding:.5em;position:fixed;top:0;left:0;right:0}.text-input[data-v-84f9f2]{width:100%;line-height:1.5em;padding:0 .2em;height:1.5em;outline:none;border:none;color:#4a148c;background-color:hsla(0,0%,100%,.87)}.text-input[data-v-84f9f2]:focus{background-color:#fff}.task-list[data-v-bbb001]{overflow-y:auto;position:fixed;top:2.5em;bottom:1.2em;left:0;right:0}.task-item[data-v-bbb001]{height:1.5em;color:hsla(0,0%,100%,.87);padding:.5em;cursor:pointer}.task-item.checked[data-v-bbb001]{text-decoration:line-through;color:hsla(0,0%,100%,.38)}.footer[data-v-b5ba49]{position:fixed;left:0;bottom:0;right:0;background-color:#fff;color:#9c27b0}.btn-clear-tasks[data-v-b5ba49]{width:100%;text-align:center;font-size:18px;height:2.5em;line-height:2.5em;text-transform:uppercase;cursor:pointer}.container[data-v-220073]{width:100%;height:100%;background-color:#9c27b0}*{margin:0;padding:0;box-sizing:border-box;font-size:28px;font-family:sans-serif}body,html{height:100%;overflow:none}.ie-upgrade-container{width:100%;height:100%;font-family:Arial,sans-serif;font-size:32px;color:#fff;background-color:#1ebbee;padding:3em 1em 1em}.ie-upgrade-link{margin:2em 0;padding:0 1em;color:#1ebbee;background-color:#fff;font-weight:700;text-align:center;display:block;width:100%;height:2em;line-height:2em;text-transform:uppercase}
|
||||
21
examples/todo-vue/build/app.js
Normal file
21
examples/todo-vue/build/app.js
Normal file
File diff suppressed because one or more lines are too long
7469
examples/todo-vue/package-lock.json
generated
Normal file
7469
examples/todo-vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
examples/todo-vue/package.json
Normal file
15
examples/todo-vue/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"start": "parcel src/index.html --global app",
|
||||
"build": "parcel build src/app.js --out-dir build --global app"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^2.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/component-compiler-utils": "^3.1.1",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vue-hot-reload-api": "^2.3.4"
|
||||
}
|
||||
}
|
||||
74
examples/todo-vue/src/App.vue
Normal file
74
examples/todo-vue/src/App.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<input-form></input-form>
|
||||
<task-list :tasks="tasks"></task-list>
|
||||
<app-fotter></app-fotter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InputForm from "./components/InputForm.vue";
|
||||
import TaskList from "./components/TaskList.vue";
|
||||
import AppFotter from "./components/Footer.vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tasks: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
InputForm,
|
||||
TaskList,
|
||||
AppFotter
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-size: 28px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: none;
|
||||
}
|
||||
|
||||
.ie-upgrade-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 32px;
|
||||
color: #ffffff;
|
||||
background-color: #1ebbee;
|
||||
padding: 3em 1em 1em 1em;
|
||||
}
|
||||
.ie-upgrade-link {
|
||||
margin: 2em 0em;
|
||||
padding: 0 1em;
|
||||
color: #1ebbee;
|
||||
background-color: #ffffff;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
23
examples/todo-vue/src/app.js
Normal file
23
examples/todo-vue/src/app.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
import { init } from './rpc';
|
||||
|
||||
let vm = new Vue({
|
||||
el: "#app",
|
||||
data: function () {
|
||||
return {
|
||||
tasks: []
|
||||
}
|
||||
},
|
||||
render: function (h) {
|
||||
return h(App, { attrs: { tasks: this.tasks } })
|
||||
}
|
||||
});
|
||||
|
||||
window.onload = function () { init(); };
|
||||
|
||||
function fromRust(tasks) {
|
||||
vm.tasks = tasks;
|
||||
}
|
||||
|
||||
export { fromRust };
|
||||
38
examples/todo-vue/src/components/Footer.vue
Normal file
38
examples/todo-vue/src/components/Footer.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div class="btn-clear-tasks" @click="clearTasks">Delete completed</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { clearDoneTasks } from "../rpc";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
clearTasks: function() {
|
||||
clearDoneTasks();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #ffffff;
|
||||
color: #9c27b0;
|
||||
}
|
||||
|
||||
.btn-clear-tasks {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
height: 2.5em;
|
||||
line-height: 2.5em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
46
examples/todo-vue/src/components/InputForm.vue
Normal file
46
examples/todo-vue/src/components/InputForm.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<form class="text-input-wrapper" @submit.prevent="submit">
|
||||
<input class="text-input" type="text" autofocus="true" v-model="text" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addTask } from "../rpc";
|
||||
|
||||
export default {
|
||||
data: function() {
|
||||
return {
|
||||
text: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit: function(e) {
|
||||
addTask(this.text);
|
||||
this.text = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-input-wrapper {
|
||||
padding: 0.5em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.text-input {
|
||||
width: 100%;
|
||||
line-height: 1.5em;
|
||||
padding: 0 0.2em;
|
||||
height: 1.5em;
|
||||
outline: none;
|
||||
border: none;
|
||||
color: #4a148c;
|
||||
background-color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
.text-input:focus {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
53
examples/todo-vue/src/components/TaskList.vue
Normal file
53
examples/todo-vue/src/components/TaskList.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="task-list">
|
||||
<div
|
||||
:class="isDone(task)"
|
||||
v-for="(task, index) in tasks"
|
||||
:key="`task.name-${index}`"
|
||||
@click="_markTask(index, task.done)"
|
||||
>{{task.name}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { markTask } from "../rpc";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tasks: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isDone: function(task) {
|
||||
let checked = task.done ? "checked" : "unchecked";
|
||||
return "task-item " + checked;
|
||||
},
|
||||
_markTask: function(i, done) {
|
||||
markTask(i, !done);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-list {
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
top: 2.5em;
|
||||
bottom: 1.2em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.task-item {
|
||||
height: 1.5em;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-item.checked {
|
||||
text-decoration: line-through;
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
</style>
|
||||
9
examples/todo-vue/src/index.html
Normal file
9
examples/todo-vue/src/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
examples/todo-vue/src/rpc.js
Normal file
27
examples/todo-vue/src/rpc.js
Normal file
@@ -0,0 +1,27 @@
|
||||
function invoke(arg) {
|
||||
window.external.invoke(JSON.stringify(arg));
|
||||
}
|
||||
function init() {
|
||||
invoke({ cmd: 'init' });
|
||||
}
|
||||
function log() {
|
||||
var s = '';
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (i != 0) {
|
||||
s = s + ' ';
|
||||
}
|
||||
s = s + JSON.stringify(arguments[i]);
|
||||
}
|
||||
invoke({ cmd: 'log', text: s });
|
||||
}
|
||||
function addTask(name) {
|
||||
invoke({ cmd: 'addTask', name: name });
|
||||
}
|
||||
function clearDoneTasks() {
|
||||
invoke({ cmd: 'clearDoneTasks' });
|
||||
}
|
||||
function markTask(index, done) {
|
||||
invoke({ cmd: 'markTask', index: index, done: done });
|
||||
}
|
||||
|
||||
export { init, log, addTask, clearDoneTasks, markTask };
|
||||
@@ -1,17 +1,17 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
extern crate actix_web;
|
||||
extern crate actix_rt;
|
||||
extern crate rust_embed;
|
||||
extern crate mime_guess;
|
||||
extern crate web_view;
|
||||
extern crate actix_web;
|
||||
extern crate futures;
|
||||
extern crate mime_guess;
|
||||
extern crate rust_embed;
|
||||
extern crate web_view;
|
||||
|
||||
use actix_web::{web, App, HttpRequest, HttpServer, HttpResponse, body::Body};
|
||||
use rust_embed::RustEmbed;
|
||||
use actix_web::{body::Body, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
use futures::future::Future;
|
||||
use mime_guess::from_path;
|
||||
use std::{borrow::Cow, thread, sync::mpsc};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, sync::mpsc, thread};
|
||||
use web_view::*;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@@ -19,7 +19,7 @@ use web_view::*;
|
||||
struct Asset;
|
||||
|
||||
fn assets(req: HttpRequest) -> HttpResponse {
|
||||
let path = if req.path() == "/" {
|
||||
let path = if req.path() == "/" {
|
||||
// if there is no path, return default file
|
||||
"index.html"
|
||||
} else {
|
||||
@@ -34,7 +34,9 @@ fn assets(req: HttpRequest) -> HttpResponse {
|
||||
Cow::Borrowed(bytes) => bytes.into(),
|
||||
Cow::Owned(bytes) => bytes.into(),
|
||||
};
|
||||
HttpResponse::Ok().content_type(from_path(path).first_or_octet_stream().as_ref()).body(body)
|
||||
HttpResponse::Ok()
|
||||
.content_type(from_path(path).first_or_octet_stream().as_ref())
|
||||
.body(body)
|
||||
}
|
||||
None => HttpResponse::NotFound().body("404 Not Found"),
|
||||
}
|
||||
@@ -48,12 +50,10 @@ fn main() {
|
||||
thread::spawn(move || {
|
||||
let sys = actix_rt::System::new("actix-example");
|
||||
|
||||
let server = HttpServer::new(|| {
|
||||
App::new().route("*", web::get().to(assets))
|
||||
})
|
||||
let server = HttpServer::new(|| App::new().route("*", web::get().to(assets)))
|
||||
.bind("127.0.0.1:0")
|
||||
.unwrap();
|
||||
|
||||
|
||||
// we specified the port to be 0,
|
||||
// meaning the operating system
|
||||
// will choose some available port
|
||||
@@ -62,12 +62,12 @@ fn main() {
|
||||
// so we know where to point webview at
|
||||
let port = server.addrs().first().unwrap().port();
|
||||
let server = server.start();
|
||||
|
||||
|
||||
let _ = port_tx.send(port);
|
||||
let _ = server_tx.send(server);
|
||||
let _ = sys.run();
|
||||
});
|
||||
|
||||
|
||||
let port = port_rx.recv().unwrap();
|
||||
let server = server_rx.recv().unwrap();
|
||||
|
||||
@@ -86,7 +86,5 @@ fn main() {
|
||||
.unwrap();
|
||||
|
||||
// gracefully shutdown actix web server
|
||||
let _ = server
|
||||
.stop(true)
|
||||
.wait();
|
||||
}
|
||||
let _ = server.stop(true).wait();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ extern crate web_view;
|
||||
use web_view::*;
|
||||
|
||||
fn main() {
|
||||
let html = format!(r#"
|
||||
let html = format!(
|
||||
r#"
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -27,9 +28,10 @@ fn main() {
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
styles = inline_style(include_str!("todo/styles.css")),
|
||||
scripts = inline_script(include_str!("todo/picodom.js")) + &inline_script(include_str!("todo/app.js")),
|
||||
);
|
||||
styles = inline_style(include_str!("todo/styles.css")),
|
||||
scripts = inline_script(include_str!("todo/picodom.js"))
|
||||
+ &inline_script(include_str!("todo/app.js")),
|
||||
);
|
||||
|
||||
let mut webview = web_view::builder()
|
||||
.title("Rust Todo App")
|
||||
|
||||
62
rustfmt.toml
62
rustfmt.toml
@@ -1,62 +0,0 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
indent_style = "Block"
|
||||
wrap_comments = false
|
||||
format_doc_comments = false
|
||||
comment_width = 80
|
||||
normalize_comments = false
|
||||
normalize_doc_attributes = false
|
||||
format_strings = false
|
||||
format_macro_matchers = false
|
||||
format_macro_bodies = true
|
||||
empty_item_single_line = true
|
||||
struct_lit_single_line = true
|
||||
fn_single_line = false
|
||||
where_single_line = false
|
||||
imports_indent = "Block"
|
||||
imports_layout = "Mixed"
|
||||
merge_imports = false
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
reorder_impl_items = false
|
||||
type_punctuation_density = "Wide"
|
||||
space_before_colon = false
|
||||
space_after_colon = true
|
||||
spaces_around_ranges = false
|
||||
binop_separator = "Front"
|
||||
remove_nested_parens = true
|
||||
combine_control_expr = true
|
||||
struct_field_align_threshold = 0
|
||||
enum_discrim_align_threshold = 0
|
||||
match_arm_blocks = true
|
||||
force_multiline_blocks = false
|
||||
fn_args_density = "Tall"
|
||||
brace_style = "SameLineWhere"
|
||||
control_brace_style = "AlwaysSameLine"
|
||||
trailing_semicolon = true
|
||||
trailing_comma = "Vertical"
|
||||
match_block_trailing_comma = false
|
||||
blank_lines_upper_bound = 1
|
||||
blank_lines_lower_bound = 0
|
||||
edition = "2015"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
condense_wildcard_suffixes = false
|
||||
color = "Auto"
|
||||
required_version = "1.0.0"
|
||||
unstable_features = false
|
||||
disable_all_formatting = false
|
||||
skip_children = false
|
||||
hide_parse_errors = false
|
||||
error_on_line_overflow = false
|
||||
error_on_unformatted = false
|
||||
report_todo = "Never"
|
||||
report_fixme = "Never"
|
||||
ignore = []
|
||||
emit_mode = "Files"
|
||||
make_backup = false
|
||||
110
src/dialog.rs
110
src/dialog.rs
@@ -1,10 +1,11 @@
|
||||
use ffi::{self, DialogFlags, DialogType};
|
||||
use std::{ffi::CString, path::PathBuf};
|
||||
use {read_str, WVResult, WebView};
|
||||
|
||||
const STR_BUF_SIZE: usize = 4096;
|
||||
use std::path::PathBuf;
|
||||
use tfd::MessageBoxIcon;
|
||||
use {WVResult, WebView};
|
||||
|
||||
/// A builder for opening a new dialog window.
|
||||
#[deprecated(
|
||||
note = "Please use crates like 'tinyfiledialogs' for dialog handling, see example in examples/dialog.rs"
|
||||
)]
|
||||
#[derive(Debug)]
|
||||
pub struct DialogBuilder<'a: 'b, 'b, T: 'a> {
|
||||
webview: &'b mut WebView<'a, T>,
|
||||
@@ -16,52 +17,24 @@ impl<'a: 'b, 'b, T: 'a> DialogBuilder<'a, 'b, T> {
|
||||
DialogBuilder { webview }
|
||||
}
|
||||
|
||||
fn dialog(
|
||||
&mut self,
|
||||
title: String,
|
||||
arg: String,
|
||||
dialog_type: DialogType,
|
||||
dialog_flags: DialogFlags,
|
||||
) -> WVResult<String> {
|
||||
let mut s = [0u8; STR_BUF_SIZE];
|
||||
|
||||
let title_cstr = CString::new(title)?;
|
||||
let arg_cstr = CString::new(arg)?;
|
||||
|
||||
unsafe {
|
||||
ffi::webview_dialog(
|
||||
self.webview.inner,
|
||||
dialog_type,
|
||||
dialog_flags,
|
||||
title_cstr.as_ptr(),
|
||||
arg_cstr.as_ptr(),
|
||||
s.as_mut_ptr() as _,
|
||||
s.len(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(read_str(&s))
|
||||
}
|
||||
|
||||
/// Opens a new open file dialog and returns the chosen file path.
|
||||
pub fn open_file<S, P>(&mut self, title: S, default_file: P) -> WVResult<Option<PathBuf>>
|
||||
where
|
||||
S: Into<String>,
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
self.dialog(
|
||||
title.into(),
|
||||
default_file.into().to_string_lossy().into_owned(),
|
||||
DialogType::Open,
|
||||
DialogFlags::FILE,
|
||||
)
|
||||
.map(|path| {
|
||||
if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(path))
|
||||
}
|
||||
})
|
||||
let default_file = default_file.into().into_os_string();
|
||||
let default_file = default_file
|
||||
.to_str()
|
||||
.expect("default_file is not valid utf-8");
|
||||
|
||||
let result = tfd::open_file_dialog(&title.into(), default_file, None).map(|p| p.into());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Opens a new save file dialog and returns the chosen file path.
|
||||
pub fn save_file(&mut self) -> WVResult<Option<PathBuf>> {
|
||||
Ok(tfd::save_file_dialog("", "").map(|p| p.into()))
|
||||
}
|
||||
|
||||
/// Opens a new choose directory dialog as returns the chosen directory path.
|
||||
@@ -74,19 +47,13 @@ impl<'a: 'b, 'b, T: 'a> DialogBuilder<'a, 'b, T> {
|
||||
S: Into<String>,
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
self.dialog(
|
||||
title.into(),
|
||||
default_directory.into().to_string_lossy().into_owned(),
|
||||
DialogType::Open,
|
||||
DialogFlags::DIRECTORY,
|
||||
)
|
||||
.map(|path| {
|
||||
if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(path))
|
||||
}
|
||||
})
|
||||
let default_directory = default_directory.into().into_os_string();
|
||||
let default_directory = default_directory
|
||||
.to_str()
|
||||
.expect("default_directory is not valid utf-8");
|
||||
|
||||
let result = tfd::select_folder_dialog(&title.into(), default_directory).map(|p| p.into());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Opens an info alert dialog.
|
||||
@@ -95,13 +62,8 @@ impl<'a: 'b, 'b, T: 'a> DialogBuilder<'a, 'b, T> {
|
||||
TS: Into<String>,
|
||||
MS: Into<String>,
|
||||
{
|
||||
self.dialog(
|
||||
title.into(),
|
||||
message.into(),
|
||||
DialogType::Alert,
|
||||
DialogFlags::INFO,
|
||||
)
|
||||
.map(|_| ())
|
||||
tfd::message_box_ok(&title.into(), &message.into(), MessageBoxIcon::Info);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens a warning alert dialog.
|
||||
@@ -110,13 +72,8 @@ impl<'a: 'b, 'b, T: 'a> DialogBuilder<'a, 'b, T> {
|
||||
TS: Into<String>,
|
||||
MS: Into<String>,
|
||||
{
|
||||
self.dialog(
|
||||
title.into(),
|
||||
message.into(),
|
||||
DialogType::Alert,
|
||||
DialogFlags::WARNING,
|
||||
)
|
||||
.map(|_| ())
|
||||
tfd::message_box_ok(&title.into(), &message.into(), MessageBoxIcon::Warning);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens an error alert dialog.
|
||||
@@ -125,12 +82,7 @@ impl<'a: 'b, 'b, T: 'a> DialogBuilder<'a, 'b, T> {
|
||||
TS: Into<String>,
|
||||
MS: Into<String>,
|
||||
{
|
||||
self.dialog(
|
||||
title.into(),
|
||||
message.into(),
|
||||
DialogType::Alert,
|
||||
DialogFlags::ERROR,
|
||||
)
|
||||
.map(|_| ())
|
||||
tfd::message_box_ok(&title.into(), &message.into(), MessageBoxIcon::Error);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
108
src/lib.rs
108
src/lib.rs
@@ -21,7 +21,10 @@
|
||||
//! [the examples]: https://github.com/Boscop/web-view/tree/master/examples
|
||||
//! [original readme]: https://github.com/zserge/webview/blob/master/README.md
|
||||
|
||||
#![allow(deprecated)] // TODO: remove this when removing dialogs
|
||||
|
||||
extern crate boxfnonce;
|
||||
extern crate tinyfiledialogs as tfd;
|
||||
extern crate urlencoding;
|
||||
extern crate webview_sys as ffi;
|
||||
|
||||
@@ -29,6 +32,7 @@ mod color;
|
||||
mod dialog;
|
||||
mod error;
|
||||
mod escape;
|
||||
|
||||
pub use color::Color;
|
||||
pub use dialog::DialogBuilder;
|
||||
pub use error::{CustomError, Error, WVResult};
|
||||
@@ -45,6 +49,17 @@ use std::{
|
||||
};
|
||||
use urlencoding::encode;
|
||||
|
||||
/// JavaScript function used to insert new css rules to webview.
|
||||
/// This function should be called with only one argument
|
||||
/// and that is the css to insert int webview.
|
||||
/// With every call of this function new style element
|
||||
/// will get created with css pasted as its children.
|
||||
const CSS_INJECT_FUNCTION: &str = "(function(e){var \
|
||||
t=document.createElement('style'),d=document.head||document.\
|
||||
getElementsByTagName('head')[0];t.setAttribute('type','text/\
|
||||
css'),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document.\
|
||||
createTextNode(e)),d.appendChild(t)})";
|
||||
|
||||
/// Content displayable inside a [`WebView`].
|
||||
///
|
||||
/// # Variants
|
||||
@@ -94,6 +109,7 @@ pub struct WebViewBuilder<'a, T: 'a, I, C> {
|
||||
pub debug: bool,
|
||||
pub invoke_handler: Option<I>,
|
||||
pub user_data: Option<T>,
|
||||
pub frameless: bool,
|
||||
}
|
||||
|
||||
impl<'a, T: 'a, I, C> Default for WebViewBuilder<'a, T, I, C>
|
||||
@@ -116,6 +132,7 @@ where
|
||||
debug,
|
||||
invoke_handler: None,
|
||||
user_data: None,
|
||||
frameless: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +187,13 @@ where
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
/// The window crated will be frameless
|
||||
///
|
||||
/// defaults to `false`
|
||||
pub fn frameless(mut self, frameless: bool) -> Self {
|
||||
self.frameless = frameless;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the invoke handler callback. This will be called when a message is received from
|
||||
/// JavaScript.
|
||||
@@ -218,6 +242,7 @@ where
|
||||
self.height,
|
||||
self.resizable,
|
||||
self.debug,
|
||||
self.frameless,
|
||||
user_data,
|
||||
invoke_handler,
|
||||
)
|
||||
@@ -259,7 +284,7 @@ struct UserData<'a, T> {
|
||||
/// [`WebViewBuilder`]: struct.WebViewBuilder.html
|
||||
#[derive(Debug)]
|
||||
pub struct WebView<'a, T: 'a> {
|
||||
inner: *mut CWebView,
|
||||
inner: Option<*mut CWebView>,
|
||||
_phantom: PhantomData<&'a mut T>,
|
||||
}
|
||||
|
||||
@@ -272,6 +297,7 @@ impl<'a, T> WebView<'a, T> {
|
||||
height: i32,
|
||||
resizable: bool,
|
||||
debug: bool,
|
||||
frameless: bool,
|
||||
user_data: T,
|
||||
invoke_handler: I,
|
||||
) -> WVResult<WebView<'a, T>>
|
||||
@@ -294,6 +320,7 @@ impl<'a, T> WebView<'a, T> {
|
||||
height,
|
||||
resizable as _,
|
||||
debug as _,
|
||||
frameless as _,
|
||||
Some(ffi_invoke_handler::<T>),
|
||||
user_data_ptr as _,
|
||||
);
|
||||
@@ -309,7 +336,7 @@ impl<'a, T> WebView<'a, T> {
|
||||
|
||||
unsafe fn from_ptr(inner: *mut CWebView) -> WebView<'a, T> {
|
||||
WebView {
|
||||
inner,
|
||||
inner: Some(inner),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -319,14 +346,14 @@ impl<'a, T> WebView<'a, T> {
|
||||
/// [`Handle`]: struct.Handle.html
|
||||
pub fn handle(&self) -> Handle<T> {
|
||||
Handle {
|
||||
inner: self.inner,
|
||||
inner: self.inner.unwrap(),
|
||||
live: Arc::downgrade(&self.user_data_wrapper().live),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn user_data_wrapper_ptr(&self) -> *mut UserData<'a, T> {
|
||||
unsafe { webview_get_user_data(self.inner) as _ }
|
||||
unsafe { webview_get_user_data(self.inner.unwrap()) as _ }
|
||||
}
|
||||
|
||||
fn user_data_wrapper(&self) -> &UserData<'a, T> {
|
||||
@@ -347,15 +374,20 @@ impl<'a, T> WebView<'a, T> {
|
||||
&mut self.user_data_wrapper_mut().inner
|
||||
}
|
||||
|
||||
/// Forces the `WebView` instance to end, without dropping.
|
||||
#[deprecated(note = "Please use exit instead")]
|
||||
pub fn terminate(&mut self) {
|
||||
unsafe { webview_terminate(self.inner) }
|
||||
self.exit();
|
||||
}
|
||||
|
||||
/// Gracefully exits the webview
|
||||
pub fn exit(&mut self) {
|
||||
unsafe { webview_exit(self.inner.unwrap()) }
|
||||
}
|
||||
|
||||
/// Executes the provided string as JavaScript code within the `WebView` instance.
|
||||
pub fn eval(&mut self, js: &str) -> WVResult {
|
||||
let js = CString::new(js)?;
|
||||
let ret = unsafe { webview_eval(self.inner, js.as_ptr()) };
|
||||
let ret = unsafe { webview_eval(self.inner.unwrap(), js.as_ptr()) };
|
||||
if ret != 0 {
|
||||
Err(Error::JsEvaluation)
|
||||
} else {
|
||||
@@ -365,13 +397,8 @@ impl<'a, T> WebView<'a, T> {
|
||||
|
||||
/// Injects the provided string as CSS within the `WebView` instance.
|
||||
pub fn inject_css(&mut self, css: &str) -> WVResult {
|
||||
let css = CString::new(css)?;
|
||||
let ret = unsafe { webview_inject_css(self.inner, css.as_ptr()) };
|
||||
if ret != 0 {
|
||||
Err(Error::CssInjection)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
let inject_func = format!("{}({})", CSS_INJECT_FUNCTION, escape(css));
|
||||
self.eval(&inject_func).map_err(|_| Error::CssInjection)
|
||||
}
|
||||
|
||||
/// Sets the color of the title bar.
|
||||
@@ -389,7 +416,7 @@ impl<'a, T> WebView<'a, T> {
|
||||
/// ```
|
||||
pub fn set_color<C: Into<Color>>(&mut self, color: C) {
|
||||
let color = color.into();
|
||||
unsafe { webview_set_color(self.inner, color.r, color.g, color.b, color.a) }
|
||||
unsafe { webview_set_color(self.inner.unwrap(), color.r, color.g, color.b, color.a) }
|
||||
}
|
||||
|
||||
/// Sets the title displayed at the top of the window.
|
||||
@@ -401,16 +428,19 @@ impl<'a, T> WebView<'a, T> {
|
||||
/// [`Error::NulByte`]: enum.Error.html#variant.NulByte
|
||||
pub fn set_title(&mut self, title: &str) -> WVResult {
|
||||
let title = CString::new(title)?;
|
||||
unsafe { webview_set_title(self.inner, title.as_ptr()) }
|
||||
unsafe { webview_set_title(self.inner.unwrap(), title.as_ptr()) }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enables or disables fullscreen.
|
||||
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
||||
unsafe { webview_set_fullscreen(self.inner, fullscreen as _) };
|
||||
unsafe { webview_set_fullscreen(self.inner.unwrap(), fullscreen as _) };
|
||||
}
|
||||
|
||||
/// Returns a builder for opening a new dialog window.
|
||||
#[deprecated(
|
||||
note = "Please use crates like 'tinyfiledialogs' for dialog handling, see example in examples/dialog.rs"
|
||||
)]
|
||||
pub fn dialog<'b>(&'b mut self) -> DialogBuilder<'a, 'b, T> {
|
||||
DialogBuilder::new(self)
|
||||
}
|
||||
@@ -418,7 +448,7 @@ impl<'a, T> WebView<'a, T> {
|
||||
/// Iterates the event loop. Returns `None` if the view has been closed or terminated.
|
||||
pub fn step(&mut self) -> Option<WVResult> {
|
||||
unsafe {
|
||||
match webview_loop(self.inner, 1) {
|
||||
match webview_loop(self.inner.unwrap(), 1) {
|
||||
0 => {
|
||||
let closure_result = &mut self.user_data_wrapper_mut().result;
|
||||
match closure_result {
|
||||
@@ -452,24 +482,28 @@ impl<'a, T> WebView<'a, T> {
|
||||
}
|
||||
|
||||
unsafe fn _into_inner(&mut self) -> T {
|
||||
let _lock = self
|
||||
let lock = self
|
||||
.user_data_wrapper()
|
||||
.live
|
||||
.write()
|
||||
.expect("A dispatch channel thread panicked while holding mutex to WebView.");
|
||||
|
||||
let user_data_ptr = self.user_data_wrapper_ptr();
|
||||
webview_exit(self.inner);
|
||||
webview_free(self.inner);
|
||||
webview_exit(self.inner.unwrap());
|
||||
webview_free(self.inner.unwrap());
|
||||
let user_data = *Box::from_raw(user_data_ptr);
|
||||
std::mem::drop(lock);
|
||||
user_data.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for WebView<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self._into_inner();
|
||||
if self.inner.is_some() {
|
||||
unsafe {
|
||||
self._into_inner();
|
||||
}
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -483,6 +517,16 @@ pub struct Handle<T> {
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Handle<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Handle {
|
||||
inner: self.inner,
|
||||
live: self.live.clone(),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Handle<T> {
|
||||
/// Schedules a closure to be run on the [`WebView`] thread.
|
||||
///
|
||||
@@ -520,31 +564,27 @@ impl<T> Handle<T> {
|
||||
unsafe impl<T> Send for Handle<T> {}
|
||||
unsafe impl<T> Sync for Handle<T> {}
|
||||
|
||||
fn read_str(s: &[u8]) -> String {
|
||||
let end = s.iter().position(|&b| b == 0).map_or(0, |i| i + 1);
|
||||
match CStr::from_bytes_with_nul(&s[..end]) {
|
||||
Ok(s) => s.to_string_lossy().into_owned(),
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn ffi_dispatch_handler<T>(webview: *mut CWebView, arg: *mut c_void) {
|
||||
unsafe {
|
||||
let mut handle = mem::ManuallyDrop::new(WebView::<T>::from_ptr(webview));
|
||||
let mut handle = WebView::<T>::from_ptr(webview);
|
||||
let result = {
|
||||
let callback =
|
||||
Box::<SendBoxFnOnce<'static, (&mut WebView<T>,), WVResult>>::from_raw(arg as _);
|
||||
callback.call(&mut handle)
|
||||
};
|
||||
handle.user_data_wrapper_mut().result = result;
|
||||
// Do not clean up the webview on drop of the temporary WebView in handle
|
||||
handle.inner = None;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn ffi_invoke_handler<T>(webview: *mut CWebView, arg: *const c_char) {
|
||||
unsafe {
|
||||
let arg = CStr::from_ptr(arg).to_string_lossy().to_string();
|
||||
let mut handle = mem::ManuallyDrop::new(WebView::<T>::from_ptr(webview));
|
||||
let result = ((*handle.user_data_wrapper_ptr()).invoke_handler)(&mut *handle, &arg);
|
||||
let mut handle = WebView::<T>::from_ptr(webview);
|
||||
let result = ((*handle.user_data_wrapper_ptr()).invoke_handler)(&mut handle, &arg);
|
||||
handle.user_data_wrapper_mut().result = result;
|
||||
// Do not clean up the webview on drop of the temporary WebView in handle
|
||||
handle.inner = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "webview-sys"
|
||||
version = "0.3.3"
|
||||
version = "0.5.0"
|
||||
authors = ["Boscop", "zxey <r.hozak@seznam.cz>"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/Boscop/web-view"
|
||||
@@ -9,6 +9,7 @@ keywords = ["web", "gui", "desktop", "electron", "webkit"]
|
||||
categories = ["gui", "web-programming", "api-bindings", "rendering", "visualization"]
|
||||
build = "build.rs"
|
||||
links = "webview"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "webview_sys"
|
||||
@@ -17,8 +18,15 @@ path = "lib.rs"
|
||||
[features]
|
||||
edge = []
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1"
|
||||
[target.'cfg(all(target_family = "unix", not(target_os = "macos")))'.dependencies]
|
||||
javascriptcore-rs-sys = "0.2"
|
||||
gtk-sys = "0.9"
|
||||
glib-sys = "0.9"
|
||||
gobject-sys = "0.9"
|
||||
webkit2gtk-sys = { version = "0.10", features = ["v2_8"] }
|
||||
gdk-sys = "0.9"
|
||||
gio-sys = "0.9"
|
||||
libc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
|
||||
@@ -38,16 +38,10 @@ fn main() {
|
||||
}
|
||||
}
|
||||
} else if target.contains("linux") || target.contains("bsd") {
|
||||
let webkit = pkg_config::Config::new()
|
||||
pkg_config::Config::new()
|
||||
.atleast_version("2.8")
|
||||
.probe("webkit2gtk-4.0")
|
||||
.unwrap();
|
||||
|
||||
build.file("webview_gtk.c");
|
||||
|
||||
for path in webkit.include_paths {
|
||||
build.include(path);
|
||||
}
|
||||
} else if target.contains("apple") {
|
||||
build
|
||||
.file("webview_cocoa.c")
|
||||
|
||||
339
webview-sys/gtk.rs
Normal file
339
webview-sys/gtk.rs
Normal file
@@ -0,0 +1,339 @@
|
||||
#![cfg(all(target_family = "unix", not(target_os = "macos")))]
|
||||
|
||||
use gdk_sys::{gdk_threads_add_idle, GdkRGBA};
|
||||
use gio_sys::GAsyncResult;
|
||||
use glib_sys::*;
|
||||
use gobject_sys::{g_signal_connect_data, GObject};
|
||||
use gtk_sys::*;
|
||||
use javascriptcore_sys::*;
|
||||
use libc::{c_char, c_double, c_int, c_void};
|
||||
use std::ffi::CStr;
|
||||
use std::mem;
|
||||
use std::ptr;
|
||||
use webkit2gtk_sys::*;
|
||||
|
||||
type ExternalInvokeCallback = extern "C" fn(webview: *mut WebView, arg: *const c_char);
|
||||
|
||||
#[repr(C)]
|
||||
struct WebView {
|
||||
url: *const c_char,
|
||||
title: *const c_char,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
resizable: c_int,
|
||||
debug: c_int,
|
||||
frameless: c_int,
|
||||
external_invoke_cb: ExternalInvokeCallback,
|
||||
window: *mut GtkWidget,
|
||||
scroller: *mut GtkWidget,
|
||||
webview: *mut GtkWidget,
|
||||
inspector_window: *mut GtkWidget,
|
||||
queue: *mut GAsyncQueue,
|
||||
ready: c_int,
|
||||
js_busy: c_int,
|
||||
should_exit: c_int,
|
||||
userdata: *mut c_void,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_set_title(webview: *mut WebView, title: *const c_char) {
|
||||
gtk_window_set_title(mem::transmute((*webview).window), title);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_set_fullscreen(webview: *mut WebView, fullscreen: c_int) {
|
||||
if fullscreen > 0 {
|
||||
gtk_window_fullscreen(mem::transmute((*webview).window));
|
||||
} else {
|
||||
gtk_window_unfullscreen(mem::transmute((*webview).window));
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_new(
|
||||
title: *const c_char,
|
||||
url: *const c_char,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
resizable: c_int,
|
||||
debug: c_int,
|
||||
frameless: c_int,
|
||||
external_invoke_cb: ExternalInvokeCallback,
|
||||
userdata: *mut c_void,
|
||||
) -> *mut WebView {
|
||||
let w = Box::new(WebView {
|
||||
url,
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
resizable,
|
||||
debug,
|
||||
frameless,
|
||||
external_invoke_cb,
|
||||
window: ptr::null_mut(),
|
||||
scroller: ptr::null_mut(),
|
||||
webview: ptr::null_mut(),
|
||||
inspector_window: ptr::null_mut(),
|
||||
queue: ptr::null_mut(),
|
||||
ready: 0,
|
||||
js_busy: 0,
|
||||
should_exit: 0,
|
||||
userdata,
|
||||
});
|
||||
|
||||
let w = Box::into_raw(w);
|
||||
|
||||
if gtk_init_check(ptr::null_mut(), ptr::null_mut()) == GFALSE {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
(*w).queue = g_async_queue_new();
|
||||
|
||||
let window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_title(mem::transmute(window), title);
|
||||
(*w).window = window;
|
||||
|
||||
if resizable > 0 {
|
||||
gtk_window_set_default_size(mem::transmute(window), width, height);
|
||||
} else {
|
||||
gtk_widget_set_size_request(mem::transmute(window), width, height);
|
||||
}
|
||||
|
||||
if frameless > 0 {
|
||||
gtk_window_set_decorated(mem::transmute(window), 0);
|
||||
}
|
||||
|
||||
gtk_window_set_resizable(mem::transmute(window), resizable);
|
||||
gtk_window_set_position(mem::transmute(window), GTK_WIN_POS_CENTER);
|
||||
|
||||
let scroller = gtk_scrolled_window_new(ptr::null_mut(), ptr::null_mut());
|
||||
gtk_container_add(mem::transmute(window), scroller);
|
||||
(*w).scroller = scroller;
|
||||
|
||||
let m = webkit_user_content_manager_new();
|
||||
webkit_user_content_manager_register_script_message_handler(
|
||||
m,
|
||||
CStr::from_bytes_with_nul_unchecked(b"external\0").as_ptr(),
|
||||
);
|
||||
|
||||
g_signal_connect_data(
|
||||
mem::transmute(m),
|
||||
CStr::from_bytes_with_nul_unchecked(b"script-message-received::external\0").as_ptr(),
|
||||
Some(mem::transmute(external_message_received_cb as *const ())),
|
||||
mem::transmute(w),
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
let webview = webkit_web_view_new_with_user_content_manager(m);
|
||||
(*w).webview = webview;
|
||||
webkit_web_view_load_uri(
|
||||
mem::transmute(webview),
|
||||
if url.is_null() {
|
||||
b"\0".as_ptr() as *const _
|
||||
} else {
|
||||
url
|
||||
},
|
||||
);
|
||||
g_signal_connect_data(
|
||||
mem::transmute(webview),
|
||||
CStr::from_bytes_with_nul_unchecked(b"load-changed\0").as_ptr(),
|
||||
Some(mem::transmute(webview_load_changed_cb as *const ())),
|
||||
mem::transmute(w),
|
||||
None,
|
||||
0,
|
||||
);
|
||||
gtk_container_add(mem::transmute(scroller), webview);
|
||||
|
||||
let settings = webkit_web_view_get_settings(mem::transmute(webview));
|
||||
// Enable webgl and canvas features.
|
||||
webkit_settings_set_enable_webgl(settings, 1);
|
||||
webkit_settings_set_enable_accelerated_2d_canvas(settings, 1);
|
||||
|
||||
if debug > 0 {
|
||||
webkit_settings_set_enable_write_console_messages_to_stdout(settings, 1);
|
||||
webkit_settings_set_enable_developer_extras(settings, 1);
|
||||
} else {
|
||||
g_signal_connect_data(
|
||||
mem::transmute(webview),
|
||||
CStr::from_bytes_with_nul_unchecked(b"context-menu\0").as_ptr(),
|
||||
Some(mem::transmute(webview_context_menu_cb as *const ())),
|
||||
mem::transmute(w),
|
||||
None,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
gtk_widget_show_all(window);
|
||||
|
||||
webkit_web_view_run_javascript(
|
||||
mem::transmute(webview),
|
||||
CStr::from_bytes_with_nul_unchecked(b"window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}}\0").as_ptr(),
|
||||
ptr::null_mut(),
|
||||
None,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
|
||||
g_signal_connect_data(
|
||||
mem::transmute(window),
|
||||
CStr::from_bytes_with_nul_unchecked(b"destroy\0").as_ptr(),
|
||||
Some(mem::transmute(webview_destroy_cb as *const ())),
|
||||
mem::transmute(w),
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
w
|
||||
}
|
||||
|
||||
extern "C" fn webview_context_menu_cb(
|
||||
_webview: *mut WebKitWebView,
|
||||
_default_menu: *mut GtkWidget,
|
||||
_hit_test_result: *mut WebKitHitTestResult,
|
||||
_triggered_with_keyboard: gboolean,
|
||||
_userdata: gboolean,
|
||||
) -> gboolean {
|
||||
GTRUE
|
||||
}
|
||||
|
||||
unsafe extern "C" fn external_message_received_cb(
|
||||
_m: *mut WebKitUserContentManager,
|
||||
r: *mut WebKitJavascriptResult,
|
||||
arg: gpointer,
|
||||
) {
|
||||
let webview: *mut WebView = mem::transmute(arg);
|
||||
let context = webkit_javascript_result_get_global_context(r);
|
||||
let value = webkit_javascript_result_get_value(r);
|
||||
let js = JSValueToStringCopy(context, value, ptr::null_mut());
|
||||
let n = JSStringGetMaximumUTF8CStringSize(js);
|
||||
let mut s = Vec::new();
|
||||
s.reserve(n);
|
||||
JSStringGetUTF8CString(js, s.as_mut_ptr(), n);
|
||||
((*webview).external_invoke_cb)(webview, s.as_ptr());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_get_user_data(webview: *mut WebView) -> *mut c_void {
|
||||
(*webview).userdata
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_free(webview: *mut WebView) {
|
||||
let _ = Box::from_raw(webview);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_loop(webview: *mut WebView, blocking: c_int) -> c_int {
|
||||
gtk_main_iteration_do(blocking);
|
||||
(*webview).should_exit
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_set_color(webview: *mut WebView, r: u8, g: u8, b: u8, a: u8) {
|
||||
let color = GdkRGBA {
|
||||
red: r as c_double / 255.0,
|
||||
green: g as c_double / 255.0,
|
||||
blue: b as c_double / 255.0,
|
||||
alpha: a as c_double / 255.0,
|
||||
};
|
||||
webkit_web_view_set_background_color(mem::transmute((*webview).webview), &color);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn webview_load_changed_cb(
|
||||
_webview: *mut WebKitWebView,
|
||||
event: WebKitLoadEvent,
|
||||
arg: gpointer,
|
||||
) {
|
||||
let w: *mut WebView = mem::transmute(arg);
|
||||
if event == WEBKIT_LOAD_FINISHED {
|
||||
(*w).ready = 1;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn webview_eval_finished(
|
||||
_object: *mut GObject,
|
||||
_result: *mut GAsyncResult,
|
||||
userdata: gpointer,
|
||||
) {
|
||||
let webview: *mut WebView = mem::transmute(userdata);
|
||||
(*webview).js_busy = 0;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_eval(webview: *mut WebView, js: *const c_char) -> c_int {
|
||||
while (*webview).ready == 0 {
|
||||
g_main_context_iteration(ptr::null_mut(), GTRUE);
|
||||
}
|
||||
|
||||
(*webview).js_busy = 1;
|
||||
|
||||
webkit_web_view_run_javascript(
|
||||
mem::transmute((*webview).webview),
|
||||
js,
|
||||
ptr::null_mut(),
|
||||
Some(webview_eval_finished),
|
||||
mem::transmute(webview),
|
||||
);
|
||||
|
||||
while (*webview).js_busy == 1 {
|
||||
g_main_context_iteration(ptr::null_mut(), GTRUE);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
type DispatchFn = extern "C" fn(webview: *mut WebView, arg: *mut c_void);
|
||||
|
||||
#[repr(C)]
|
||||
struct DispatchArg {
|
||||
func: DispatchFn,
|
||||
webview: *mut WebView,
|
||||
arg: *mut c_void,
|
||||
}
|
||||
|
||||
unsafe extern "C" fn webview_dispatch_wrapper(userdata: gpointer) -> gboolean {
|
||||
let webview: *mut WebView = mem::transmute(userdata);
|
||||
|
||||
loop {
|
||||
let arg: *mut DispatchArg = mem::transmute(g_async_queue_try_pop((*webview).queue));
|
||||
if arg.is_null() {
|
||||
break;
|
||||
}
|
||||
|
||||
((*arg).func)(webview, (*arg).arg);
|
||||
let _ = Box::from_raw(arg);
|
||||
}
|
||||
|
||||
GFALSE
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_dispatch(webview: *mut WebView, func: DispatchFn, arg: *mut c_void) {
|
||||
let arg = Box::new(DispatchArg { func, webview, arg });
|
||||
|
||||
let queue = (*webview).queue;
|
||||
|
||||
g_async_queue_lock(queue);
|
||||
g_async_queue_push_unlocked(queue, mem::transmute(Box::into_raw(arg)));
|
||||
|
||||
if g_async_queue_length_unlocked(queue) == 1 {
|
||||
gdk_threads_add_idle(Some(webview_dispatch_wrapper), mem::transmute(webview));
|
||||
}
|
||||
|
||||
g_async_queue_unlock(queue);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_destroy_cb(_widget: *mut GtkWidget, arg: gpointer) {
|
||||
webview_exit(mem::transmute(arg));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_exit(webview: *mut WebView) {
|
||||
(*webview).should_exit = 1;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn webview_print_log(s: *const c_char) {
|
||||
let format = std::ffi::CString::new("%s\n").unwrap();
|
||||
libc::printf(format.as_ptr(), s);
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
//!
|
||||
//! [webview]: https://github.com/zserge/webview
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[cfg(all(target_family = "unix", not(target_os = "macos")))]
|
||||
mod gtk;
|
||||
|
||||
use std::os::raw::*;
|
||||
|
||||
@@ -15,36 +15,25 @@ pub enum CWebView {} // opaque type, only used in ffi pointers
|
||||
type ErasedExternalInvokeFn = extern "C" fn(webview: *mut CWebView, arg: *const c_char);
|
||||
type ErasedDispatchFn = extern "C" fn(webview: *mut CWebView, arg: *mut c_void);
|
||||
|
||||
#[repr(C)]
|
||||
pub enum DialogType {
|
||||
Open = 0,
|
||||
Save = 1,
|
||||
Alert = 2,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[repr(C)]
|
||||
pub struct DialogFlags: u32 {
|
||||
const FILE = 0b0000;
|
||||
const DIRECTORY = 0b0001;
|
||||
const INFO = 0b0010;
|
||||
const WARNING = 0b0100;
|
||||
const ERROR = 0b0110;
|
||||
}
|
||||
}
|
||||
|
||||
extern {
|
||||
pub fn webview_free(this: *mut CWebView);
|
||||
pub fn webview_new(title: *const c_char, url: *const c_char, width: c_int, height: c_int, resizable: c_int, debug: c_int, external_invoke_cb: Option<ErasedExternalInvokeFn>, userdata: *mut c_void) -> *mut CWebView;
|
||||
pub fn webview_loop(this: *mut CWebView, blocking: c_int) -> c_int;
|
||||
pub fn webview_terminate(this: *mut CWebView);
|
||||
pub fn webview_exit(this: *mut CWebView);
|
||||
pub fn webview_get_user_data(this: *mut CWebView) -> *mut c_void;
|
||||
pub fn webview_dispatch(this: *mut CWebView, f: Option<ErasedDispatchFn>, arg: *mut c_void);
|
||||
pub fn webview_eval(this: *mut CWebView, js: *const c_char) -> c_int;
|
||||
pub fn webview_inject_css(this: *mut CWebView, css: *const c_char) -> c_int;
|
||||
pub fn webview_set_title(this: *mut CWebView, title: *const c_char);
|
||||
pub fn webview_set_fullscreen(this: *mut CWebView, fullscreen: c_int);
|
||||
pub fn webview_set_color(this: *mut CWebView, red: u8, green: u8, blue: u8, alpha: u8);
|
||||
pub fn webview_dialog(this: *mut CWebView, dialog_type: DialogType, flags: DialogFlags, title: *const c_char, arg: *const c_char, result: *mut c_char, result_size: usize);
|
||||
extern "C" {
|
||||
pub fn webview_free(this: *mut CWebView);
|
||||
pub fn webview_new(
|
||||
title: *const c_char,
|
||||
url: *const c_char,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
resizable: c_int,
|
||||
debug: c_int,
|
||||
frameless: c_int,
|
||||
external_invoke_cb: Option<ErasedExternalInvokeFn>,
|
||||
userdata: *mut c_void,
|
||||
) -> *mut CWebView;
|
||||
pub fn webview_loop(this: *mut CWebView, blocking: c_int) -> c_int;
|
||||
pub fn webview_exit(this: *mut CWebView);
|
||||
pub fn webview_get_user_data(this: *mut CWebView) -> *mut c_void;
|
||||
pub fn webview_dispatch(this: *mut CWebView, f: Option<ErasedDispatchFn>, arg: *mut c_void);
|
||||
pub fn webview_eval(this: *mut CWebView, js: *const c_char) -> c_int;
|
||||
pub fn webview_set_title(this: *mut CWebView, title: *const c_char);
|
||||
pub fn webview_set_fullscreen(this: *mut CWebView, fullscreen: c_int);
|
||||
pub fn webview_set_color(this: *mut CWebView, red: u8, green: u8, blue: u8, alpha: u8);
|
||||
}
|
||||
|
||||
@@ -20,110 +20,51 @@ typedef void* webview_t;
|
||||
typedef void (*webview_external_invoke_cb_t)(webview_t w, const char *arg);
|
||||
typedef void (*webview_dispatch_fn)(webview_t w, void *arg);
|
||||
|
||||
enum webview_dialog_type {
|
||||
WEBVIEW_DIALOG_TYPE_OPEN = 0,
|
||||
WEBVIEW_DIALOG_TYPE_SAVE = 1,
|
||||
WEBVIEW_DIALOG_TYPE_ALERT = 2
|
||||
};
|
||||
|
||||
WEBVIEW_API void webview_run(webview_t w);
|
||||
WEBVIEW_API int webview_loop(webview_t w, int blocking);
|
||||
WEBVIEW_API int webview_eval(webview_t w, const char *js);
|
||||
WEBVIEW_API int webview_inject_css(webview_t w, const char *css);
|
||||
WEBVIEW_API void webview_set_title(webview_t w, const char *title);
|
||||
WEBVIEW_API void webview_set_fullscreen(webview_t w, int fullscreen);
|
||||
WEBVIEW_API void webview_set_color(webview_t w, uint8_t r, uint8_t g,
|
||||
uint8_t b, uint8_t a);
|
||||
WEBVIEW_API void webview_dialog(webview_t w,
|
||||
enum webview_dialog_type dlgtype, int flags,
|
||||
const char *title, const char *arg,
|
||||
char *result, size_t resultsz);
|
||||
WEBVIEW_API void webview_dispatch(webview_t w, webview_dispatch_fn fn,
|
||||
void *arg);
|
||||
WEBVIEW_API void webview_terminate(webview_t w);
|
||||
WEBVIEW_API void webview_exit(webview_t w);
|
||||
WEBVIEW_API void webview_debug(const char *format, ...);
|
||||
WEBVIEW_API void webview_print_log(const char *s);
|
||||
|
||||
WEBVIEW_API void* webview_get_user_data(webview_t w);
|
||||
WEBVIEW_API webview_t webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, webview_external_invoke_cb_t external_invoke_cb, void* userdata);
|
||||
WEBVIEW_API webview_t webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, int frameless, webview_external_invoke_cb_t external_invoke_cb, void* userdata);
|
||||
WEBVIEW_API void webview_free(webview_t w);
|
||||
WEBVIEW_API void webview_destroy(webview_t w);
|
||||
|
||||
// TODO WEBVIEW_API void webview_navigate(webview_t w, const char* url);
|
||||
|
||||
#define WEBVIEW_DIALOG_FLAG_FILE (0 << 0)
|
||||
#define WEBVIEW_DIALOG_FLAG_DIRECTORY (1 << 0)
|
||||
|
||||
#define WEBVIEW_DIALOG_FLAG_INFO (1 << 1)
|
||||
#define WEBVIEW_DIALOG_FLAG_WARNING (2 << 1)
|
||||
#define WEBVIEW_DIALOG_FLAG_ERROR (3 << 1)
|
||||
#define WEBVIEW_DIALOG_FLAG_ALERT_MASK (3 << 1)
|
||||
|
||||
struct webview_dispatch_arg {
|
||||
webview_dispatch_fn fn;
|
||||
webview_t w;
|
||||
void *arg;
|
||||
};
|
||||
|
||||
#define DEFAULT_URL \
|
||||
"data:text/" \
|
||||
"html,%3C%21DOCTYPE%20html%3E%0A%3Chtml%20lang=%22en%22%3E%0A%3Chead%3E%" \
|
||||
"3Cmeta%20charset=%22utf-8%22%3E%3Cmeta%20http-equiv=%22X-UA-Compatible%22%" \
|
||||
"20content=%22IE=edge%22%3E%3C%2Fhead%3E%0A%3Cbody%3E%3Cdiv%20id=%22app%22%" \
|
||||
"3E%3C%2Fdiv%3E%3Cscript%20type=%22text%2Fjavascript%22%3E%3C%2Fscript%3E%" \
|
||||
"3C%2Fbody%3E%0A%3C%2Fhtml%3E"
|
||||
|
||||
#define CSS_INJECT_FUNCTION \
|
||||
"(function(e){var " \
|
||||
"t=document.createElement('style'),d=document.head||document." \
|
||||
"getElementsByTagName('head')[0];t.setAttribute('type','text/" \
|
||||
"css'),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document." \
|
||||
"createTextNode(e)),d.appendChild(t)})"
|
||||
|
||||
static int webview_js_encode(const char *s, char *esc, size_t n) {
|
||||
int r = 1; /* At least one byte for trailing zero */
|
||||
for (; *s; s++) {
|
||||
const unsigned char c = *s;
|
||||
if (c >= 0x20 && c < 0x80 && strchr("<>\\'\"", c) == NULL) {
|
||||
if (n > 0) {
|
||||
*esc++ = c;
|
||||
n--;
|
||||
}
|
||||
r++;
|
||||
} else {
|
||||
if (n > 0) {
|
||||
snprintf(esc, n, "\\x%02x", (int)c);
|
||||
esc += 4;
|
||||
n -= 4;
|
||||
}
|
||||
r += 4;
|
||||
}
|
||||
// Convert ASCII hex digit to a nibble (four bits, 0 - 15).
|
||||
//
|
||||
// Use unsigned to avoid signed overflow UB.
|
||||
static inline unsigned char hex2nibble(unsigned char c) {
|
||||
if (c >= '0' && c <= '9') {
|
||||
return c - '0';
|
||||
} else if (c >= 'a' && c <= 'f') {
|
||||
return 10 + (c - 'a');
|
||||
} else if (c >= 'A' && c <= 'F') {
|
||||
return 10 + (c - 'A');
|
||||
}
|
||||
return r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
WEBVIEW_API int webview_inject_css(webview_t w, const char *css) {
|
||||
int n = webview_js_encode(css, NULL, 0);
|
||||
char *esc = (char *)calloc(1, sizeof(CSS_INJECT_FUNCTION) + n + 4);
|
||||
if (esc == NULL) {
|
||||
return -1;
|
||||
}
|
||||
char *js = (char *)calloc(1, n);
|
||||
webview_js_encode(css, js, n);
|
||||
snprintf(esc, sizeof(CSS_INJECT_FUNCTION) + n + 4, "%s(\"%s\")",
|
||||
CSS_INJECT_FUNCTION, js);
|
||||
int r = webview_eval(w, esc);
|
||||
free(js);
|
||||
free(esc);
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline const char *webview_check_url(const char *url) {
|
||||
if (url == NULL || strlen(url) == 0) {
|
||||
return DEFAULT_URL;
|
||||
}
|
||||
return url;
|
||||
// Convert ASCII hex string (two characters) to byte.
|
||||
//
|
||||
// E.g., "0B" => 0x0B, "af" => 0xAF.
|
||||
static inline char hex2char(const char* p) {
|
||||
return hex2nibble(p[0]) * 16 + hex2nibble(p[1]);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -19,6 +19,7 @@ struct cocoa_webview {
|
||||
int height;
|
||||
int resizable;
|
||||
int debug;
|
||||
int frameless;
|
||||
webview_external_invoke_cb_t external_invoke_cb;
|
||||
struct webview_priv priv;
|
||||
void *userdata;
|
||||
@@ -33,7 +34,10 @@ WEBVIEW_API void* webview_get_user_data(webview_t w) {
|
||||
return wv->userdata;
|
||||
}
|
||||
|
||||
WEBVIEW_API webview_t webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, webview_external_invoke_cb_t external_invoke_cb, void* userdata) {
|
||||
WEBVIEW_API webview_t webview_new(
|
||||
const char* title, const char* url,
|
||||
int width, int height, int resizable, int debug, int frameless,
|
||||
webview_external_invoke_cb_t external_invoke_cb, void* userdata) {
|
||||
struct cocoa_webview* wv = (struct cocoa_webview*)calloc(1, sizeof(*wv));
|
||||
wv->width = width;
|
||||
wv->height = height;
|
||||
@@ -41,6 +45,7 @@ WEBVIEW_API webview_t webview_new(const char* title, const char* url, int width,
|
||||
wv->url = url;
|
||||
wv->resizable = resizable;
|
||||
wv->debug = debug;
|
||||
wv->frameless = frameless;
|
||||
wv->external_invoke_cb = external_invoke_cb;
|
||||
wv->userdata = userdata;
|
||||
if (webview_init(wv) != 0) {
|
||||
@@ -71,6 +76,8 @@ WEBVIEW_API webview_t webview_new(const char* title, const char* url, int width,
|
||||
#define WKNavigationResponsePolicyAllow 1
|
||||
#define WKUserScriptInjectionTimeAtDocumentStart 0
|
||||
#define NSApplicationActivationPolicyRegular 0
|
||||
#define NSApplicationDefinedEvent 15
|
||||
#define NSWindowStyleMaskBorderless 0
|
||||
|
||||
static id get_nsstring(const char *c_str) {
|
||||
return objc_msgSend((id)objc_getClass("NSString"),
|
||||
@@ -88,9 +95,39 @@ static id create_menu_item(id title, const char *action, const char *key) {
|
||||
}
|
||||
|
||||
static void webview_window_will_close(id self, SEL cmd, id notification) {
|
||||
struct cocoa_webview *w =
|
||||
struct cocoa_webview *wv =
|
||||
(struct cocoa_webview *)objc_getAssociatedObject(self, "webview");
|
||||
webview_terminate(w);
|
||||
wv->priv.should_exit = 1;
|
||||
/***
|
||||
Since by default for `webview_loop` is set to be blocking
|
||||
we need to somehow signal the application that our
|
||||
state has changed. The activity in the `invoke_handler` does
|
||||
not interact with the `webview_loop` at all. This means that
|
||||
the `exit` wouldn't be recognized by the application until
|
||||
another event occurs like mouse movement or a key press.
|
||||
To enable the invoke_handler to notify the application
|
||||
correctly we need to send a custom event to the application.
|
||||
We are going to first create an event with the type
|
||||
NSApplicationDefined, and zero for all the other properties.
|
||||
***/
|
||||
id event = objc_msgSend((id)objc_getClass("NSEvent"),
|
||||
sel_registerName("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:"),
|
||||
NSApplicationDefinedEvent,
|
||||
(id)objc_getClass("NSZeroPoint"),
|
||||
0, 0.0, 0, NULL, 0, 0, 0);
|
||||
id app = objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication"));
|
||||
/***
|
||||
With a custom event crated and a pointer to the sharedApplication
|
||||
we can now send the event. We need to make sure it get's queued as
|
||||
early as possible, so we will set the argument atStart to
|
||||
the NSDate distantPast constructor. This will trigger a noop
|
||||
event on the application allowing the `webview_loop` to continue
|
||||
its current iteration.
|
||||
***/
|
||||
objc_msgSend(app, sel_registerName("postEvent:atStart:"), event,
|
||||
objc_msgSend((id)objc_getClass("NSDate"),
|
||||
sel_registerName("distantPast")));
|
||||
}
|
||||
|
||||
static void webview_external_invoke(id self, SEL cmd, id contentController,
|
||||
@@ -211,13 +248,17 @@ WEBVIEW_API int webview_init(webview_t w) {
|
||||
objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication"));
|
||||
|
||||
Class __WKScriptMessageHandler = objc_allocateClassPair(
|
||||
static Class __WKScriptMessageHandler;
|
||||
if(__WKScriptMessageHandler == NULL) {
|
||||
__WKScriptMessageHandler = objc_allocateClassPair(
|
||||
objc_getClass("NSObject"), "__WKScriptMessageHandler", 0);
|
||||
class_addMethod(
|
||||
__WKScriptMessageHandler,
|
||||
sel_registerName("userContentController:didReceiveScriptMessage:"),
|
||||
(IMP)webview_external_invoke, "v@:@@");
|
||||
objc_registerClassPair(__WKScriptMessageHandler);
|
||||
class_addProtocol(__WKScriptMessageHandler, objc_getProtocol("WKScriptMessageHandler"));
|
||||
class_addMethod(
|
||||
__WKScriptMessageHandler,
|
||||
sel_registerName("userContentController:didReceiveScriptMessage:"),
|
||||
(IMP)webview_external_invoke, "v@:@@");
|
||||
objc_registerClassPair(__WKScriptMessageHandler);
|
||||
}
|
||||
|
||||
id scriptMessageHandler =
|
||||
objc_msgSend((id)__WKScriptMessageHandler, sel_registerName("new"));
|
||||
@@ -231,27 +272,36 @@ WEBVIEW_API int webview_init(webview_t w) {
|
||||
https://github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
|
||||
***/
|
||||
|
||||
Class __WKDownloadDelegate = objc_allocateClassPair(
|
||||
static Class __WKDownloadDelegate;
|
||||
if(__WKDownloadDelegate == NULL) {
|
||||
__WKDownloadDelegate = objc_allocateClassPair(
|
||||
objc_getClass("NSObject"), "__WKDownloadDelegate", 0);
|
||||
class_addMethod(
|
||||
__WKDownloadDelegate,
|
||||
sel_registerName("_download:decideDestinationWithSuggestedFilename:"
|
||||
"completionHandler:"),
|
||||
(IMP)run_save_panel, "v@:@@?");
|
||||
class_addMethod(__WKDownloadDelegate,
|
||||
sel_registerName("_download:didFailWithError:"),
|
||||
(IMP)download_failed, "v@:@@");
|
||||
objc_registerClassPair(__WKDownloadDelegate);
|
||||
class_addProtocol(__WKDownloadDelegate, objc_getProtocol("WKDownloadDelegate"));
|
||||
|
||||
class_addMethod(
|
||||
__WKDownloadDelegate,
|
||||
sel_registerName("_download:decideDestinationWithSuggestedFilename:"
|
||||
"completionHandler:"),
|
||||
(IMP)run_save_panel, "v@:@@?");
|
||||
class_addMethod(__WKDownloadDelegate,
|
||||
sel_registerName("_download:didFailWithError:"),
|
||||
(IMP)download_failed, "v@:@@");
|
||||
objc_registerClassPair(__WKDownloadDelegate);
|
||||
}
|
||||
|
||||
id downloadDelegate =
|
||||
objc_msgSend((id)__WKDownloadDelegate, sel_registerName("new"));
|
||||
|
||||
Class __WKPreferences = objc_allocateClassPair(objc_getClass("WKPreferences"),
|
||||
static Class __WKPreferences;
|
||||
if(__WKPreferences == NULL) {
|
||||
__WKPreferences = objc_allocateClassPair(objc_getClass("WKPreferences"),
|
||||
"__WKPreferences", 0);
|
||||
objc_property_attribute_t type = {"T", "c"};
|
||||
objc_property_attribute_t ownership = {"N", ""};
|
||||
objc_property_attribute_t attrs[] = {type, ownership};
|
||||
class_replaceProperty(__WKPreferences, "developerExtrasEnabled", attrs, 2);
|
||||
objc_registerClassPair(__WKPreferences);
|
||||
objc_property_attribute_t type = {"T", "c"};
|
||||
objc_property_attribute_t ownership = {"N", ""};
|
||||
objc_property_attribute_t attrs[] = {type, ownership};
|
||||
class_replaceProperty(__WKPreferences, "developerExtrasEnabled", attrs, 2);
|
||||
objc_registerClassPair(__WKPreferences);
|
||||
}
|
||||
id wkPref = objc_msgSend((id)__WKPreferences, sel_registerName("new"));
|
||||
objc_msgSend(wkPref, sel_registerName("setValue:forKey:"),
|
||||
objc_msgSend((id)objc_getClass("NSNumber"),
|
||||
@@ -298,12 +348,15 @@ WEBVIEW_API int webview_init(webview_t w) {
|
||||
userController);
|
||||
objc_msgSend(config, sel_registerName("setPreferences:"), wkPref);
|
||||
|
||||
Class __NSWindowDelegate = objc_allocateClassPair(objc_getClass("NSObject"),
|
||||
static Class __NSWindowDelegate;
|
||||
if(__NSWindowDelegate == NULL) {
|
||||
__NSWindowDelegate = objc_allocateClassPair(objc_getClass("NSObject"),
|
||||
"__NSWindowDelegate", 0);
|
||||
class_addProtocol(__NSWindowDelegate, objc_getProtocol("NSWindowDelegate"));
|
||||
class_replaceMethod(__NSWindowDelegate, sel_registerName("windowWillClose:"),
|
||||
(IMP)webview_window_will_close, "v@:@");
|
||||
objc_registerClassPair(__NSWindowDelegate);
|
||||
class_addProtocol(__NSWindowDelegate, objc_getProtocol("NSWindowDelegate"));
|
||||
class_replaceMethod(__NSWindowDelegate, sel_registerName("windowWillClose:"),
|
||||
(IMP)webview_window_will_close, "v@:@");
|
||||
objc_registerClassPair(__NSWindowDelegate);
|
||||
}
|
||||
|
||||
wv->priv.windowDelegate =
|
||||
objc_msgSend((id)__NSWindowDelegate, sel_registerName("new"));
|
||||
@@ -316,9 +369,13 @@ WEBVIEW_API int webview_init(webview_t w) {
|
||||
sel_registerName("stringWithUTF8String:"), wv->title);
|
||||
|
||||
CGRect r = CGRectMake(0, 0, wv->width, wv->height);
|
||||
|
||||
unsigned int style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
||||
NSWindowStyleMaskMiniaturizable;
|
||||
unsigned int style;
|
||||
if (wv->frameless) {
|
||||
style = NSWindowStyleMaskBorderless;
|
||||
} else {
|
||||
style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
||||
NSWindowStyleMaskMiniaturizable;
|
||||
}
|
||||
if (wv->resizable) {
|
||||
style = style | NSWindowStyleMaskResizable;
|
||||
}
|
||||
@@ -335,35 +392,40 @@ WEBVIEW_API int webview_init(webview_t w) {
|
||||
wv->priv.windowDelegate);
|
||||
objc_msgSend(wv->priv.window, sel_registerName("center"));
|
||||
|
||||
Class __WKUIDelegate =
|
||||
objc_allocateClassPair(objc_getClass("NSObject"), "__WKUIDelegate", 0);
|
||||
class_addProtocol(__WKUIDelegate, objc_getProtocol("WKUIDelegate"));
|
||||
class_addMethod(__WKUIDelegate,
|
||||
sel_registerName("webView:runOpenPanelWithParameters:"
|
||||
"initiatedByFrame:completionHandler:"),
|
||||
(IMP)run_open_panel, "v@:@@@?");
|
||||
class_addMethod(__WKUIDelegate,
|
||||
sel_registerName("webView:runJavaScriptAlertPanelWithMessage:"
|
||||
"initiatedByFrame:completionHandler:"),
|
||||
(IMP)run_alert_panel, "v@:@@@?");
|
||||
class_addMethod(
|
||||
__WKUIDelegate,
|
||||
sel_registerName("webView:runJavaScriptConfirmPanelWithMessage:"
|
||||
"initiatedByFrame:completionHandler:"),
|
||||
(IMP)run_confirmation_panel, "v@:@@@?");
|
||||
objc_registerClassPair(__WKUIDelegate);
|
||||
static Class __WKUIDelegate;
|
||||
if(__WKUIDelegate == NULL) {
|
||||
__WKUIDelegate = objc_allocateClassPair(objc_getClass("NSObject"), "__WKUIDelegate", 0);
|
||||
class_addProtocol(__WKUIDelegate, objc_getProtocol("WKUIDelegate"));
|
||||
class_addMethod(__WKUIDelegate,
|
||||
sel_registerName("webView:runOpenPanelWithParameters:"
|
||||
"initiatedByFrame:completionHandler:"),
|
||||
(IMP)run_open_panel, "v@:@@@?");
|
||||
class_addMethod(__WKUIDelegate,
|
||||
sel_registerName("webView:runJavaScriptAlertPanelWithMessage:"
|
||||
"initiatedByFrame:completionHandler:"),
|
||||
(IMP)run_alert_panel, "v@:@@@?");
|
||||
class_addMethod(
|
||||
__WKUIDelegate,
|
||||
sel_registerName("webView:runJavaScriptConfirmPanelWithMessage:"
|
||||
"initiatedByFrame:completionHandler:"),
|
||||
(IMP)run_confirmation_panel, "v@:@@@?");
|
||||
objc_registerClassPair(__WKUIDelegate);
|
||||
}
|
||||
id uiDel = objc_msgSend((id)__WKUIDelegate, sel_registerName("new"));
|
||||
|
||||
Class __WKNavigationDelegate = objc_allocateClassPair(
|
||||
static Class __WKNavigationDelegate;
|
||||
if(__WKNavigationDelegate == NULL) {
|
||||
__WKNavigationDelegate = objc_allocateClassPair(
|
||||
objc_getClass("NSObject"), "__WKNavigationDelegate", 0);
|
||||
class_addProtocol(__WKNavigationDelegate,
|
||||
objc_getProtocol("WKNavigationDelegate"));
|
||||
class_addMethod(
|
||||
__WKNavigationDelegate,
|
||||
sel_registerName(
|
||||
"webView:decidePolicyForNavigationResponse:decisionHandler:"),
|
||||
(IMP)make_nav_policy_decision, "v@:@@?");
|
||||
objc_registerClassPair(__WKNavigationDelegate);
|
||||
class_addProtocol(__WKNavigationDelegate,
|
||||
objc_getProtocol("WKNavigationDelegate"));
|
||||
class_addMethod(
|
||||
__WKNavigationDelegate,
|
||||
sel_registerName(
|
||||
"webView:decidePolicyForNavigationResponse:decisionHandler:"),
|
||||
(IMP)make_nav_policy_decision, "v@:@@?");
|
||||
objc_registerClassPair(__WKNavigationDelegate);
|
||||
}
|
||||
id navDel = objc_msgSend((id)__WKNavigationDelegate, sel_registerName("new"));
|
||||
|
||||
wv->priv.webview =
|
||||
@@ -376,7 +438,7 @@ WEBVIEW_API int webview_init(webview_t w) {
|
||||
|
||||
id nsURL = objc_msgSend((id)objc_getClass("NSURL"),
|
||||
sel_registerName("URLWithString:"),
|
||||
get_nsstring(webview_check_url(wv->url)));
|
||||
get_nsstring(wv->url == NULL ? "" : wv->url));
|
||||
|
||||
objc_msgSend(wv->priv.webview, sel_registerName("loadRequest:"),
|
||||
objc_msgSend((id)objc_getClass("NSURLRequest"),
|
||||
@@ -463,10 +525,10 @@ WEBVIEW_API int webview_loop(webview_t w, int blocking) {
|
||||
sel_registerName("distantFuture"))
|
||||
: objc_msgSend((id)objc_getClass("NSDate"),
|
||||
sel_registerName("distantPast")));
|
||||
|
||||
id app = objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication"));
|
||||
id event = objc_msgSend(
|
||||
objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication")),
|
||||
app,
|
||||
sel_registerName("nextEventMatchingMask:untilDate:inMode:dequeue:"),
|
||||
ULONG_MAX, until,
|
||||
objc_msgSend((id)objc_getClass("NSString"),
|
||||
@@ -479,7 +541,6 @@ WEBVIEW_API int webview_loop(webview_t w, int blocking) {
|
||||
sel_registerName("sharedApplication")),
|
||||
sel_registerName("sendEvent:"), event);
|
||||
}
|
||||
|
||||
return wv->priv.should_exit;
|
||||
}
|
||||
|
||||
@@ -537,87 +598,6 @@ WEBVIEW_API void webview_set_color(webview_t w, uint8_t r, uint8_t g,
|
||||
objc_msgSend(wv->priv.window,
|
||||
sel_registerName("setTitlebarAppearsTransparent:"), 1);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_dialog(webview_t w,
|
||||
enum webview_dialog_type dlgtype, int flags,
|
||||
const char *title, const char *arg,
|
||||
char *result, size_t resultsz) {
|
||||
struct cocoa_webview* wv = (struct cocoa_webview*)w;
|
||||
if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ||
|
||||
dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) {
|
||||
id panel = (id)objc_getClass("NSSavePanel");
|
||||
if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN) {
|
||||
id openPanel = objc_msgSend((id)objc_getClass("NSOpenPanel"),
|
||||
sel_registerName("openPanel"));
|
||||
if (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY) {
|
||||
objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 0);
|
||||
objc_msgSend(openPanel, sel_registerName("setCanChooseDirectories:"),
|
||||
1);
|
||||
} else {
|
||||
objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 1);
|
||||
objc_msgSend(openPanel, sel_registerName("setCanChooseDirectories:"),
|
||||
0);
|
||||
}
|
||||
objc_msgSend(openPanel, sel_registerName("setResolvesAliases:"), 0);
|
||||
objc_msgSend(openPanel, sel_registerName("setAllowsMultipleSelection:"),
|
||||
0);
|
||||
panel = openPanel;
|
||||
} else {
|
||||
panel = objc_msgSend((id)objc_getClass("NSSavePanel"),
|
||||
sel_registerName("savePanel"));
|
||||
}
|
||||
|
||||
objc_msgSend(panel, sel_registerName("setCanCreateDirectories:"), 1);
|
||||
objc_msgSend(panel, sel_registerName("setShowsHiddenFiles:"), 1);
|
||||
objc_msgSend(panel, sel_registerName("setExtensionHidden:"), 0);
|
||||
objc_msgSend(panel, sel_registerName("setCanSelectHiddenExtension:"), 0);
|
||||
objc_msgSend(panel, sel_registerName("setTreatsFilePackagesAsDirectories:"),
|
||||
1);
|
||||
objc_msgSend(
|
||||
panel, sel_registerName("beginSheetModalForWindow:completionHandler:"),
|
||||
wv->priv.window, ^(id result) {
|
||||
objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication")),
|
||||
sel_registerName("stopModalWithCode:"), result);
|
||||
});
|
||||
|
||||
if (objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication")),
|
||||
sel_registerName("runModalForWindow:"),
|
||||
panel) == (id)NSModalResponseOK) {
|
||||
id url = objc_msgSend(panel, sel_registerName("URL"));
|
||||
id path = objc_msgSend(url, sel_registerName("path"));
|
||||
const char *filename =
|
||||
(const char *)objc_msgSend(path, sel_registerName("UTF8String"));
|
||||
strlcpy(result, filename, resultsz);
|
||||
}
|
||||
} else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) {
|
||||
id a = objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new"));
|
||||
switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) {
|
||||
case WEBVIEW_DIALOG_FLAG_INFO:
|
||||
objc_msgSend(a, sel_registerName("setAlertStyle:"),
|
||||
NSAlertStyleInformational);
|
||||
break;
|
||||
case WEBVIEW_DIALOG_FLAG_WARNING:
|
||||
printf("Warning\n");
|
||||
objc_msgSend(a, sel_registerName("setAlertStyle:"), NSAlertStyleWarning);
|
||||
break;
|
||||
case WEBVIEW_DIALOG_FLAG_ERROR:
|
||||
printf("Error\n");
|
||||
objc_msgSend(a, sel_registerName("setAlertStyle:"), NSAlertStyleCritical);
|
||||
break;
|
||||
}
|
||||
objc_msgSend(a, sel_registerName("setShowsHelp:"), 0);
|
||||
objc_msgSend(a, sel_registerName("setShowsSuppressionButton:"), 0);
|
||||
objc_msgSend(a, sel_registerName("setMessageText:"), get_nsstring(title));
|
||||
objc_msgSend(a, sel_registerName("setInformativeText:"), get_nsstring(arg));
|
||||
objc_msgSend(a, sel_registerName("addButtonWithTitle:"),
|
||||
get_nsstring("OK"));
|
||||
objc_msgSend(a, sel_registerName("runModal"));
|
||||
objc_msgSend(a, sel_registerName("release"));
|
||||
}
|
||||
}
|
||||
|
||||
static void webview_dispatch_cb(void *arg) {
|
||||
struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)arg;
|
||||
(context->fn)(context->w, context->arg);
|
||||
@@ -635,16 +615,10 @@ WEBVIEW_API void webview_dispatch(webview_t w, webview_dispatch_fn fn,
|
||||
dispatch_async_f(dispatch_get_main_queue(), context, webview_dispatch_cb);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_terminate(webview_t w) {
|
||||
struct cocoa_webview* wv = (struct cocoa_webview*)w;
|
||||
wv->priv.should_exit = 1;
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_exit(webview_t w) {
|
||||
struct cocoa_webview* wv = (struct cocoa_webview*)w;
|
||||
id app = objc_msgSend((id)objc_getClass("NSApplication"),
|
||||
sel_registerName("sharedApplication"));
|
||||
objc_msgSend(app, sel_registerName("terminate:"), app);
|
||||
wv->external_invoke_cb = NULL;
|
||||
objc_msgSend(wv->priv.window, sel_registerName("close"));
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_print_log(const char *s) { printf("%s\n", s); }
|
||||
WEBVIEW_API void webview_print_log(const char *s) { printf("%s\n", s); }
|
||||
|
||||
@@ -41,9 +41,7 @@ inline std::string url_decode(const char *s)
|
||||
size_t length = strlen(s);
|
||||
for (unsigned int i = 0; i < length; i++) {
|
||||
if (s[i] == '%') {
|
||||
int n;
|
||||
sscanf(s + i + 1, "%2x", &n);
|
||||
decoded.push_back(static_cast<char>(n));
|
||||
decoded.push_back(hex2char(s + i + 1));
|
||||
i = i + 2;
|
||||
} else if (s[i] == '+') {
|
||||
decoded.push_back(' ');
|
||||
@@ -66,33 +64,101 @@ inline std::string html_from_uri(const char *s)
|
||||
|
||||
LRESULT CALLBACK WebviewWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
|
||||
class browser_window {
|
||||
private:
|
||||
bool EnableDpiAwareness() {
|
||||
auto lib_user32 = GetModuleHandleW(L"user32.dll");
|
||||
if(lib_user32) {
|
||||
auto fn_set_thread_dpi_awareness_context =
|
||||
reinterpret_cast<decltype(&SetThreadDpiAwarenessContext)>(
|
||||
GetProcAddress(lib_user32, "SetThreadDpiAwarenessContext")
|
||||
);
|
||||
if (
|
||||
fn_set_thread_dpi_awareness_context
|
||||
&& fn_set_thread_dpi_awareness_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Don't worry when SetThreadDpiAwarenessContext is not available. If
|
||||
// it's not available, we are not on a recent windows 10, and we don't
|
||||
// have edge...
|
||||
return false;
|
||||
}
|
||||
int MyGetDpiForWindow(HWND hWnd) {
|
||||
auto lib_user32 = GetModuleHandleW(L"user32.dll");
|
||||
if (lib_user32) {
|
||||
auto fn_get_dpi_for_window = reinterpret_cast<decltype(&GetDpiForWindow)>(
|
||||
GetProcAddress(lib_user32, "GetDpiForWindow")
|
||||
);
|
||||
if (fn_get_dpi_for_window) {
|
||||
return fn_get_dpi_for_window(hWnd);
|
||||
}
|
||||
}
|
||||
// Again, don't worry when GetDpiForWindow is not available. If we are
|
||||
// not on Windows 10, we don't have edge...
|
||||
return 96;
|
||||
}
|
||||
public:
|
||||
browser_window(msg_cb_t cb, const char* title, int width, int height, bool resizable)
|
||||
browser_window(msg_cb_t cb, const char* title, int width, int height, bool resizable, bool frameless)
|
||||
: m_cb(cb)
|
||||
{
|
||||
HINSTANCE hInstance = GetModuleHandle(nullptr);
|
||||
|
||||
HICON winresIcon = (HICON)LoadImage(
|
||||
hInstance,
|
||||
(LPWSTR)(1),
|
||||
IMAGE_ICON,
|
||||
0,
|
||||
0,
|
||||
LR_DEFAULTSIZE
|
||||
);
|
||||
|
||||
WNDCLASSEX wc;
|
||||
ZeroMemory(&wc, sizeof(WNDCLASSEX));
|
||||
wc.cbSize = sizeof(WNDCLASSEX);
|
||||
wc.hInstance = hInstance;
|
||||
wc.lpfnWndProc = WebviewWndProc;
|
||||
wc.lpszClassName = L"webview";
|
||||
wc.hIcon = winresIcon;
|
||||
RegisterClassEx(&wc);
|
||||
|
||||
EnableDpiAwareness();
|
||||
|
||||
DWORD style = WS_OVERLAPPEDWINDOW;
|
||||
if (!resizable) {
|
||||
style = WS_OVERLAPPED | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU;
|
||||
style &= ~(WS_SIZEBOX);
|
||||
}
|
||||
|
||||
if (frameless) {
|
||||
style &= ~(WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
|
||||
}
|
||||
|
||||
// Create window first, because we need the window to get DPI for the window.
|
||||
BSTR window_title = webview_to_bstr(title);
|
||||
m_window = CreateWindowEx(0, L"webview", window_title, style, 0, 0, 0, 0,
|
||||
HWND_DESKTOP, NULL, hInstance, (void *)this);
|
||||
SysFreeString(window_title);
|
||||
|
||||
// Have to call this before SetWindowPos or it will crash!
|
||||
SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this);
|
||||
if (frameless)
|
||||
{
|
||||
SetWindowLongPtr(m_window, GWL_STYLE, style);
|
||||
}
|
||||
this->saved_style = style;
|
||||
|
||||
UINT dpi = MyGetDpiForWindow(m_window);
|
||||
if (dpi == 0) {
|
||||
dpi = 96;
|
||||
}
|
||||
|
||||
RECT clientRect;
|
||||
RECT rect;
|
||||
rect.left = 0;
|
||||
rect.top = 0;
|
||||
rect.right = width;
|
||||
rect.bottom = height;
|
||||
rect.right = MulDiv(width, dpi, 96);
|
||||
rect.bottom = MulDiv(height, dpi, 96);
|
||||
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, 0);
|
||||
|
||||
GetClientRect(GetDesktopWindow(), &clientRect);
|
||||
int left = (clientRect.right / 2) - ((rect.right - rect.left) / 2);
|
||||
int top = (clientRect.bottom / 2) - ((rect.bottom - rect.top) / 2);
|
||||
@@ -101,15 +167,10 @@ public:
|
||||
rect.bottom = rect.bottom - rect.top + top;
|
||||
rect.top = top;
|
||||
|
||||
BSTR window_title = webview_to_bstr(title);
|
||||
m_window = CreateWindowEx(0, L"webview", window_title, style, rect.left, rect.top,
|
||||
rect.right - rect.left, rect.bottom - rect.top,
|
||||
HWND_DESKTOP, NULL, hInstance, (void *)this);
|
||||
SysFreeString(window_title);
|
||||
// Set position, size and show window *atomically*.
|
||||
SetWindowPos(m_window, HWND_TOP, rect.left, rect.top,
|
||||
rect.right - rect.left, rect.bottom - rect.top, SWP_SHOWWINDOW);
|
||||
|
||||
SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this);
|
||||
|
||||
ShowWindow(m_window, SW_SHOW);
|
||||
UpdateWindow(m_window);
|
||||
SetFocus(m_window);
|
||||
}
|
||||
@@ -146,15 +207,16 @@ public:
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void terminate() { PostQuitMessage(0); }
|
||||
void exit() { PostQuitMessage(0); }
|
||||
void dispatch(dispatch_fn_t f)
|
||||
{
|
||||
|
||||
PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f));
|
||||
}
|
||||
|
||||
void set_title(const char* title)
|
||||
{
|
||||
|
||||
BSTR window_title = webview_to_bstr(title);
|
||||
SetWindowText(m_window, window_title);
|
||||
SysFreeString(window_title);
|
||||
@@ -162,6 +224,7 @@ public:
|
||||
|
||||
void set_size(int width, int height)
|
||||
{
|
||||
|
||||
RECT r;
|
||||
r.left = 50;
|
||||
r.top = 50;
|
||||
@@ -174,6 +237,7 @@ public:
|
||||
|
||||
void set_fullscreen(bool fullscreen)
|
||||
{
|
||||
|
||||
if (this->is_fullscreen == fullscreen) {
|
||||
return;
|
||||
}
|
||||
@@ -215,6 +279,7 @@ public:
|
||||
|
||||
void set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
|
||||
{
|
||||
|
||||
HBRUSH brush = CreateSolidBrush(RGB(r, g, b));
|
||||
SetClassLongPtr(this->m_window, GCLP_HBRBACKGROUND, (LONG_PTR)brush);
|
||||
}
|
||||
@@ -238,11 +303,20 @@ LRESULT CALLBACK WebviewWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case WM_SIZE:
|
||||
w->resize();
|
||||
break;
|
||||
case WM_DPICHANGED: {
|
||||
auto rect = reinterpret_cast<LPRECT>(lp);
|
||||
auto x = rect->left;
|
||||
auto y = rect->top;
|
||||
auto w = rect->right - x;
|
||||
auto h = rect->bottom - y;
|
||||
SetWindowPos(hwnd, nullptr, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
break;
|
||||
}
|
||||
case WM_CLOSE:
|
||||
DestroyWindow(hwnd);
|
||||
break;
|
||||
case WM_DESTROY:
|
||||
w->terminate();
|
||||
w->exit();
|
||||
break;
|
||||
default:
|
||||
return DefWindowProc(hwnd, msg, wp, lp);
|
||||
@@ -257,8 +331,8 @@ using namespace Windows::Web::UI::Interop;
|
||||
|
||||
class webview : public browser_window {
|
||||
public:
|
||||
webview(webview_external_invoke_cb_t invoke_cb, const char* title, int width, int height, bool resizable, bool debug)
|
||||
: browser_window(std::bind(&webview::on_message, this, std::placeholders::_1), title, width, height, resizable)
|
||||
webview(webview_external_invoke_cb_t invoke_cb, const char* title, int width, int height, bool resizable, bool debug, bool frameless)
|
||||
: browser_window(std::bind(&webview::on_message, this, std::placeholders::_1), title, width, height, resizable, frameless)
|
||||
, invoke_cb(invoke_cb)
|
||||
{
|
||||
init_apartment(winrt::apartment_type::single_threaded);
|
||||
@@ -292,6 +366,7 @@ public:
|
||||
|
||||
void navigate(const char* url)
|
||||
{
|
||||
|
||||
std::string html = html_from_uri(url);
|
||||
if (html != "") {
|
||||
m_webview.NavigateToString(winrt::to_hstring(html.c_str()));
|
||||
@@ -302,18 +377,19 @@ public:
|
||||
}
|
||||
void init(const char* js)
|
||||
{
|
||||
|
||||
init_js.append("(function(){")
|
||||
.append(js)
|
||||
.append("})();");
|
||||
}
|
||||
void eval(const char* js)
|
||||
{
|
||||
|
||||
m_webview.InvokeScriptAsync(
|
||||
L"eval", single_threaded_vector<hstring>({ winrt::to_hstring(js) }));
|
||||
}
|
||||
|
||||
void terminate() {
|
||||
browser_window::terminate();
|
||||
void exit() {
|
||||
m_webview.Close();
|
||||
}
|
||||
|
||||
@@ -324,6 +400,7 @@ public:
|
||||
private:
|
||||
void on_message(const char* msg)
|
||||
{
|
||||
|
||||
this->invoke_cb(this, msg);
|
||||
}
|
||||
|
||||
@@ -355,103 +432,6 @@ static inline char *webview_from_utf16(WCHAR *ws) {
|
||||
return s;
|
||||
}
|
||||
|
||||
#include <shobjidl.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define iid_ref(x) &(x)
|
||||
#define iid_unref(x) *(x)
|
||||
#else
|
||||
#define iid_ref(x) (x)
|
||||
#define iid_unref(x) (x)
|
||||
#endif
|
||||
|
||||
/* These are missing parts from MinGW */
|
||||
#ifndef __IFileDialog_INTERFACE_DEFINED__
|
||||
#define __IFileDialog_INTERFACE_DEFINED__
|
||||
enum _FILEOPENDIALOGOPTIONS {
|
||||
FOS_OVERWRITEPROMPT = 0x2,
|
||||
FOS_STRICTFILETYPES = 0x4,
|
||||
FOS_NOCHANGEDIR = 0x8,
|
||||
FOS_PICKFOLDERS = 0x20,
|
||||
FOS_FORCEFILESYSTEM = 0x40,
|
||||
FOS_ALLNONSTORAGEITEMS = 0x80,
|
||||
FOS_NOVALIDATE = 0x100,
|
||||
FOS_ALLOWMULTISELECT = 0x200,
|
||||
FOS_PATHMUSTEXIST = 0x800,
|
||||
FOS_FILEMUSTEXIST = 0x1000,
|
||||
FOS_CREATEPROMPT = 0x2000,
|
||||
FOS_SHAREAWARE = 0x4000,
|
||||
FOS_NOREADONLYRETURN = 0x8000,
|
||||
FOS_NOTESTFILECREATE = 0x10000,
|
||||
FOS_HIDEMRUPLACES = 0x20000,
|
||||
FOS_HIDEPINNEDPLACES = 0x40000,
|
||||
FOS_NODEREFERENCELINKS = 0x100000,
|
||||
FOS_DONTADDTORECENT = 0x2000000,
|
||||
FOS_FORCESHOWHIDDEN = 0x10000000,
|
||||
FOS_DEFAULTNOMINIMODE = 0x20000000,
|
||||
FOS_FORCEPREVIEWPANEON = 0x40000000
|
||||
};
|
||||
typedef DWORD FILEOPENDIALOGOPTIONS;
|
||||
typedef enum FDAP { FDAP_BOTTOM = 0, FDAP_TOP = 1 } FDAP;
|
||||
DEFINE_GUID(IID_IFileDialog, 0x42f85136, 0xdb7e, 0x439c, 0x85, 0xf1, 0xe4, 0x07,
|
||||
0x5d, 0x13, 0x5f, 0xc8);
|
||||
typedef struct IFileDialogVtbl {
|
||||
BEGIN_INTERFACE
|
||||
HRESULT(STDMETHODCALLTYPE *QueryInterface)
|
||||
(IFileDialog *This, REFIID riid, void **ppvObject);
|
||||
ULONG(STDMETHODCALLTYPE *AddRef)(IFileDialog *This);
|
||||
ULONG(STDMETHODCALLTYPE *Release)(IFileDialog *This);
|
||||
HRESULT(STDMETHODCALLTYPE *Show)(IFileDialog *This, HWND hwndOwner);
|
||||
HRESULT(STDMETHODCALLTYPE *SetFileTypes)
|
||||
(IFileDialog *This, UINT cFileTypes, const COMDLG_FILTERSPEC *rgFilterSpec);
|
||||
HRESULT(STDMETHODCALLTYPE *SetFileTypeIndex)
|
||||
(IFileDialog *This, UINT iFileType);
|
||||
HRESULT(STDMETHODCALLTYPE *GetFileTypeIndex)
|
||||
(IFileDialog *This, UINT *piFileType);
|
||||
HRESULT(STDMETHODCALLTYPE *Advise)
|
||||
(IFileDialog *This, IFileDialogEvents *pfde, DWORD *pdwCookie);
|
||||
HRESULT(STDMETHODCALLTYPE *Unadvise)(IFileDialog *This, DWORD dwCookie);
|
||||
HRESULT(STDMETHODCALLTYPE *SetOptions)
|
||||
(IFileDialog *This, FILEOPENDIALOGOPTIONS fos);
|
||||
HRESULT(STDMETHODCALLTYPE *GetOptions)
|
||||
(IFileDialog *This, FILEOPENDIALOGOPTIONS *pfos);
|
||||
HRESULT(STDMETHODCALLTYPE *SetDefaultFolder)
|
||||
(IFileDialog *This, IShellItem *psi);
|
||||
HRESULT(STDMETHODCALLTYPE *SetFolder)(IFileDialog *This, IShellItem *psi);
|
||||
HRESULT(STDMETHODCALLTYPE *GetFolder)(IFileDialog *This, IShellItem **ppsi);
|
||||
HRESULT(STDMETHODCALLTYPE *GetCurrentSelection)
|
||||
(IFileDialog *This, IShellItem **ppsi);
|
||||
HRESULT(STDMETHODCALLTYPE *SetFileName)(IFileDialog *This, LPCWSTR pszName);
|
||||
HRESULT(STDMETHODCALLTYPE *GetFileName)(IFileDialog *This, LPWSTR *pszName);
|
||||
HRESULT(STDMETHODCALLTYPE *SetTitle)(IFileDialog *This, LPCWSTR pszTitle);
|
||||
HRESULT(STDMETHODCALLTYPE *SetOkButtonLabel)
|
||||
(IFileDialog *This, LPCWSTR pszText);
|
||||
HRESULT(STDMETHODCALLTYPE *SetFileNameLabel)
|
||||
(IFileDialog *This, LPCWSTR pszLabel);
|
||||
HRESULT(STDMETHODCALLTYPE *GetResult)(IFileDialog *This, IShellItem **ppsi);
|
||||
HRESULT(STDMETHODCALLTYPE *AddPlace)
|
||||
(IFileDialog *This, IShellItem *psi, FDAP fdap);
|
||||
HRESULT(STDMETHODCALLTYPE *SetDefaultExtension)
|
||||
(IFileDialog *This, LPCWSTR pszDefaultExtension);
|
||||
HRESULT(STDMETHODCALLTYPE *Close)(IFileDialog *This, HRESULT hr);
|
||||
HRESULT(STDMETHODCALLTYPE *SetClientGuid)(IFileDialog *This, REFGUID guid);
|
||||
HRESULT(STDMETHODCALLTYPE *ClearClientData)(IFileDialog *This);
|
||||
HRESULT(STDMETHODCALLTYPE *SetFilter)
|
||||
(IFileDialog *This, IShellItemFilter *pFilter);
|
||||
END_INTERFACE
|
||||
} IFileDialogVtbl;
|
||||
interface IFileDialog {
|
||||
CONST_VTBL IFileDialogVtbl *lpVtbl;
|
||||
};
|
||||
DEFINE_GUID(IID_IFileOpenDialog, 0xd57c7288, 0xd4ad, 0x4768, 0xbe, 0x02, 0x9d,
|
||||
0x96, 0x95, 0x32, 0xd9, 0x60);
|
||||
DEFINE_GUID(IID_IFileSaveDialog, 0x84bccd23, 0x5fde, 0x4cdb, 0xae, 0xa4, 0xaf,
|
||||
0x64, 0xb8, 0x3d, 0x78, 0xab);
|
||||
#endif
|
||||
|
||||
|
||||
// HERE
|
||||
|
||||
WEBVIEW_API void webview_run(webview_t w)
|
||||
{
|
||||
static_cast<webview::webview*>(w)->run();
|
||||
@@ -484,114 +464,16 @@ WEBVIEW_API void webview_set_color(webview_t w, uint8_t r, uint8_t g,
|
||||
static_cast<webview::webview*>(w)->set_color(r, g, b, a);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_dialog(webview_t w,
|
||||
enum webview_dialog_type dlgtype, int flags,
|
||||
const char *title, const char *arg,
|
||||
char *result, size_t resultsz) {
|
||||
HWND hwnd = static_cast<webview::webview*>(w)->m_window;
|
||||
if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ||
|
||||
dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) {
|
||||
IFileDialog *dlg = NULL;
|
||||
IShellItem *res = NULL;
|
||||
WCHAR *ws = NULL;
|
||||
char *s = NULL;
|
||||
FILEOPENDIALOGOPTIONS opts = 0, add_opts = 0;
|
||||
if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN) {
|
||||
if (CoCreateInstance(
|
||||
iid_unref(&CLSID_FileOpenDialog), NULL, CLSCTX_INPROC_SERVER,
|
||||
iid_unref(&IID_IFileOpenDialog), (void **)&dlg) != S_OK) {
|
||||
goto error_dlg;
|
||||
}
|
||||
if (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY) {
|
||||
add_opts |= FOS_PICKFOLDERS;
|
||||
}
|
||||
add_opts |= FOS_NOCHANGEDIR | FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE |
|
||||
FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_SHAREAWARE |
|
||||
FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS |
|
||||
FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE;
|
||||
} else {
|
||||
if (CoCreateInstance(
|
||||
iid_unref(&CLSID_FileSaveDialog), NULL, CLSCTX_INPROC_SERVER,
|
||||
iid_unref(&IID_IFileSaveDialog), (void **)&dlg) != S_OK) {
|
||||
goto error_dlg;
|
||||
}
|
||||
add_opts |= FOS_OVERWRITEPROMPT | FOS_NOCHANGEDIR |
|
||||
FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE | FOS_SHAREAWARE |
|
||||
FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS |
|
||||
FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE;
|
||||
}
|
||||
if (dlg->GetOptions(&opts) != S_OK) {
|
||||
goto error_dlg;
|
||||
}
|
||||
opts &= ~FOS_NOREADONLYRETURN;
|
||||
opts |= add_opts;
|
||||
if (dlg->SetOptions(opts) != S_OK) {
|
||||
goto error_dlg;
|
||||
}
|
||||
if (dlg->Show(hwnd) != S_OK) {
|
||||
goto error_dlg;
|
||||
}
|
||||
if (dlg->GetResult(&res) != S_OK) {
|
||||
goto error_dlg;
|
||||
}
|
||||
if (res->GetDisplayName(SIGDN_FILESYSPATH, &ws) != S_OK) {
|
||||
goto error_result;
|
||||
}
|
||||
s = webview_from_utf16(ws);
|
||||
CoTaskMemFree(ws);
|
||||
if (!s) goto error_result;
|
||||
strncpy(result, s, resultsz);
|
||||
GlobalFree(s);
|
||||
result[resultsz - 1] = '\0';
|
||||
error_result:
|
||||
res->Release();
|
||||
error_dlg:
|
||||
dlg->Release();
|
||||
return;
|
||||
} else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) {
|
||||
#if 0
|
||||
/* MinGW often doesn't contain TaskDialog, we'll use MessageBox for now */
|
||||
WCHAR *wtitle = webview_to_utf16(title);
|
||||
WCHAR *warg = webview_to_utf16(arg);
|
||||
TaskDialog(hwnd, NULL, NULL, wtitle, warg, 0, NULL, NULL);
|
||||
GlobalFree(warg);
|
||||
GlobalFree(wtitle);
|
||||
#else
|
||||
UINT type = MB_OK;
|
||||
switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) {
|
||||
case WEBVIEW_DIALOG_FLAG_INFO:
|
||||
type |= MB_ICONINFORMATION;
|
||||
break;
|
||||
case WEBVIEW_DIALOG_FLAG_WARNING:
|
||||
type |= MB_ICONWARNING;
|
||||
break;
|
||||
case WEBVIEW_DIALOG_FLAG_ERROR:
|
||||
type |= MB_ICONERROR;
|
||||
break;
|
||||
}
|
||||
BSTR box_title = webview_to_bstr(title);
|
||||
BSTR box_text = webview_to_bstr(arg);
|
||||
MessageBox(hwnd, box_text, box_title, type);
|
||||
SysFreeString(box_title);
|
||||
SysFreeString(box_text);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_dispatch(webview_t w, webview_dispatch_fn fn,
|
||||
void *arg)
|
||||
{
|
||||
static_cast<webview::webview*>(w)->dispatch([=]() { fn(w, arg); });
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_terminate(webview_t w)
|
||||
{
|
||||
static_cast<webview::webview*>(w)->terminate();
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_exit(webview_t w)
|
||||
{
|
||||
webview_terminate(w);
|
||||
webview::webview* wv = static_cast<webview::webview*>(w);
|
||||
DestroyWindow(wv->m_window);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_debug(const char *format, ...)
|
||||
@@ -609,9 +491,11 @@ WEBVIEW_API void* webview_get_user_data(webview_t w)
|
||||
return static_cast<webview::webview*>(w)->get_user_data();
|
||||
}
|
||||
|
||||
WEBVIEW_API webview_t webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, webview_external_invoke_cb_t external_invoke_cb, void* userdata)
|
||||
WEBVIEW_API webview_t webview_new(
|
||||
const char* title, const char* url, int width, int height, int resizable, int debug,
|
||||
int frameless, webview_external_invoke_cb_t external_invoke_cb, void* userdata)
|
||||
{
|
||||
auto w = new webview::webview(external_invoke_cb, title, width, height, resizable, debug);
|
||||
auto w = new webview::webview(external_invoke_cb, title, width, height, resizable, debug, frameless);
|
||||
w->set_user_data(userdata);
|
||||
w->navigate(url);
|
||||
return w;
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
#include "webview.h"
|
||||
|
||||
#include <JavaScriptCore/JavaScript.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
struct webview_priv {
|
||||
GtkWidget *window;
|
||||
GtkWidget *scroller;
|
||||
GtkWidget *webview;
|
||||
GtkWidget *inspector_window;
|
||||
GAsyncQueue *queue;
|
||||
int ready;
|
||||
int js_busy;
|
||||
int should_exit;
|
||||
};
|
||||
|
||||
struct gtk_webview {
|
||||
const char *url;
|
||||
const char *title;
|
||||
int width;
|
||||
int height;
|
||||
int resizable;
|
||||
int debug;
|
||||
webview_external_invoke_cb_t external_invoke_cb;
|
||||
struct webview_priv priv;
|
||||
void *userdata;
|
||||
};
|
||||
|
||||
WEBVIEW_API void webview_free(webview_t w) {
|
||||
free(w);
|
||||
}
|
||||
|
||||
WEBVIEW_API void* webview_get_user_data(webview_t w) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
return wv->userdata;
|
||||
}
|
||||
|
||||
WEBVIEW_API webview_t webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, webview_external_invoke_cb_t external_invoke_cb, void* userdata) {
|
||||
struct gtk_webview* w = (struct gtk_webview*)calloc(1, sizeof(*w));
|
||||
w->width = width;
|
||||
w->height = height;
|
||||
w->title = title;
|
||||
w->url = url;
|
||||
w->resizable = resizable;
|
||||
w->debug = debug;
|
||||
w->external_invoke_cb = external_invoke_cb;
|
||||
w->userdata = userdata;
|
||||
if (webview_init(w) != 0) {
|
||||
webview_free(w);
|
||||
return NULL;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
static void external_message_received_cb(WebKitUserContentManager *m,
|
||||
WebKitJavascriptResult *r,
|
||||
gpointer arg) {
|
||||
(void)m;
|
||||
struct gtk_webview *w = (struct gtk_webview *)arg;
|
||||
if (w->external_invoke_cb == NULL) {
|
||||
return;
|
||||
}
|
||||
JSGlobalContextRef context = webkit_javascript_result_get_global_context(r);
|
||||
JSValueRef value = webkit_javascript_result_get_value(r);
|
||||
JSStringRef js = JSValueToStringCopy(context, value, NULL);
|
||||
size_t n = JSStringGetMaximumUTF8CStringSize(js);
|
||||
char *s = g_new(char, n);
|
||||
JSStringGetUTF8CString(js, s, n);
|
||||
w->external_invoke_cb(w, s);
|
||||
JSStringRelease(js);
|
||||
g_free(s);
|
||||
}
|
||||
|
||||
static void webview_load_changed_cb(WebKitWebView *webview,
|
||||
WebKitLoadEvent event, gpointer arg) {
|
||||
(void)webview;
|
||||
struct gtk_webview *w = (struct gtk_webview *)arg;
|
||||
if (event == WEBKIT_LOAD_FINISHED) {
|
||||
w->priv.ready = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void webview_destroy_cb(GtkWidget *widget, gpointer arg) {
|
||||
(void)widget;
|
||||
webview_terminate((webview_t)arg);
|
||||
}
|
||||
|
||||
static gboolean webview_context_menu_cb(WebKitWebView *webview,
|
||||
GtkWidget *default_menu,
|
||||
WebKitHitTestResult *hit_test_result,
|
||||
gboolean triggered_with_keyboard,
|
||||
gpointer userdata) {
|
||||
(void)webview;
|
||||
(void)default_menu;
|
||||
(void)hit_test_result;
|
||||
(void)triggered_with_keyboard;
|
||||
(void)userdata;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int webview_init(struct gtk_webview *w) {
|
||||
if (gtk_init_check(0, NULL) == FALSE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
w->priv.ready = 0;
|
||||
w->priv.should_exit = 0;
|
||||
w->priv.queue = g_async_queue_new();
|
||||
w->priv.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_title(GTK_WINDOW(w->priv.window), w->title);
|
||||
|
||||
if (w->resizable) {
|
||||
gtk_window_set_default_size(GTK_WINDOW(w->priv.window), w->width,
|
||||
w->height);
|
||||
} else {
|
||||
gtk_widget_set_size_request(w->priv.window, w->width, w->height);
|
||||
}
|
||||
gtk_window_set_resizable(GTK_WINDOW(w->priv.window), !!w->resizable);
|
||||
gtk_window_set_position(GTK_WINDOW(w->priv.window), GTK_WIN_POS_CENTER);
|
||||
|
||||
w->priv.scroller = gtk_scrolled_window_new(NULL, NULL);
|
||||
gtk_container_add(GTK_CONTAINER(w->priv.window), w->priv.scroller);
|
||||
|
||||
WebKitUserContentManager *m = webkit_user_content_manager_new();
|
||||
webkit_user_content_manager_register_script_message_handler(m, "external");
|
||||
g_signal_connect(m, "script-message-received::external",
|
||||
G_CALLBACK(external_message_received_cb), w);
|
||||
|
||||
w->priv.webview = webkit_web_view_new_with_user_content_manager(m);
|
||||
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(w->priv.webview),
|
||||
webview_check_url(w->url));
|
||||
g_signal_connect(G_OBJECT(w->priv.webview), "load-changed",
|
||||
G_CALLBACK(webview_load_changed_cb), w);
|
||||
gtk_container_add(GTK_CONTAINER(w->priv.scroller), w->priv.webview);
|
||||
|
||||
WebKitSettings *settings =
|
||||
webkit_web_view_get_settings(WEBKIT_WEB_VIEW(w->priv.webview));
|
||||
|
||||
// Enable webgl and canvas features.
|
||||
webkit_settings_set_enable_webgl(settings, true);
|
||||
webkit_settings_set_enable_accelerated_2d_canvas(settings, true);
|
||||
|
||||
if (w->debug) {
|
||||
WebKitSettings *settings =
|
||||
webkit_web_view_get_settings(WEBKIT_WEB_VIEW(w->priv.webview));
|
||||
webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
|
||||
webkit_settings_set_enable_developer_extras(settings, true);
|
||||
} else {
|
||||
g_signal_connect(G_OBJECT(w->priv.webview), "context-menu",
|
||||
G_CALLBACK(webview_context_menu_cb), w);
|
||||
}
|
||||
|
||||
gtk_widget_show_all(w->priv.window);
|
||||
|
||||
webkit_web_view_run_javascript(
|
||||
WEBKIT_WEB_VIEW(w->priv.webview),
|
||||
"window.external={invoke:function(x){"
|
||||
"window.webkit.messageHandlers.external.postMessage(x);}}",
|
||||
NULL, NULL, NULL);
|
||||
|
||||
g_signal_connect(G_OBJECT(w->priv.window), "destroy",
|
||||
G_CALLBACK(webview_destroy_cb), w);
|
||||
return 0;
|
||||
}
|
||||
|
||||
WEBVIEW_API int webview_loop(webview_t w, int blocking) {
|
||||
gtk_main_iteration_do(blocking);
|
||||
return ((struct gtk_webview*)w)->priv.should_exit;
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_set_title(webview_t w, const char *title) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
gtk_window_set_title(GTK_WINDOW(wv->priv.window), title);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_set_fullscreen(webview_t w, int fullscreen) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
if (fullscreen) {
|
||||
gtk_window_fullscreen(GTK_WINDOW(wv->priv.window));
|
||||
} else {
|
||||
gtk_window_unfullscreen(GTK_WINDOW(wv->priv.window));
|
||||
}
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_set_color(webview_t w, uint8_t r, uint8_t g,
|
||||
uint8_t b, uint8_t a) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
GdkRGBA color = {r / 255.0, g / 255.0, b / 255.0, a / 255.0};
|
||||
webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(wv->priv.webview),
|
||||
&color);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_dialog(webview_t w,
|
||||
enum webview_dialog_type dlgtype, int flags,
|
||||
const char *title, const char *arg,
|
||||
char *result, size_t resultsz) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
GtkWidget *dlg;
|
||||
if (result != NULL) {
|
||||
result[0] = '\0';
|
||||
}
|
||||
if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ||
|
||||
dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) {
|
||||
dlg = gtk_file_chooser_dialog_new(
|
||||
title, GTK_WINDOW(wv->priv.window),
|
||||
(dlgtype == WEBVIEW_DIALOG_TYPE_OPEN
|
||||
? (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY
|
||||
? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
|
||||
: GTK_FILE_CHOOSER_ACTION_OPEN)
|
||||
: GTK_FILE_CHOOSER_ACTION_SAVE),
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
(dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ? "_Open" : "_Save"),
|
||||
GTK_RESPONSE_ACCEPT, NULL);
|
||||
gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dlg), FALSE);
|
||||
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dlg), FALSE);
|
||||
gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dlg), TRUE);
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE);
|
||||
gtk_file_chooser_set_create_folders(GTK_FILE_CHOOSER(dlg), TRUE);
|
||||
gint response = gtk_dialog_run(GTK_DIALOG(dlg));
|
||||
if (response == GTK_RESPONSE_ACCEPT) {
|
||||
gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
|
||||
g_strlcpy(result, filename, resultsz);
|
||||
g_free(filename);
|
||||
}
|
||||
gtk_widget_destroy(dlg);
|
||||
} else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) {
|
||||
GtkMessageType type = GTK_MESSAGE_OTHER;
|
||||
switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) {
|
||||
case WEBVIEW_DIALOG_FLAG_INFO:
|
||||
type = GTK_MESSAGE_INFO;
|
||||
break;
|
||||
case WEBVIEW_DIALOG_FLAG_WARNING:
|
||||
type = GTK_MESSAGE_WARNING;
|
||||
break;
|
||||
case WEBVIEW_DIALOG_FLAG_ERROR:
|
||||
type = GTK_MESSAGE_ERROR;
|
||||
break;
|
||||
}
|
||||
dlg = gtk_message_dialog_new(GTK_WINDOW(wv->priv.window), GTK_DIALOG_MODAL,
|
||||
type, GTK_BUTTONS_OK, "%s", title);
|
||||
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dlg), "%s",
|
||||
arg);
|
||||
gtk_dialog_run(GTK_DIALOG(dlg));
|
||||
gtk_widget_destroy(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
static void webview_eval_finished(GObject *object, GAsyncResult *result,
|
||||
gpointer userdata) {
|
||||
(void)object;
|
||||
(void)result;
|
||||
struct gtk_webview *w = (struct gtk_webview *)userdata;
|
||||
w->priv.js_busy = 0;
|
||||
}
|
||||
|
||||
WEBVIEW_API int webview_eval(webview_t w, const char *js) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
while (wv->priv.ready == 0) {
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
}
|
||||
wv->priv.js_busy = 1;
|
||||
webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(wv->priv.webview), js, NULL,
|
||||
webview_eval_finished, w);
|
||||
while (wv->priv.js_busy) {
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static gboolean webview_dispatch_wrapper(gpointer userdata) {
|
||||
struct gtk_webview *w = (struct gtk_webview *)userdata;
|
||||
for (;;) {
|
||||
struct webview_dispatch_arg *arg =
|
||||
(struct webview_dispatch_arg *)g_async_queue_try_pop(w->priv.queue);
|
||||
if (arg == NULL) {
|
||||
break;
|
||||
}
|
||||
(arg->fn)(w, arg->arg);
|
||||
g_free(arg);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_dispatch(webview_t w, webview_dispatch_fn fn,
|
||||
void *arg) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
struct webview_dispatch_arg *context =
|
||||
(struct webview_dispatch_arg *)g_new(struct webview_dispatch_arg, 1);
|
||||
context->w = w;
|
||||
context->arg = arg;
|
||||
context->fn = fn;
|
||||
g_async_queue_lock(wv->priv.queue);
|
||||
g_async_queue_push_unlocked(wv->priv.queue, context);
|
||||
if (g_async_queue_length_unlocked(wv->priv.queue) == 1) {
|
||||
gdk_threads_add_idle(webview_dispatch_wrapper, w);
|
||||
}
|
||||
g_async_queue_unlock(wv->priv.queue);
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_terminate(webview_t w) {
|
||||
struct gtk_webview *wv = (struct webview *)w;
|
||||
wv->priv.should_exit = 1;
|
||||
}
|
||||
|
||||
WEBVIEW_API void webview_exit(webview_t w) { (void)w; }
|
||||
WEBVIEW_API void webview_print_log(const char *s) {
|
||||
fprintf(stderr, "%s\n", s);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user