diff --git a/.changes/add-find-in-page.md b/.changes/add-find-in-page.md new file mode 100644 index 000000000..47d20365c --- /dev/null +++ b/.changes/add-find-in-page.md @@ -0,0 +1,5 @@ +--- +"wry": minor +--- + +Add `WebView::find_in_page`. diff --git a/examples/find_in_page.rs b/examples/find_in_page.rs new file mode 100644 index 000000000..bb77bc73c --- /dev/null +++ b/examples/find_in_page.rs @@ -0,0 +1,75 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use wry::webview::FindInPageOption; + +fn main() -> wry::Result<()> { + use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + webview::WebViewBuilder, + }; + + #[derive(Debug)] + enum UserEvent { + FindInPage(String), + } + + let event_loop: EventLoop = EventLoop::with_user_event(); + let proxy = event_loop.create_proxy(); + let window = WindowBuilder::new() + .with_title("Hello World") + .build(&event_loop)?; + let webview = WebViewBuilder::new(window)? + .with_html( + r#" + + +

Tauri is a toolkit that helps developers make applications for the major desktop platforms - using virtually any frontend framework in existence. The core is built with Rust, and the CLI leverages Node.js making Tauri a genuinely polyglot approach to creating and maintaining great apps. If you want to know more about the technical details, then please visit the Introduction. If you want to know more about this project's philosophy - then keep reading.

