#![windows_subsystem = "windows"]
extern crate serde;
extern crate serde_json;
extern crate webview2_com;
extern crate windows;
use std::{cell::RefCell, collections::HashMap, fmt, mem, ptr, rc::Rc, sync::mpsc};
use serde::Deserialize;
use serde_json::{Number, Value};
use windows::{
core::*,
Win32::{
Foundation::{E_POINTER, HINSTANCE, HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM},
Graphics::Gdi,
System::{Com::*, LibraryLoader, Threading},
UI::{
HiDpi,
Input::KeyboardAndMouse,
WindowsAndMessaging::{self, MSG, WINDOW_LONG_PTR_INDEX, WNDCLASSW},
},
},
};
use webview2_com::{Microsoft::Web::WebView2::Win32::*, *};
fn main() -> Result<()> {
unsafe {
CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()?;
}
set_process_dpi_awareness()?;
let webview = WebView::create(None, true)?;
webview.bind("hostCallback", move |request| {
if let [Value::String(str), Value::Number(a), Value::Number(b)] = &request[..] {
if str == "Add" {
let result = a.as_f64().unwrap_or(0f64) + b.as_f64().unwrap_or(0f64);
let result = Number::from_f64(result);
if let Some(result) = result {
return Ok(Value::Number(result));
}
}
}
Err(Error::WebView2Error(webview2_com::Error::CallbackError(
String::from(r#"Usage: window.hostCallback("Add", a, b)"#),
)))
})?;
webview
.set_title("webview2-com example (crates/webview2-com/examples)")?
.init(
r#"window.hostCallback("Add", 2, 6).then(result => console.log(`Result: ${result}`));"#,
)?
.navigate("https://github.com/wravery/webview2-rs")?;
webview.run()
}
#[derive(Debug)]
pub enum Error {
WebView2Error(webview2_com::Error),
WindowsError(windows::core::Error),
JsonError(serde_json::Error),
LockError,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl From<webview2_com::Error> for Error {
fn from(err: webview2_com::Error) -> Self {
Self::WebView2Error(err)
}
}
impl From<windows::core::Error> for Error {
fn from(err: windows::core::Error) -> Self {
Self::WindowsError(err)
}
}
impl From<HRESULT> for Error {
fn from(err: HRESULT) -> Self {
Self::WindowsError(windows::core::Error::from(err))
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::JsonError(err)
}
}
impl<T> From<std::sync::PoisonError<T>> for Error {
fn from(_: std::sync::PoisonError<T>) -> Self {
Self::LockError
}
}
impl<T> From<std::sync::TryLockError<T>> for Error {
fn from(_: std::sync::TryLockError<T>) -> Self {
Self::LockError
}
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone)]
pub struct FrameWindow {
window: Rc<HWND>,
size: Rc<RefCell<SIZE>>,
}
impl FrameWindow {
fn new() -> Self {
let hwnd = {
let window_class = WNDCLASSW {
lpfnWndProc: Some(window_proc),
lpszClassName: w!("WebView"),
..Default::default()
};
unsafe {
WindowsAndMessaging::RegisterClassW(&window_class);
WindowsAndMessaging::CreateWindowExW(
Default::default(),
w!("WebView"),
w!("WebView"),
WindowsAndMessaging::WS_OVERLAPPEDWINDOW,
WindowsAndMessaging::CW_USEDEFAULT,
WindowsAndMessaging::CW_USEDEFAULT,
WindowsAndMessaging::CW_USEDEFAULT,
WindowsAndMessaging::CW_USEDEFAULT,
None,
None,
LibraryLoader::GetModuleHandleW(None)
.ok()
.map(|h| HINSTANCE(h.0)),
None,
)
}
};
FrameWindow {
window: Rc::new(hwnd.unwrap_or_default()),
size: Rc::new(RefCell::new(SIZE { cx: 0, cy: 0 })),
}
}
}
struct WebViewController(ICoreWebView2Controller);
type WebViewSender = mpsc::Sender<Box<dyn FnOnce(WebView) + Send>>;
type WebViewReceiver = mpsc::Receiver<Box<dyn FnOnce(WebView) + Send>>;
type BindingCallback = Box<dyn FnMut(Vec<Value>) -> Result<Value>>;
type BindingsMap = HashMap<String, BindingCallback>;
#[derive(Clone)]
pub struct WebView {
controller: Rc<WebViewController>,
webview: Rc<ICoreWebView2>,
tx: WebViewSender,
rx: Rc<WebViewReceiver>,
thread_id: u32,
bindings: Rc<RefCell<BindingsMap>>,
frame: Option<FrameWindow>,
parent: Rc<HWND>,
url: Rc<RefCell<String>>,
}
impl Drop for WebViewController {
fn drop(&mut self) {
unsafe { self.0.Close() }.unwrap();
}
}
#[derive(Debug, Deserialize)]
struct InvokeMessage {
id: u64,
method: String,
params: Vec<Value>,
}
impl WebView {
pub fn create(parent: Option<HWND>, debug: bool) -> Result<WebView> {
let (parent, frame) = match parent {
Some(hwnd) => (hwnd, None),
None => {
let frame = FrameWindow::new();
(*frame.window, Some(frame))
}
};
let environment = {
let (tx, rx) = mpsc::channel();
CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation(
Box::new(|environmentcreatedhandler| unsafe {
CreateCoreWebView2Environment(&environmentcreatedhandler)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |error_code, environment| {
error_code?;
tx.send(environment.ok_or_else(|| windows::core::Error::from(E_POINTER)))
.expect("send over mpsc channel");
Ok(())
}),
)?;
rx.recv()
.map_err(|_| Error::WebView2Error(webview2_com::Error::SendError))?
}?;
let controller = {
let (tx, rx) = mpsc::channel();
CreateCoreWebView2ControllerCompletedHandler::wait_for_async_operation(
Box::new(move |handler| unsafe {
environment
.CreateCoreWebView2Controller(parent, &handler)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(move |error_code, controller| {
error_code?;
tx.send(controller.ok_or_else(|| windows::core::Error::from(E_POINTER)))
.expect("send over mpsc channel");
Ok(())
}),
)?;
rx.recv()
.map_err(|_| Error::WebView2Error(webview2_com::Error::SendError))?
}?;
let size = get_window_size(parent);
let mut client_rect = RECT::default();
unsafe {
let _ = WindowsAndMessaging::GetClientRect(parent, &mut client_rect);
controller.SetBounds(RECT {
left: 0,
top: 0,
right: size.cx,
bottom: size.cy,
})?;
controller.SetIsVisible(true)?;
}
let webview = unsafe { controller.CoreWebView2()? };
if !debug {
unsafe {
let settings = webview.Settings()?;
settings.SetAreDefaultContextMenusEnabled(false)?;
settings.SetAreDevToolsEnabled(false)?;
}
}
if let Some(frame) = frame.as_ref() {
*frame.size.borrow_mut() = size;
}
let (tx, rx) = mpsc::channel();
let rx = Rc::new(rx);
let thread_id = unsafe { Threading::GetCurrentThreadId() };
let webview = WebView {
controller: Rc::new(WebViewController(controller)),
webview: Rc::new(webview),
tx,
rx,
thread_id,
bindings: Rc::new(RefCell::new(HashMap::new())),
frame,
parent: Rc::new(parent),
url: Rc::new(RefCell::new(String::new())),
};
webview
.init(r#"window.external = { invoke: s => window.chrome.webview.postMessage(s) };"#)?;
let bindings = webview.bindings.clone();
let bound = webview.clone();
unsafe {
let mut _token = 0;
webview.webview.add_WebMessageReceived(
&WebMessageReceivedEventHandler::create(Box::new(move |_webview, args| {
if let Some(args) = args {
let mut message = PWSTR(ptr::null_mut());
if args.WebMessageAsJson(&mut message).is_ok() {
let message = CoTaskMemPWSTR::from(message);
if let Ok(value) =
serde_json::from_str::<InvokeMessage>(&message.to_string())
{
let mut bindings = bindings.borrow_mut();
if let Some(f) = bindings.get_mut(&value.method) {
match (*f)(value.params) {
Ok(result) => bound.resolve(value.id, 0, result),
Err(err) => bound.resolve(
value.id,
1,
Value::String(format!("{err:#?}")),
),
}
.unwrap();
}
}
}
}
Ok(())
})),
&mut _token,
)?;
}
if webview.frame.is_some() {
WebView::set_window_webview(parent, Some(Box::new(webview.clone())));
}
Ok(webview)
}
pub fn run(self) -> Result<()> {
let webview = self.webview.as_ref();
let url = self.url.borrow().clone();
let (tx, rx) = mpsc::channel();
if !url.is_empty() {
let handler =
NavigationCompletedEventHandler::create(Box::new(move |_sender, _args| {
tx.send(()).expect("send over mpsc channel");
Ok(())
}));
let mut token = 0;
unsafe {
webview.add_NavigationCompleted(&handler, &mut token)?;
let url = CoTaskMemPWSTR::from(url.as_str());
webview.Navigate(*url.as_ref().as_pcwstr())?;
let result = webview2_com::wait_with_pump(rx);
webview.remove_NavigationCompleted(token)?;
result?;
}
}
if let Some(frame) = self.frame.as_ref() {
let hwnd = *frame.window;
unsafe {
let _ = WindowsAndMessaging::ShowWindow(hwnd, WindowsAndMessaging::SW_SHOW);
let _ = Gdi::UpdateWindow(hwnd);
let _ = KeyboardAndMouse::SetFocus(Some(hwnd));
}
}
let mut msg = MSG::default();
loop {
while let Ok(f) = self.rx.try_recv() {
(f)(self.clone());
}
unsafe {
let result = WindowsAndMessaging::GetMessageW(&mut msg, None, 0, 0).0;
match result {
-1 => break Err(windows::core::Error::from_thread().into()),
0 => break Ok(()),
_ => match msg.message {
WindowsAndMessaging::WM_APP => (),
_ => {
let _ = WindowsAndMessaging::TranslateMessage(&msg);
WindowsAndMessaging::DispatchMessageW(&msg);
}
},
}
}
}
}
pub fn terminate(self) -> Result<()> {
self.dispatch(|_webview| unsafe {
WindowsAndMessaging::PostQuitMessage(0);
})?;
if self.frame.is_some() {
WebView::set_window_webview(self.get_window(), None);
}
Ok(())
}
pub fn set_title(&self, title: &str) -> Result<&Self> {
if let Some(frame) = self.frame.as_ref() {
let title = CoTaskMemPWSTR::from(title);
unsafe {
let _ =
WindowsAndMessaging::SetWindowTextW(*frame.window, *title.as_ref().as_pcwstr());
}
}
Ok(self)
}
pub fn set_size(&self, width: i32, height: i32) -> Result<&Self> {
if let Some(frame) = self.frame.as_ref() {
*frame.size.borrow_mut() = SIZE {
cx: width,
cy: height,
};
unsafe {
self.controller.0.SetBounds(RECT {
left: 0,
top: 0,
right: width,
bottom: height,
})?;
let _ = WindowsAndMessaging::SetWindowPos(
*frame.window,
None,
0,
0,
width,
height,
WindowsAndMessaging::SWP_NOACTIVATE
| WindowsAndMessaging::SWP_NOZORDER
| WindowsAndMessaging::SWP_NOMOVE,
);
}
}
Ok(self)
}
pub fn get_window(&self) -> HWND {
*self.parent
}
pub fn navigate(&self, url: &str) -> Result<&Self> {
let url = url.into();
*self.url.borrow_mut() = url;
Ok(self)
}
pub fn init(&self, js: &str) -> Result<&Self> {
let webview = self.webview.clone();
let js = String::from(js);
AddScriptToExecuteOnDocumentCreatedCompletedHandler::wait_for_async_operation(
Box::new(move |handler| unsafe {
let js = CoTaskMemPWSTR::from(js.as_str());
webview
.AddScriptToExecuteOnDocumentCreated(*js.as_ref().as_pcwstr(), &handler)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(|error_code, _id| error_code),
)?;
Ok(self)
}
pub fn eval(&self, js: &str) -> Result<&Self> {
let webview = self.webview.clone();
let js = String::from(js);
ExecuteScriptCompletedHandler::wait_for_async_operation(
Box::new(move |handler| unsafe {
let js = CoTaskMemPWSTR::from(js.as_str());
webview
.ExecuteScript(*js.as_ref().as_pcwstr(), &handler)
.map_err(webview2_com::Error::WindowsError)
}),
Box::new(|error_code, _result| error_code),
)?;
Ok(self)
}
pub fn dispatch<F>(&self, f: F) -> Result<&Self>
where
F: FnOnce(WebView) + Send + 'static,
{
self.tx.send(Box::new(f)).expect("send the fn");
unsafe {
let _ = WindowsAndMessaging::PostThreadMessageW(
self.thread_id,
WindowsAndMessaging::WM_APP,
WPARAM::default(),
LPARAM::default(),
);
}
Ok(self)
}
pub fn bind<F>(&self, name: &str, f: F) -> Result<&Self>
where
F: FnMut(Vec<Value>) -> Result<Value> + 'static,
{
self.bindings
.borrow_mut()
.insert(String::from(name), Box::new(f));
let js = String::from(
r#"
(function() {
var name = '"#,
) + name
+ r#"';
var RPC = window._rpc = (window._rpc || {nextSeq: 1});
window[name] = function() {
var seq = RPC.nextSeq++;
var promise = new Promise(function(resolve, reject) {
RPC[seq] = {
resolve: resolve,
reject: reject,
};
});
window.external.invoke({
id: seq,
method: name,
params: Array.prototype.slice.call(arguments),
});
return promise;
}
})()"#;
self.init(&js)
}
pub fn resolve(&self, id: u64, status: i32, result: Value) -> Result<&Self> {
let result = result.to_string();
self.dispatch(move |webview| {
let method = match status {
0 => "resolve",
_ => "reject",
};
let js = format!(
r#"
window._rpc[{id}].{method}({result});
window._rpc[{id}] = undefined;"#
);
webview.eval(&js).expect("eval return script");
})
}
fn set_window_webview(hwnd: HWND, webview: Option<Box<WebView>>) -> Option<Box<WebView>> {
unsafe {
match SetWindowLong(
hwnd,
WindowsAndMessaging::GWLP_USERDATA,
match webview {
Some(webview) => Box::into_raw(webview) as _,
None => 0_isize,
},
) {
0 => None,
ptr => Some(Box::from_raw(ptr as *mut _)),
}
}
}
fn get_window_webview(hwnd: HWND) -> Option<Box<WebView>> {
unsafe {
let data = GetWindowLong(hwnd, WindowsAndMessaging::GWLP_USERDATA);
match data {
0 => None,
_ => {
let webview_ptr = data as *mut WebView;
let raw = Box::from_raw(webview_ptr);
let webview = raw.clone();
mem::forget(raw);
Some(webview)
}
}
}
}
}
fn set_process_dpi_awareness() -> Result<()> {
unsafe { HiDpi::SetProcessDpiAwareness(HiDpi::PROCESS_PER_MONITOR_DPI_AWARE)? };
Ok(())
}
extern "system" fn window_proc(hwnd: HWND, msg: u32, w_param: WPARAM, l_param: LPARAM) -> LRESULT {
let webview = match WebView::get_window_webview(hwnd) {
Some(webview) => webview,
None => return unsafe { WindowsAndMessaging::DefWindowProcW(hwnd, msg, w_param, l_param) },
};
let frame = webview
.frame
.as_ref()
.expect("should only be called for owned windows");
match msg {
WindowsAndMessaging::WM_SIZE => {
let size = get_window_size(hwnd);
unsafe {
webview
.controller
.0
.SetBounds(RECT {
left: 0,
top: 0,
right: size.cx,
bottom: size.cy,
})
.unwrap();
}
*frame.size.borrow_mut() = size;
LRESULT::default()
}
WindowsAndMessaging::WM_CLOSE => {
unsafe {
let _ = WindowsAndMessaging::DestroyWindow(hwnd);
}
LRESULT::default()
}
WindowsAndMessaging::WM_DESTROY => {
webview.terminate().expect("window is gone");
LRESULT::default()
}
_ => unsafe { WindowsAndMessaging::DefWindowProcW(hwnd, msg, w_param, l_param) },
}
}
fn get_window_size(hwnd: HWND) -> SIZE {
let mut client_rect = RECT::default();
let _ = unsafe { WindowsAndMessaging::GetClientRect(hwnd, &mut client_rect) };
SIZE {
cx: client_rect.right - client_rect.left,
cy: client_rect.bottom - client_rect.top,
}
}
#[allow(non_snake_case)]
#[cfg(target_pointer_width = "32")]
unsafe fn SetWindowLong(window: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize {
WindowsAndMessaging::SetWindowLongW(window, index, value as _) as _
}
#[allow(non_snake_case)]
#[cfg(target_pointer_width = "64")]
unsafe fn SetWindowLong(window: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize {
WindowsAndMessaging::SetWindowLongPtrW(window, index, value)
}
#[allow(non_snake_case)]
#[cfg(target_pointer_width = "32")]
unsafe fn GetWindowLong(window: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize {
WindowsAndMessaging::GetWindowLongW(window, index) as _
}
#[allow(non_snake_case)]
#[cfg(target_pointer_width = "64")]
unsafe fn GetWindowLong(window: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize {
WindowsAndMessaging::GetWindowLongPtrW(window, index)
}