Skip to content

Commit

Permalink
feat: add document title changed handler, closes #804 (#825)
Browse files Browse the repository at this point in the history
* feat: add document title changed handler, closes #804

* linux

* android

* macOS

* fix selector and change read

* use `of_object` instead

* fix macOS

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
  • Loading branch information
amrbashir and lucasfernog committed Dec 30, 2022
1 parent fea5b96 commit 14a0ee3
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changes/document-title-changed-handler.md
@@ -0,0 +1,5 @@
---
"wry": "minor"
---

Add APIs to process webview document title change.
15 changes: 14 additions & 1 deletion src/webview/android/binding.rs
Expand Up @@ -13,7 +13,7 @@ use tao::platform::android::ndk_glue::jni::{
JNIEnv,
};

use super::{IPC, REQUEST_HANDLER};
use super::{IPC, REQUEST_HANDLER, TITLE_CHANGE_HANDLER};

fn handle_request(env: JNIEnv, request: JObject) -> Result<jobject, JniError> {
let mut request_builder = Request::builder();
Expand Down Expand Up @@ -167,3 +167,16 @@ pub unsafe fn ipc(env: JNIEnv, _: JClass, arg: JString) {
Err(e) => log::warn!("Failed to parse JString: {}", e),
}
}

#[allow(non_snake_case)]
pub unsafe fn handleReceivedTitle(env: JNIEnv, _: JClass, _webview: JObject, title: JString) {
match env.get_string(title) {
Ok(title) => {
let title = title.to_string_lossy().to_string();
if let Some(w) = TITLE_CHANGE_HANDLER.get() {
(w.0)(&w.1, title)
}
}
Err(e) => log::warn!("Failed to parse JString: {}", e),
}
}
9 changes: 9 additions & 0 deletions src/webview/android/kotlin/RustWebChromeClient.kt
Expand Up @@ -492,4 +492,13 @@ class RustWebChromeClient(appActivity: AppCompatActivity) : WebChromeClient() {
val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(imageFileName, ".jpg", storageDir)
}

override fun onReceivedTitle(
view: WebView,
title: String
) {
handleReceivedTitle(view, title)
}

private external fun handleReceivedTitle(webview: WebView, title: String);
}
22 changes: 22 additions & 0 deletions src/webview/android/mod.rs
Expand Up @@ -52,11 +52,19 @@ macro_rules! android_binding {
jobject
);
android_fn!($domain, $package, Ipc, ipc, [JString]);
android_fn!(
$domain,
$package,
RustWebChromeClient,
handleReceivedTitle,
[JObject, JString],
);
};
}

pub static IPC: OnceCell<UnsafeIpc> = OnceCell::new();
pub static REQUEST_HANDLER: OnceCell<UnsafeRequestHandler> = OnceCell::new();
pub static TITLE_CHANGE_HANDLER: OnceCell<UnsafeTitleHandler> = OnceCell::new();

pub struct UnsafeIpc(Box<dyn Fn(&Window, String)>, Rc<Window>);
impl UnsafeIpc {
Expand All @@ -78,6 +86,15 @@ impl UnsafeRequestHandler {
unsafe impl Send for UnsafeRequestHandler {}
unsafe impl Sync for UnsafeRequestHandler {}

pub struct UnsafeTitleHandler(Box<dyn Fn(&Window, String)>, Rc<Window>);
impl UnsafeTitleHandler {
pub fn new(f: Box<dyn Fn(&Window, String)>, w: Rc<Window>) -> Self {
Self(f, w)
}
}
unsafe impl Send for UnsafeTitleHandler {}
unsafe impl Sync for UnsafeTitleHandler {}

pub unsafe fn setup(env: JNIEnv, looper: &ForeignLooper, activity: GlobalRef) {
// we must create the WebChromeClient here because it calls `registerForActivityResult`,
// which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook
Expand Down Expand Up @@ -224,6 +241,11 @@ impl InnerWebView {
IPC.get_or_init(move || UnsafeIpc::new(Box::new(i), w));
}

let w = window.clone();
if let Some(i) = attributes.document_title_changed_handler {
TITLE_CHANGE_HANDLER.get_or_init(move || UnsafeTitleHandler::new(i, w));
}

Ok(Self { window })
}

Expand Down
13 changes: 13 additions & 0 deletions src/webview/mod.rs
Expand Up @@ -224,6 +224,9 @@ pub struct WebViewAttributes {
/// This configuration only impacts macOS.
/// [Documentation](https://developer.apple.com/documentation/webkit/wkwebview/1414995-allowsbackforwardnavigationgestu).
pub back_forward_navigation_gestures: bool,

/// Set a handler closure to process the change of the webview's document title.
pub document_title_changed_handler: Option<Box<dyn Fn(&Window, String)>>,
}

impl Default for WebViewAttributes {
Expand Down Expand Up @@ -251,6 +254,7 @@ impl Default for WebViewAttributes {
zoom_hotkeys_enabled: false,
accept_first_mouse: false,
back_forward_navigation_gestures: false,
document_title_changed_handler: None,
}
}
}
Expand Down Expand Up @@ -577,6 +581,15 @@ impl<'a> WebViewBuilder<'a> {
self
}

/// Set a handler closure to process the change of the webview's document title.
pub fn with_document_title_changed_handler(
mut self,
callback: impl Fn(&Window, String) + 'static,
) -> Self {
self.webview.document_title_changed_handler = Some(Box::new(callback));
self
}

/// Consume the builder and create the [`WebView`].
///
/// Platform-specific behavior:
Expand Down
11 changes: 11 additions & 0 deletions src/webview/webkitgtk/mod.rs
Expand Up @@ -106,6 +106,17 @@ impl InnerWebView {
close_window.gtk_window().close();
});