+ +"#, + )? + .with_ipc_handler(move |_, text: String| { + proxy + .send_event(UserEvent::FindInPage(text.clone())) + .unwrap(); + }); + + #[cfg(debug_assertions)] + let webview = webview.with_devtools(true); + + let webview = webview.build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::UserEvent(UserEvent::FindInPage(text)) => { + webview.find_in_page( + text, + FindInPageOption { + case_sensitive: true, + max_match_count: 100, + ..FindInPageOption::default() + }, + |found| println!("Is found: {}", found), + ); + } + _ => (), + } + }); +} diff --git a/src/webview/android/mod.rs b/src/webview/android/mod.rs index 10d131621..f6a87c979 100644 --- a/src/webview/android/mod.rs +++ b/src/webview/android/mod.rs @@ -1,6 +1,6 @@ use std::{collections::HashSet, ffi::c_void, ptr::null_mut, rc::Rc, sync::RwLock}; -use crate::{application::window::Window, Result}; +use crate::{application::window::Window, webview::FindInPageOption, Result}; use super::{WebContext, WebViewAttributes}; @@ -120,6 +120,12 @@ impl InnerWebView { } pub fn zoom(&self, scale_factor: f64) {} + + pub fn find_in_page(&self, _string: String, _option: FindInPageOption, _f: F) + where + F: Fn(bool) + 'static, + { + } } pub struct UnsafeIpc(*mut c_void); diff --git a/src/webview/mod.rs b/src/webview/mod.rs index ce24d3e2f..d1d2a7c7c 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -517,6 +517,20 @@ impl WebView { self.webview.zoom(scale_factor); } + /// Searches for the specified string in the WebView content. + /// + /// ## Platform-specific: + /// + /// - **Windows / Android**: Unsupported. + /// - **macOS**: available on macOS 10.15.4+ only. + /// - **iOS**: available on iOS 13.4+ only. + pub fn find_in_page(&self, string: String, option: FindInPageOption, f: F) + where + F: Fn(bool) + 'static, + { + self.webview.find_in_page(string, option, f); + } + #[cfg(target_os = "android")] pub fn run(self, env: JNIEnv, jclass: JClass, jobject: JObject) -> jobject { self.webview.run(env, jclass, jobject).unwrap() @@ -528,6 +542,23 @@ impl WebView { } } +/// A configuration for [`WebView::find_in_page`]. +#[derive(Default)] +pub struct FindInPageOption { + /// Indicate the search direction. + pub backwards: bool, + /// Match the search string in a case-sensitive. + pub case_sensitive: bool, + /// Wrap around to the other side of the page. + pub wraps: bool, + /// Maximum number of search strings to highlight. + /// + /// ## Platform-specific: + /// + /// **Windows / macOS / Android / iOS**: Unsupported. + pub max_match_count: u32, +} + /// An event enumeration sent to [`FileDropHandler`]. #[non_exhaustive] #[derive(Debug, Serialize, Clone)] diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs index d74087afa..b1d0e1965 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webview/webkitgtk/mod.rs @@ -18,7 +18,8 @@ use gio::Cancellable; use glib::signal::Inhibit; use gtk::prelude::*; use webkit2gtk::{ - traits::*, NavigationPolicyDecision, PolicyDecisionType, UserContentInjectedFrames, UserScript, + traits::*, FindController, FindControllerBuilder, FindControllerExt, FindOptions, + NavigationPolicyDecision, PolicyDecisionType, UserContentInjectedFrames, UserScript, UserScriptInjectionTime, WebView, WebViewBuilder, }; use webkit2gtk_sys::{ @@ -31,7 +32,7 @@ pub use web_context::WebContextImpl; use crate::{ application::{platform::unix::*, window::Window}, - webview::{web_context::WebContext, WebViewAttributes}, + webview::{web_context::WebContext, FindInPageOption, WebViewAttributes}, Error, Result, }; @@ -42,6 +43,7 @@ pub struct InnerWebView { pub(crate) webview: Rc, #[cfg(any(debug_assertions, feature = "devtools"))] is_inspector_open: Arc, + find_controller: Rc, } impl InnerWebView { @@ -294,10 +296,17 @@ impl InnerWebView { is_inspector_open }; + let find_controller = { + let builder = FindControllerBuilder::new(); + let controller = builder.web_view(&*webview).build(); + Rc::new(controller) + }; + let w = Self { webview, #[cfg(any(debug_assertions, feature = "devtools"))] is_inspector_open, + find_controller, }; // Initialize message handler @@ -390,6 +399,36 @@ impl InnerWebView { pub fn zoom(&self, scale_factor: f64) { WebViewExt::set_zoom_level(&*self.webview, scale_factor); } + + pub fn find_in_page(&self, string: String, option: FindInPageOption, f: F) + where + F: Fn(bool) + 'static, + { + let mut flags = FindOptions::NONE; + if option.backwards { + flags |= FindOptions::BACKWARDS; + } + if !option.case_sensitive { + flags |= FindOptions::CASE_INSENSITIVE; + } + if option.wraps { + flags |= FindOptions::WRAP_AROUND; + } + + self + .find_controller + .search(&string, flags.bits(), option.max_match_count); + + let handler = Rc::new(Box::new(f)); + let found = handler.clone(); + + self + .find_controller + .connect_failed_to_find_text(move |_| (*handler)(false)); + self + .find_controller + .connect_found_text(move |_, _| (*found)(true)); + } } pub fn platform_webview_version() -> Result { diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index 8418c5630..94a01a508 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -5,7 +5,7 @@ mod file_drop; use crate::{ - webview::{WebContext, WebViewAttributes}, + webview::{FindInPageOption, WebContext, WebViewAttributes}, Error, Result, }; @@ -621,6 +621,12 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ pub fn zoom(&self, scale_factor: f64) { let _ = unsafe { self.controller.SetZoomFactor(scale_factor) }; } + + pub fn find_in_page(&self, _string: String, _option: FindInPageOption, _f: F) + where + F: Fn(bool) + 'static, + { + } } pub fn platform_webview_version() -> Result { diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index c1e21ce31..b3e210450 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -6,6 +6,7 @@ mod file_drop; mod web_context; +use block::ConcreteBlock; pub use web_context::WebContextImpl; #[cfg(target_os = "macos")] @@ -43,7 +44,7 @@ use crate::{ dpi::{LogicalSize, PhysicalSize}, window::Window, }, - webview::{FileDropEvent, WebContext, WebViewAttributes}, + webview::{FileDropEvent, FindInPageOption, WebContext, WebViewAttributes}, Result, }; @@ -608,6 +609,22 @@ r#"Object.defineProperty(window, 'ipc', { let _: () = msg_send![self.webview, setPageZoom: scale_factor]; } } + + pub fn find_in_page(&self, string: String, option: FindInPageOption, f: F) + where + F: Fn(bool) + 'static, + { + unsafe { + let config: id = msg_send![class!(WKFindConfiguration), new]; + let _: () = msg_send![config, setBackwards: option.backwards]; + let _: () = msg_send![config, setCaseSensitive: option.case_sensitive]; + let _: () = msg_send![config, setWraps: option.wraps]; + let _: () = msg_send![self.webview, findString: NSString::new(&string) withConfiguration: config completionHandler:ConcreteBlock::new(|result: id| { + let match_found: BOOL = msg_send![result, matchFound]; + f(match_found == YES); + })]; + } + } } pub fn platform_webview_version() -> Result {