├── .gitignore ├── Cargo.toml ├── README.md ├── examples └── winit.rs └── src ├── cocoa.rs ├── edge.rs ├── edge_winit.rs ├── error.rs ├── gtk.rs ├── lib.rs └── windows.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webviewcontrol" 3 | version = "0.0.1" 4 | authors = ["Chris Morgan "] 5 | edition = "2018" 6 | 7 | [features] 8 | default = [ 9 | "edgehtml", 10 | "winit", 11 | ] 12 | 13 | # (These features should/will only take effect on Windows.) 14 | edgehtml = ["winrt", "winapi/roapi", "winapi/winerror"] 15 | mshtml = [] # TODO 16 | 17 | # gtk-webkit2 is primarily intended for Linux use 18 | #gtk-webkit2 = ["gtk-webkit2-sys"] 19 | 20 | # Cocoa is macOS-only. 21 | #cocoa = [] 22 | 23 | [dependencies] 24 | once_cell = "1.2.0" 25 | 26 | [dependencies.winit] 27 | version = "0.20.0-alpha3" 28 | optional = true 29 | 30 | [target."cfg(target_os = \"windows\")".dependencies.winapi] 31 | version = "0.3.6" 32 | #features = ["roapi", "winerror", "synchapi", "combaseapi", "winbase"] 33 | optional = true 34 | 35 | [target."cfg(target_os = \"windows\")".dependencies.winrt] 36 | version = "0.6.0" 37 | features = ["windows-web"] 38 | optional = true 39 | 40 | # [features] 41 | # cocoa-wkwebview = […] 42 | # # It’s called edgehtml-win32 because it’s using the EdgeHTML engine via the Win32 out-of-process wrapper (Windows.Web.UI.Interop.WebViewControl) rather than via the older and preferred UWP interface which is potentially more efficient and may resolve problems with control focus and menus, but bothersome due to sandboxing and setting up the appx package, and possibly requiring C++/WinRT code. Still, edgehtml-uwp may come in the future. 43 | # edgehtml-win32 = […] 44 | # gtk-webkit2 = […] 45 | # mshtml = […] 46 | # 47 | # [target."cfg(target_os = \"macos\")".features] 48 | # default = ["cocoa-wkwebview"] 49 | # 50 | # [target."cfg(target_os = \"windows\")".features] 51 | # default = ["edgehtml-win32", "mshtml"] 52 | # 53 | # [target."cfg(not(any(target_os = \"macos\", target_os = \"windows\")))".features] 54 | # default = ["gtk-webkit2"] 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webviewcontrol-rs 2 | 3 | ## Goals 4 | 5 | A cross-platform web view control, meeting these requirements: 6 | 7 | - pure Rust (not needing to build any C++ code or bind to non-platform libraries); 8 | - can use EdgeHTML on suitable versions of Windows, without making the whole app run in the UWP sandbox; 9 | - properly a control, rather than *needing* to be the entire window (though that’s the most common case—so provide a handy function that uses winit for that; on that topic, dialogs don’t belong in this crate, but rather in another crate). 10 | 11 | ## Similar projects 12 | 13 | - [Boscop/web-view](https://github.com/Boscop/web-view) binds [zserge/webview](https://github.com/zserge/webview). Depends on C or C++ code, doesn’t currently support EdgeHTML (though it’s in progress), doesn’t currently support high or mixed DPI environments (though there’s a patch that starts that), requires that it be the entire window. 14 | 15 | - [quadrupleslap/tether](https://github.com/quadrupleslap/tether) uses EdgeHTML only on Windows, requires running in the UWP sandbox and adding an appx manifest and C++/CX stuff. 16 | 17 | ## Status 18 | 19 | Windows.Web.Ui.Interop.WebViewControl (EdgeHTML) basic usage is working. Nothing more advanced than navigating to a URL is really supported at present. 20 | 21 | No other browser engines are currently supported. 22 | 23 | winit usage is working fine. 24 | 25 | Druid used to be partially supported, but there were serious issues that are unlikely to be resolved any time soon due to Druid being yet immature and currently opinionated about owning the entire surface, so I deleted the code after 5c746ca06af323303322c237da6603ad89b404d1. Eventually Druid might be compatible with multiple HWNDs and this type of drawing, but don’t hold your breath for it being in the near future. I hope that it works eventually, because I see potential in a hybrid app that’s partially web tech and partially native tech. 26 | -------------------------------------------------------------------------------- /examples/winit.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use winit::event::{Event, WindowEvent}; 4 | use winit::event_loop::{ControlFlow, EventLoop}; 5 | use winit::platform::desktop::EventLoopExtDesktop; 6 | use winit::window::WindowBuilder; 7 | 8 | use webviewcontrol::edge::{self, init_single_threaded_apartment, Control, Process, WebView}; 9 | use webviewcontrol::edge_winit::{new_control, HwndType}; 10 | 11 | fn main() { 12 | init_single_threaded_apartment(); 13 | 14 | if !edge::is_available() { 15 | panic!("EdgeHTML control is not available!"); 16 | } 17 | let mut args = std::env::args(); 18 | // Ignore program name argument. 19 | args.next(); 20 | let url: Cow = args 21 | .next() 22 | .map(|url| url.into()) 23 | .unwrap_or("http://www.example.com".into()); 24 | println!("Opening a web view to {}", url); 25 | 26 | let mut event_loop = EventLoop::new(); 27 | 28 | let window = WindowBuilder::new() 29 | .with_title("It’s a WebView!") 30 | .build(&event_loop) 31 | .unwrap(); 32 | 33 | let process = Process::new(); 34 | let control = new_control( 35 | &process, 36 | &window, 37 | HwndType::FillWindow, 38 | None, 39 | None, 40 | Some(move |control: Control| { 41 | println!("Control created!"); 42 | control.navigate(&url).unwrap(); 43 | }), 44 | ) 45 | .unwrap(); 46 | control.focus(); 47 | 48 | event_loop.run_return(|event, _, control_flow| { 49 | *control_flow = ControlFlow::Wait; 50 | match event { 51 | Event::WindowEvent { window_id, event } => match event { 52 | WindowEvent::Focused(false) => { 53 | println!("Window lost focus, TODO record whether control was focused"); 54 | } 55 | WindowEvent::Focused(true) => { 56 | println!("Window gained focus, TODO only refocus control if it was before"); 57 | control.focus(); 58 | } 59 | WindowEvent::CloseRequested => { 60 | *control_flow = ControlFlow::Exit; 61 | } 62 | WindowEvent::Resized(size) => { 63 | let size: (u32, u32) = size.to_physical(window.hidpi_factor()).into(); 64 | // Error in resizing? Meh. 65 | let _ = control.resize(None, Some((size.0 as i32, size.1 as i32))); 66 | } 67 | _ => (), 68 | }, 69 | _ => (), 70 | } 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/cocoa.rs: -------------------------------------------------------------------------------- 1 | struct WebViewControl { 2 | pool: id, 3 | window: id, 4 | webview: id, 5 | windowDelegate: id, 6 | should_exit: int, 7 | } 8 | -------------------------------------------------------------------------------- /src/edge.rs: -------------------------------------------------------------------------------- 1 | //! A control backed by a Win32 EdgeHTML WebView. 2 | //! 3 | //! This uses Windows.Web.UI.Interop.WebViewControl, and requires at a minimum the October 2018 4 | //! build of Windows 10, 17763. (TODO: support detecting whether it’ll work ahead of time.) 5 | //! 6 | //! Most of the Windows.Web.UI.Interop namespace was added in build 17083, but that may not be 7 | //! sufficient for this library, which may depend on AddInitializeScript and perhaps GotFocus and 8 | //! LostFocus, all of which were only introduced in build 17763. 9 | //! 10 | //! See https://docs.microsoft.com/en-us/microsoft-edge/dev-guide#webview for some details of 11 | //! limitations (e.g. Push API isn’t supported). Further known limitations: 12 | //! 13 | //! 1. If you want a native menu, you need to associate an hMenu with the HWND this control uses, 14 | //! and then it works fine. 15 | //! 2. The WebViewControl will be focused automatically if something on the page displayed takes 16 | //! focus (e.g. https://www.duckduckgo.com), but not otherwise (e.g. http://www.example.com). 17 | //! 3. Keyboard-based focus switching between controls outside the WebViewControl and inside does 18 | //! not work out of the box, and I haven’t done anything about that yet. 19 | //! 4. If the WebViewControl is focused, then Alt+F4 won’t work. 20 | //! 5. If the WebViewControl is focused and has no associated hMenu, Alt+Space won’t work. 21 | //! (I dunno, do we need to add the system menu to it?) 22 | //! 6. For that matter, control sizing is untested in the presence of a menu. 23 | //! 7. It may crash if you look at it funny (e.g. try to navigate to a non-URL). 24 | //! 25 | //! I believe the focus issues are mostly because we’re interacting with it through this Win32 26 | //! interop wrapper rather than the UWP way; the control is actually being run in a separate 27 | //! process. If you’re willing to have your entire program run in the UWP sandbox, you can do 28 | //! things that way, but this crate doesn’t support that at this time, because the author of this 29 | //! crate doesn’t want to live in the UWP sandbox, wants regular Win32 stuff, dislikes the pain of 30 | //! actually buiblding an APPX package if you’re not using Microsoft’s tooling from top to bottom 31 | //! (e.g. C♯ and XAML), and suspects that using EdgeHTML through UWP will require the use of 32 | //! C++/WinRT, with the winrt crate not yet being capable enough. (This last especially may be 33 | //! incorrect.) But if you really just want a Rust UWP EdgeHTML-powered window with no other 34 | //! controls, https://github.com/quadrupleslap/tether is probably a good place to look. 35 | 36 | use std::cell::RefCell; 37 | use std::io; 38 | use std::mem; 39 | use std::ptr; 40 | use std::rc::Rc; 41 | 42 | use winapi::shared::minwindef::UINT; 43 | use winapi::shared::windef::{HWND, RECT}; 44 | use winapi::um::winnt::LPCWSTR; 45 | use winapi::um::winuser; 46 | 47 | use winrt::windows::foundation::{ 48 | metadata::ApiInformation, AsyncOperationCompletedHandler, EventRegistrationToken, Rect, 49 | TypedEventHandler, Uri, 50 | }; 51 | use winrt::windows::web::ui::{ 52 | interop::{ 53 | IWebViewControlSite, WebViewControl, WebViewControlMoveFocusReason, WebViewControlProcess, 54 | }, 55 | IWebViewControl, 56 | //IWebViewControl2, 57 | WebViewControlScriptNotifyEventArgs, 58 | }; 59 | use winrt::{ApartmentType, ComPtr, FastHString, RtDefaultConstructible}; 60 | 61 | use crate::error::Error; 62 | 63 | /// Dangerously pretend that the wrapped value is Send. 64 | /// 65 | /// There are various things where the winrt crate currently unnecessarily requires Send. I’m not 66 | /// sure if they’re all strictly unnecessary or whether there may be some cases where the Sendness 67 | /// is actually of value, but with Windows.Web.UI.Interop.* at least we’re stuck with operating in 68 | /// a single-threaded apartment, and so we can reasonably drop the Send requirement: nothing should 69 | /// ever be escaping to other threads. See https://github.com/contextfree/winrt-rust/issues/63 for 70 | /// discussion. This is naturally quite a dangerous thing to do, but I am courageous. 71 | struct FakeSend(T); 72 | unsafe impl Send for FakeSend {} 73 | 74 | use crate::windows::OUR_HINSTANCE; 75 | 76 | // L"WebViewControl Host" 77 | static HOST_CLASS_NAME: [u16; 20] = [ 78 | b'W' as u16, 79 | b'e' as u16, 80 | b'b' as u16, 81 | b'V' as u16, 82 | b'i' as u16, 83 | b'e' as u16, 84 | b'w' as u16, 85 | b'C' as u16, 86 | b'o' as u16, 87 | b'n' as u16, 88 | b't' as u16, 89 | b'r' as u16, 90 | b'o' as u16, 91 | b'l' as u16, 92 | b' ' as u16, 93 | b'H' as u16, 94 | b'o' as u16, 95 | b's' as u16, 96 | b't' as u16, 97 | 0, 98 | ]; 99 | 100 | pub fn is_available() -> bool { 101 | ApiInformation::is_type_present(&FastHString::from("Windows.Web.UI.Interop.WebViewControl")) 102 | .unwrap_or(false) 103 | // When we start using AddInitializeScript, which has a higher baseline, switch to this: 104 | // ApiInformation::is_method_present( 105 | // &FastHString::from("Windows.Web.UI.Interop.WebViewControl"), 106 | // &FastHString::from("AddInitializeScript"), 107 | // ).unwrap_or(false) 108 | } 109 | 110 | unsafe fn register_host_class() { 111 | winuser::RegisterClassExW(&winuser::WNDCLASSEXW { 112 | cbSize: mem::size_of::() as UINT, 113 | style: winuser::CS_HREDRAW | winuser::CS_VREDRAW | winuser::CS_OWNDC, 114 | lpfnWndProc: Some(winuser::DefWindowProcW), 115 | cbClsExtra: 0, 116 | cbWndExtra: 0, 117 | hInstance: OUR_HINSTANCE.0, 118 | hIcon: ptr::null_mut(), 119 | hCursor: ptr::null_mut(), 120 | hbrBackground: ptr::null_mut(), 121 | lpszMenuName: ptr::null(), 122 | lpszClassName: HOST_CLASS_NAME.as_ptr(), 123 | hIconSm: ptr::null_mut(), 124 | }); 125 | } 126 | 127 | /// Create a window/control for a web view. 128 | /// 129 | /// The provided parent SHOULD not be null. Things may break if it is. 130 | /// 131 | /// The provided position and size are specified in physical pixels. 132 | fn new_hwnd(parent: HWND, position: (i32, i32), size: (i32, i32)) -> Result { 133 | // Idempotent, as subsequent attempts will silently fail; meh. 134 | unsafe { 135 | register_host_class(); 136 | } 137 | 138 | let handle = unsafe { 139 | winuser::CreateWindowExW( 140 | 0, 141 | HOST_CLASS_NAME.as_ptr(), 142 | [0].as_ptr() as LPCWSTR, 143 | winuser::WS_CHILD | winuser::WS_VISIBLE, 144 | position.0, 145 | position.1, 146 | size.0, 147 | size.1, 148 | parent, 149 | // TODO: fill out hMenu 150 | ptr::null_mut(), 151 | OUR_HINSTANCE.0, 152 | ptr::null_mut(), 153 | ) 154 | }; 155 | 156 | if handle.is_null() { 157 | return Err(Error::Io(io::Error::last_os_error())); 158 | } 159 | 160 | Ok(handle) 161 | } 162 | 163 | /// Initialize a single-threaded winrt context. This must be called before instantiating a control, 164 | /// or it’ll default to the multi-threaded apartment type, which doesn’t work for EdgeHTML. 165 | pub fn init_single_threaded_apartment() { 166 | winrt::init_apartment(ApartmentType::STA); 167 | } 168 | 169 | /// What HWND to associate the WebViewControl with, and how to handle resizing. 170 | pub enum HwndType { 171 | /// Use the top-level window’s HWND. This causes resizing to only affect the WebViewControl, 172 | /// not the HWND, which the user has… presumably already handled? But what about programmatic 173 | /// resizing, do we want to support that, hmm? Maybe this type isn’t useful after all? TODO. 174 | FillWindow(HWND), 175 | /// Use the HWND passed, taking ownership of it (so that on control destruction 176 | /// DestroyWindow will be called). 177 | ConsumeHwnd(HWND), 178 | /// Create a new HWND with the window as its parent. 179 | NewHwndInWindow(HWND), 180 | } 181 | 182 | #[derive(Clone)] 183 | pub struct Process { 184 | process: ComPtr, 185 | } 186 | 187 | impl Process { 188 | pub fn new() -> Process { 189 | let process = WebViewControlProcess::new(); 190 | process 191 | .add_process_exited(&TypedEventHandler::new(move |_proc, _result| { 192 | eprintln!("WebViewControlProcess exited, should we do anything about it?"); 193 | Ok(()) 194 | })) 195 | .unwrap(); 196 | 197 | Process { process } 198 | } 199 | 200 | pub fn create_control( 201 | &self, 202 | hwnd_type: HwndType, 203 | position: (i32, i32), 204 | size: (i32, i32), 205 | callback: Option, 206 | ) -> Result { 207 | let hwnd = match hwnd_type { 208 | HwndType::FillWindow(hwnd) => hwnd, 209 | HwndType::ConsumeHwnd(hwnd) => hwnd, 210 | HwndType::NewHwndInWindow(parent) => new_hwnd(parent, position, size)?, 211 | }; 212 | 213 | let operation = self.process.create_web_view_control_async( 214 | hwnd as usize as i64, 215 | Rect { 216 | X: position.0 as f32, 217 | Y: position.1 as f32, 218 | Width: size.0 as f32, 219 | Height: size.1 as f32, 220 | }, 221 | )?; 222 | 223 | let control = Control { 224 | inner: Rc::new(RefCell::new(ControlInner { 225 | hwnd, 226 | is_window_hwnd: match hwnd_type { 227 | HwndType::FillWindow(_) => true, 228 | _ => false, 229 | }, 230 | control: None, 231 | queued_bounds_update: None, 232 | queued_focus: false, 233 | })), 234 | }; 235 | 236 | // I believe AsyncOperationCompletedHandler should simply not require Send, but it does for 237 | // now. So, time to pretend Send with this menace. 238 | let mut control2 = FakeSend(control.clone()); 239 | let mut callback = FakeSend(callback); 240 | operation 241 | .set_completed(&AsyncOperationCompletedHandler::new( 242 | move |sender, _args| { 243 | // When it doesn’t require Send, the following four lines should reduce to this: 244 | // control = operation.get_results().unwrap(); 245 | let web_view_control = unsafe { &mut *sender }.get_results().unwrap(); 246 | control2.0.control_created(web_view_control); 247 | if let Some(callback) = callback.0.take() { 248 | // XXX: unnecessary clone here, because this closure is FnMut rather than 249 | // FnOnce as it could in theory safely be. 250 | callback(control2.0.clone()); 251 | } 252 | Ok(()) 253 | }, 254 | )) 255 | .unwrap(); 256 | 257 | Ok(control) 258 | } 259 | } 260 | 261 | // A better solution would probably involve futures and Pin. 262 | // Then we could hopefully do away with the Rc> wrapping. 263 | #[derive(Clone)] 264 | pub struct Control { 265 | inner: Rc>, 266 | } 267 | 268 | pub struct ControlInner { 269 | hwnd: HWND, 270 | is_window_hwnd: bool, 271 | 272 | // Option because it’s async. 273 | control: Option>, 274 | 275 | // Certain operations may be queued while the control is loading. For example, handling resize. 276 | queued_bounds_update: Option, 277 | queued_focus: bool, 278 | } 279 | 280 | impl ControlInner { 281 | /// Updates the WebViewControl’s bounds based on the HWND’s current values. 282 | /// Returns an error if it fails to get the window rect, which I think shouldn’t ever happen. 283 | /// Currently returns success if the control is simply not ready yet. 284 | /// TODO: revisit that decision, taking into account also what happens if the window is resized 285 | /// while the control is girding its loins. 286 | fn update_bounds(&mut self) -> Result<(), Error> { 287 | let mut rect = RECT { 288 | top: 0, 289 | left: 0, 290 | bottom: 0, 291 | right: 0, 292 | }; 293 | if unsafe { winuser::GetWindowRect(self.hwnd, &mut rect) } == 0 { 294 | return Err(Error::Io(io::Error::last_os_error())); 295 | } 296 | self.update_bounds_from_rect(Rect { 297 | X: if self.is_window_hwnd { 298 | 0.0 299 | } else { 300 | rect.left as f32 301 | }, 302 | Y: if self.is_window_hwnd { 303 | 0.0 304 | } else { 305 | rect.top as f32 306 | }, 307 | Width: (rect.right - rect.left) as f32, 308 | Height: (rect.bottom - rect.top) as f32, 309 | }) 310 | } 311 | 312 | fn update_bounds_from_rect(&mut self, rect: Rect) -> Result<(), Error> { 313 | println!("Updating bounds to {:?}", rect); 314 | if let Some(ref control) = self.control { 315 | let control_site = control.query_interface::().unwrap(); 316 | control_site.set_bounds(rect)?; 317 | } else { 318 | self.queued_bounds_update = Some(rect); 319 | } 320 | Ok(()) 321 | } 322 | 323 | fn focus(&mut self) -> Result<(), Error> { 324 | if let Some(ref control) = self.control { 325 | let control_site = control.query_interface::().unwrap(); 326 | control_site.move_focus(WebViewControlMoveFocusReason::Programmatic)?; 327 | } else { 328 | self.queued_focus = true; 329 | } 330 | Ok(()) 331 | } 332 | } 333 | 334 | impl Control { 335 | // For internal use, part of the CreateWebViewControlAsync completed handler. 336 | fn control_created(&mut self, web_view_control: Option>) { 337 | let mut inner = self.inner.borrow_mut(); 338 | inner.control = web_view_control; 339 | if let Some(rect) = inner.queued_bounds_update { 340 | inner.queued_bounds_update = None; 341 | // There’s nothing we can do if this fails; maybe better to be silent like this? 342 | let _ = inner.update_bounds_from_rect(rect); 343 | } 344 | if inner.queued_focus { 345 | let _ = inner.focus(); 346 | } 347 | } 348 | 349 | pub fn focus(&self) -> Result<(), Error> { 350 | self.inner.borrow_mut().focus() 351 | } 352 | 353 | pub fn resize( 354 | &self, 355 | position: Option<(i32, i32)>, 356 | size: Option<(i32, i32)>, 357 | ) -> Result<(), Error> { 358 | let mut inner = self.inner.borrow_mut(); 359 | if !inner.is_window_hwnd { 360 | let (x, y) = position.unwrap_or((0, 0)); 361 | let (width, height) = size.unwrap_or((0, 0)); 362 | let mut flags = winuser::SWP_NOZORDER; 363 | if position.is_none() { 364 | flags |= winuser::SWP_NOMOVE; 365 | } 366 | if size.is_none() { 367 | flags |= winuser::SWP_NOSIZE; 368 | } 369 | unsafe { 370 | winuser::SetWindowPos(inner.hwnd, ptr::null_mut(), x, y, width, height, flags); 371 | winuser::UpdateWindow(inner.hwnd); 372 | } 373 | } 374 | if let Some((width, height)) = size { 375 | // Bounds X and Y seem to be relative to the HWND, hence zeroing them. 376 | inner.update_bounds_from_rect(Rect { 377 | X: 0.0, 378 | Y: 0.0, 379 | Width: width as f32, 380 | Height: height as f32, 381 | })?; 382 | } else { 383 | inner.update_bounds()?; 384 | } 385 | Ok(()) 386 | } 387 | 388 | /// Get the underlying HWND associated with this WebViewControl. 389 | /// 390 | /// Not sure why you’d want this, but I know we need it for internal stuff. 391 | pub fn get_hwnd(&self) -> HWND { 392 | self.inner.borrow().hwnd 393 | } 394 | 395 | /// Get the underlying Windows.Web.UI.Interop.WebViewControl instance. 396 | /// 397 | /// This allows you to do more advanced, engine-specific magicks. 398 | /// 399 | /// Returns None if the control hasn’t been created yet (it takes a second to get started). 400 | pub fn get_inner(&self) -> Option> { 401 | self.inner.borrow().control.clone() 402 | } 403 | } 404 | 405 | pub trait WebView { 406 | type Error; 407 | fn navigate(&self, url: &str) -> Result<(), Self::Error>; 408 | } 409 | 410 | impl WebView for Control { 411 | type Error = winrt::Error; 412 | fn navigate(&self, url: &str) -> Result<(), winrt::Error> { 413 | if let Some(ref control) = self.inner.borrow().control { 414 | control.navigate(&*Uri::create_uri(&FastHString::from(&*url))?)?; 415 | } 416 | Ok(()) 417 | } 418 | } 419 | 420 | pub struct EdgeWebViewControl { 421 | control: ComPtr, 422 | } 423 | 424 | // The methods commented out need a new release of the winrt crate, and then typically some fixup 425 | // because I haven’t sorted their signatures out. 426 | impl EdgeWebViewControl { 427 | // --- Properties --- 428 | 429 | /// Returns true if the control is functioning and go_back() can work. 430 | pub fn can_go_back(&self) -> bool { 431 | self.control.get_can_go_back().unwrap_or(false) 432 | } 433 | 434 | /// Returns true if the control is functioning and go_forward() can work. 435 | pub fn can_go_forward(&self) -> bool { 436 | self.control.get_can_go_forward().unwrap_or(false) 437 | } 438 | 439 | /// Returns true if the control is functioning and contains an element that wants to be 440 | /// fullscreen. 441 | pub fn contains_full_screen_element(&self) -> bool { 442 | self.control 443 | .get_contains_full_screen_element() 444 | .unwrap_or(false) 445 | } 446 | 447 | // pub fn default_background_color(&self) { 448 | // self.control.get_default_background_color() 449 | // } 450 | 451 | // pub fn set_default_background_color(&self) { 452 | // self.control.set_default_background_color() 453 | // } 454 | 455 | // pub fn deferred_permission_requests(&self) { 456 | // self.control.get_deferred_permission_requests() 457 | // } 458 | 459 | /// Retrieves the document title. 460 | /// 461 | /// Returns an empty string if the control is not functioning. 462 | pub fn document_title(&self) -> String { 463 | self.control 464 | .get_document_title() 465 | .map(|s| s.to_string()) 466 | .unwrap_or(String::new()) 467 | } 468 | 469 | // /// Sets the zoom factor for the contents of the control. 470 | // /// Returns 1.0 if the control is not functioning. 471 | // pub fn scale(&self) -> f64 { 472 | // self.control.get_scale() 473 | // } 474 | 475 | // Skipped properties: 476 | // 477 | // • Bounds, because we manage that otherwise. 478 | // • IsVisible, purely because I can’t think why that exists yet. 479 | // • Process, because we don’t *want* to expose that cycle. 480 | // • Settings, because we’ll expose these otherwise, if ever. 481 | 482 | // --- Methods --- 483 | 484 | // pub fn add_initialize_script(&self, script: &str) { 485 | // self.control.add_initialize_script(script.into()) 486 | // } 487 | 488 | // pub fn build_local_stream_uri(&self) {} 489 | 490 | // /// The building block for taking a screenshot of the control. 491 | // pub fn capture_preview_to_stream_async(&self) {} 492 | 493 | pub fn capture_selected_content_to_data_package_async(&self) {} 494 | pub fn close(&self) {} 495 | pub fn get_deferred_permission_request_by_id(&self) {} 496 | pub fn go_back(&self) {} 497 | pub fn go_forward(&self) {} 498 | pub fn invoke_script_async(&self) {} 499 | pub fn move_focus(&self) {} 500 | pub fn navigate(&self) {} 501 | pub fn navigate_to_local_stream_uri(&self) {} 502 | pub fn navigate_to_string(&self) {} 503 | pub fn navigate_with_http_request_message(&self) {} 504 | pub fn refresh(&self) {} 505 | pub fn stop(&self) {} 506 | 507 | // --- Events --- 508 | 509 | // Skipped: various events to do with loading. If you really need them, take the control ComPtr 510 | // and do it yourself. 511 | 512 | /* 513 | pub fn add_accelerator_key_pressed(&self, f: F) 514 | -> Result 515 | where F: FnMut(TODO) + 'static 516 | { 517 | let mut f = FakeSend(f); 518 | self.control.add_accelerator_key_pressed(&TypedEventHandler::new( 519 | move |_sender, args: *mut _| { 520 | let args = unsafe { &mut *args }; 521 | f.0(args); 522 | Ok(()) 523 | } 524 | )) 525 | } 526 | */ 527 | 528 | pub fn add_contains_full_screen_element_changed( 529 | &self, 530 | f: F, 531 | ) -> Result 532 | where 533 | F: FnMut(bool) + 'static, 534 | { 535 | let mut f = FakeSend(f); 536 | self.control 537 | .add_contains_full_screen_element_changed(&TypedEventHandler::new( 538 | move |sender: *mut IWebViewControl, _args| { 539 | let sender = unsafe { &mut *sender }; 540 | f.0(sender.get_contains_full_screen_element()?); 541 | Ok(()) 542 | }, 543 | )) 544 | } 545 | 546 | /* 547 | pub fn add_new_window_requested(&self, f: F) 548 | -> Result 549 | where F: FnMut(TODO) + 'static 550 | { 551 | let mut f = FakeSend(f); 552 | self.control.add_new_window_requested(&TypedEventHandler::new( 553 | move |_sender, args: *mut _| { 554 | let args = unsafe { &mut *args }; 555 | f.0(args); 556 | Ok(()) 557 | } 558 | )) 559 | } 560 | 561 | pub fn add_permission_requested(&self, f: F) 562 | -> Result 563 | where F: FnMut(TODO) + 'static 564 | { 565 | let mut f = FakeSend(f); 566 | self.control.add_permission_requested(&TypedEventHandler::new( 567 | move |_sender, args: *mut _| { 568 | let args = unsafe { &mut *args }; 569 | f.0(args); 570 | Ok(()) 571 | } 572 | )) 573 | } 574 | */ 575 | 576 | /// Define a function to handle script notifications triggered from JavaScript like this: 577 | /// 578 | /// ```javascript 579 | /// window.external.notify(string) 580 | /// ``` 581 | pub fn add_script_notify(&self, f: F) -> Result 582 | where 583 | F: FnMut(String) + 'static, 584 | { 585 | // I do not know whether TypedEventHandler is unconditionally handled in the same thread or 586 | // not; but for our case at least, we do not need its Sendness. Let’s live dangerously! 587 | let mut f = FakeSend(f); 588 | self.control.add_script_notify(&TypedEventHandler::new( 589 | move |_sender, args: *mut WebViewControlScriptNotifyEventArgs| { 590 | let args = unsafe { &mut *args }; 591 | // args also has get_uri(), but I figure we don’t need it… for now, at least. 592 | let value = args.get_value().map(|s| s.to_string())?; 593 | f.0(value); 594 | Ok(()) 595 | }, 596 | )) 597 | } 598 | 599 | /* 600 | pub fn add_unsafe_content_warning_displaying(&self, f: F) -> Result 601 | where F: FnMut(TODO) + 'static 602 | { 603 | let mut f = FakeSend(f); 604 | self.control.add_unsafe_content_warning_displaying(&TypedEventHandler::new( 605 | move |_sender, args: *mut _| { 606 | let args = unsafe { &mut *args }; 607 | f.0(args); 608 | Ok(()) 609 | } 610 | )) 611 | } 612 | 613 | pub fn add_unsupported_uri_scheme_identified(&self, f: F) -> Result 614 | where F: FnMut(TODO) + 'static 615 | { 616 | let mut f = FakeSend(f); 617 | self.control.add_unsupported_uri_scheme_identified(&TypedEventHandler::new( 618 | move |_sender, args: *mut _| { 619 | let args = unsafe { &mut *args }; 620 | f.0(args); 621 | Ok(()) 622 | } 623 | )) 624 | } 625 | 626 | pub fn add_unviewable_content_identified(&self, f: F) -> Result 627 | where F: FnMut(TODO) + 'static 628 | { 629 | let mut f = FakeSend(f); 630 | self.control.add_unviewable_content_identified(&TypedEventHandler::new( 631 | move |_sender, args: *mut _| { 632 | let args = unsafe { &mut *args }; 633 | f.0(args); 634 | Ok(()) 635 | } 636 | )) 637 | } 638 | 639 | pub fn add_web_resource_requested(&self, f: F) -> Result 640 | where F: FnMut(TODO) + 'static 641 | { 642 | let mut f = FakeSend(f); 643 | self.control.add_web_resource_requested(&TypedEventHandler::new( 644 | move |_sender, args: *mut _| { 645 | let args = unsafe { &mut *args }; 646 | f.0(args); 647 | Ok(()) 648 | } 649 | )) 650 | } 651 | */ 652 | } 653 | -------------------------------------------------------------------------------- /src/edge_winit.rs: -------------------------------------------------------------------------------- 1 | use crate::edge::{self, Control, Process}; 2 | use crate::error::Error; 3 | 4 | use winapi::shared::windef::HWND; 5 | 6 | use winit::dpi::{LogicalPosition, LogicalSize}; 7 | use winit::platform::windows::WindowExtWindows; 8 | use winit::window::Window; 9 | 10 | pub enum HwndType { 11 | FillWindow, 12 | ConsumeHwnd(HWND), 13 | NewHwndInWindow, 14 | } 15 | 16 | pub fn new_control( 17 | process: &Process, 18 | window: &Window, 19 | hwnd_type: HwndType, 20 | position: Option, 21 | size: Option, 22 | callback: Option, 23 | ) -> Result 24 | where 25 | F: FnOnce(Control) + 'static, 26 | { 27 | let window_hwnd = window.hwnd() as *mut _; 28 | let hwnd_type = match hwnd_type { 29 | HwndType::FillWindow => edge::HwndType::FillWindow(window_hwnd), 30 | HwndType::ConsumeHwnd(hwnd) => edge::HwndType::ConsumeHwnd(hwnd), 31 | HwndType::NewHwndInWindow => edge::HwndType::NewHwndInWindow(window_hwnd), 32 | }; 33 | // Fill in defaults for position and size, and convert them to physical units. 34 | let dpi_factor = window.hidpi_factor(); 35 | let position = position 36 | .unwrap_or(LogicalPosition { x: 0.0, y: 0.0 }) 37 | .to_physical(dpi_factor) 38 | .into(); 39 | let size: (u32, u32) = size 40 | .unwrap_or(window.inner_size()) 41 | .to_physical(dpi_factor) 42 | .into(); 43 | process.create_control( 44 | hwnd_type, 45 | // The true size will be sorted out by size(), which will queue the size change until 46 | // the control is created. 47 | position, 48 | (size.0 as i32, size.1 as i32), 49 | callback, 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::io; 3 | 4 | #[cfg(all(windows, feature = "edgehtml"))] 5 | use winrt; 6 | 7 | /// webviewcontrol’s uniform error type. 8 | /// 9 | /// The particular variants that are available vary by platform. Here are the variants you can 10 | /// expect: 11 | /// 12 | /// - EdgeHTML: the poorly named `Io` for OS errors (the HWND side of things), or `Rt` for WinRT 13 | /// errors (the WebViewControl side of things). As the WinRT errors don’t implement 14 | /// `std::error::Error`, the `source()` method will return `None` for these. 15 | #[derive(Debug)] 16 | pub enum Error { 17 | Io(io::Error), 18 | #[cfg(all(windows, feature = "edgehtml"))] 19 | Rt(winrt::Error), 20 | } 21 | 22 | impl fmt::Display for Error { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | match *self { 25 | Error::Io(ref err) => write!(f, "I/O error: {}", err), 26 | #[cfg(all(windows, feature = "edgehtml"))] 27 | Error::Rt(ref err) => write!(f, "WinRT error: {:?}", err), 28 | } 29 | } 30 | } 31 | 32 | impl std::error::Error for Error { 33 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 34 | match *self { 35 | Error::Io(ref err) => Some(err), 36 | #[cfg(all(windows, feature = "edgehtml"))] 37 | Error::Rt(_) => None, // Doesn’t implement std::error::Error 38 | } 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(error: io::Error) -> Error { 44 | Error::Io(error) 45 | } 46 | } 47 | 48 | #[cfg(all(windows, feature = "edgehtml"))] 49 | impl From for Error { 50 | fn from(error: winrt::Error) -> Error { 51 | Error::Rt(error) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/gtk.rs: -------------------------------------------------------------------------------- 1 | use gtk::Widget; 2 | use glib_sys::GAsyncQueue; 3 | 4 | struct WebViewControl { 5 | window: Widget, 6 | scroller: Widget, 7 | webview: Widget, 8 | inspector_window: Widget, 9 | queue: *mut GAsyncQueue, 10 | ready: int, 11 | js_busy: int, 12 | should_exit: int, 13 | } 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Experimentation ground. Fear not, more platforms will be added and a consistent API added 2 | //! before it’s done. 3 | 4 | pub mod error; 5 | pub use error::Error; 6 | 7 | #[cfg(all(windows, any(feature = "edgehtml", feature = "mshtml")))] 8 | mod windows; 9 | 10 | #[cfg(all(windows, feature = "edgehtml"))] 11 | pub mod edge; 12 | 13 | #[cfg(all(windows, feature = "edgehtml", feature = "winit"))] 14 | pub mod edge_winit; 15 | 16 | #[cfg(all(windows, feature = "mshtml"))] 17 | pub mod mshtml; 18 | 19 | pub enum Backend { 20 | #[cfg(all(windows, feature = "edgehtml"))] 21 | EdgeHTML, 22 | #[cfg(all(windows, feature = "mshtml"))] 23 | MSHTML, 24 | #[cfg(feature = "gtk-webkit2")] 25 | GtkWebkit2, 26 | #[cfg(feature = "cocoa")] 27 | Cocoa, 28 | } 29 | 30 | pub enum WebViewControl { 31 | #[cfg(all(windows, feature = "mshtml"))] 32 | MSHTML(mshtml::Control), 33 | #[cfg(all(windows, feature = "edgehtml"))] 34 | EdgeHTML(edge::Control), 35 | #[cfg(feature = "gtk-webkit2")] 36 | GtkWebkit2(gtk::Control), 37 | #[cfg(feature = "cocoa")] 38 | Cocoa(cocoa::Control), 39 | } 40 | -------------------------------------------------------------------------------- /src/windows.rs: -------------------------------------------------------------------------------- 1 | use std::ptr; 2 | 3 | use once_cell::sync::Lazy; 4 | 5 | use winapi::shared::minwindef::HINSTANCE; 6 | use winapi::um::libloaderapi; 7 | 8 | pub struct HInstanceWrapper(pub HINSTANCE); 9 | unsafe impl Send for HInstanceWrapper {} 10 | unsafe impl Sync for HInstanceWrapper {} 11 | 12 | pub static OUR_HINSTANCE: Lazy = 13 | Lazy::new(|| HInstanceWrapper(unsafe { libloaderapi::GetModuleHandleW(ptr::null()) })); 14 | --------------------------------------------------------------------------------