// document title changed handler
if let Some(document_title_changed_handler) = attributes.document_title_changed_handler {
let w = window_rc.clone();
webview.connect_title_notify(move |webview| {
document_title_changed_handler(
&w,
webview.title().map(|t| t.to_string()).unwrap_or_default(),
)
});
}

webview.add_events(
EventMask::POINTER_MOTION_MASK
| EventMask::BUTTON1_MOTION_MASK
Expand Down
21 changes: 21 additions & 0 deletions src/webview/webview2/mod.rs
Expand Up @@ -272,6 +272,27 @@ impl InnerWebView {
.map_err(webview2_com::Error::WindowsError)?;
}

// document title changed handler
if let Some(document_title_changed_handler) = attributes.document_title_changed_handler {
let window_c = window.clone();
unsafe {
webview
.add_DocumentTitleChanged(
&DocumentTitleChangedEventHandler::create(Box::new(move |webview, _| {
let mut title = PWSTR::null();
if let Some(webview) = webview {
webview.DocumentTitle(&mut title)?;
let title = take_pwstr(title);
document_title_changed_handler(&window_c, title);
}
Ok(())
})),
&mut token,
)
.map_err(webview2_com::Error::WindowsError)?;
}
}

// Initialize scripts
Self::add_script_to_execute_on_document_created(
&webview,
Expand Down
70 changes: 65 additions & 5 deletions src/webview/wkwebview/mod.rs
Expand Up @@ -76,6 +76,7 @@ pub(crate) struct InnerWebView {
// Note that if following functions signatures are changed in the future,
// all functions pointer declarations in objc callbacks below all need to get updated.
ipc_handler_ptr: *mut (Box<dyn Fn(&Window, String)>, Rc<Window>),
document_title_changed_handler: *mut (Box<dyn Fn(&Window, String)>, Rc<Window>),
navigation_decide_policy_ptr: *mut Box<dyn Fn(String, bool) -> bool>,
#[cfg(target_os = "macos")]
file_drop_ptr: *mut (Box<dyn Fn(&Window, FileDropEvent) -> bool>, Rc<Window>),
Expand Down Expand Up @@ -378,6 +379,60 @@ impl InnerWebView {
null_mut()
};

// Document title changed handler
let document_title_changed_handler = if let Some(document_title_changed_handler) =
attributes.document_title_changed_handler
{
let cls = ClassDecl::new("DocumentTitleChangedDelegate", class!(NSObject));
let cls = match cls {
Some(mut cls) => {
cls.add_ivar::<*mut c_void>("function");
cls.add_method(
sel!(observeValueForKeyPath:ofObject:change:context:),
observe_value_for_key_path as extern "C" fn(&Object, Sel, id, id, id, id),
);
extern "C" fn observe_value_for_key_path(
this: &Object,
_sel: Sel,
key_path: id,
of_object: id,
_change: id,
_context: id,
) {
let key = NSString(key_path);
if key.to_str() == "title" {
unsafe {
let function = this.get_ivar::<*mut c_void>("function");
if !function.is_null() {
let function = &mut *(*function
as *mut (Box<dyn for<'r> Fn(&'r Window, String)>, Rc<Window>));
let title: id = msg_send![of_object, title];
(function.0)(&function.1, NSString(title).to_str().to_string());
}
}
}
}
cls.register()
}
None => class!(DocumentTitleChangedDelegate),
};

let handler: id = msg_send![cls, new];
let document_title_changed_handler =
Box::into_raw(Box::new((document_title_changed_handler, window.clone())));

(*handler).set_ivar(
"function",
document_title_changed_handler as *mut _ as *mut c_void,
);

let _: () = msg_send![webview, addObserver:handler forKeyPath:NSString::new("title") options:0x01 context:nil ];

document_title_changed_handler
} else {
null_mut()
};

// Navigation handler
extern "C" fn navigation_policy(this: &Object, _: Sel, _: id, action: id, handler: id) {
unsafe {
Expand Down Expand Up @@ -684,6 +739,7 @@ impl InnerWebView {
manager,
pending_scripts,
ipc_handler_ptr,
document_title_changed_handler,
navigation_decide_policy_ptr,
#[cfg(target_os = "macos")]
file_drop_ptr,
Expand Down Expand Up @@ -935,28 +991,32 @@ impl Drop for InnerWebView {
// We need to drop handler closures here
unsafe {
if !self.ipc_handler_ptr.is_null() {
let _ = Box::from_raw(self.ipc_handler_ptr);
drop(Box::from_raw(self.ipc_handler_ptr));

let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME);
let _: () = msg_send![self.manager, removeScriptMessageHandlerForName: ipc];
}

if !self.document_title_changed_handler.is_null() {
drop(Box::from_raw(self.document_title_changed_handler));
}

if !self.navigation_decide_policy_ptr.is_null() {
let _ = Box::from_raw(self.navigation_decide_policy_ptr);
drop(Box::from_raw(self.navigation_decide_policy_ptr));
}

#[cfg(target_os = "macos")]
if !self.file_drop_ptr.is_null() {
let _ = Box::from_raw(self.file_drop_ptr);
drop(Box::from_raw(self.file_drop_ptr));
}

if !self.download_delegate.is_null() {
let _ = self.download_delegate.drop_in_place();
drop(self.download_delegate.drop_in_place());
}

for ptr in self.protocol_ptrs.iter() {
if !ptr.is_null() {
let _ = Box::from_raw(*ptr);
drop(Box::from_raw(*ptr));
}
}

Expand Down

0 comments on commit 14a0ee3

Please sign in to comment.