From ea36294cbca98f7725c91d1464fd92e77c89698a Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sat, 12 Apr 2025 21:10:07 -0300 Subject: [PATCH] feat(core): allow changing or disabling the input accessory view on iOS (#13208) * feat(core): allow changing or disabling the input accessory view on iOS needs https://github.com/tauri-apps/wry/pull/1544 * remove unused code * fix imports * lint * fix features * wry 0.51.2 --- .changes/disable-input-accessory-view-ios.md | 7 +++ .../input-accessory-view-builder-runtime.md | 6 ++ .changes/input-accessory-view-builder.md | 5 ++ Cargo.lock | 7 ++- crates/tauri-cli/config.schema.json | 5 ++ crates/tauri-runtime-wry/Cargo.toml | 2 +- crates/tauri-runtime-wry/src/lib.rs | 17 +++-- crates/tauri-runtime/Cargo.toml | 7 +++ crates/tauri-runtime/src/webview.rs | 47 +++++++++++++- .../schemas/config.schema.json | 5 ++ crates/tauri-utils/src/config.rs | 15 ++++- crates/tauri/Cargo.toml | 8 ++- crates/tauri/src/app.rs | 4 -- crates/tauri/src/webview/mod.rs | 30 +++++++++ crates/tauri/src/webview/plugin.rs | 11 ++++ crates/tauri/src/webview/webview_window.rs | 63 ++++++++++++++++--- packages/api/src/webview.ts | 7 +++ packages/api/src/window.ts | 7 +++ 18 files changed, 230 insertions(+), 23 deletions(-) create mode 100644 .changes/disable-input-accessory-view-ios.md create mode 100644 .changes/input-accessory-view-builder-runtime.md create mode 100644 .changes/input-accessory-view-builder.md diff --git a/.changes/disable-input-accessory-view-ios.md b/.changes/disable-input-accessory-view-ios.md new file mode 100644 index 000000000..1dc82e38d --- /dev/null +++ b/.changes/disable-input-accessory-view-ios.md @@ -0,0 +1,7 @@ +--- +"@tauri-apps/api": minor:feat +"tauri-utils": minor:feat +--- + +Added `disableInputAccessoryView: bool` config for iOS. + diff --git a/.changes/input-accessory-view-builder-runtime.md b/.changes/input-accessory-view-builder-runtime.md new file mode 100644 index 000000000..890342d38 --- /dev/null +++ b/.changes/input-accessory-view-builder-runtime.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime": minor:feat +"tauri-runtime-wry": minor:feat +--- + +Added `WebviewAttributes::input_accessory_view_builder` on iOS. diff --git a/.changes/input-accessory-view-builder.md b/.changes/input-accessory-view-builder.md new file mode 100644 index 000000000..db2fd8037 --- /dev/null +++ b/.changes/input-accessory-view-builder.md @@ -0,0 +1,5 @@ +--- +"tauri": minor:feat +--- + +Added `WebviewWindowBuilder::with_input_accessory_view_builder` and `WebviewBuilder::with_input_accessory_view_builder` on iOS. diff --git a/Cargo.lock b/Cargo.lock index cf2a6e88e..d1ed22b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8408,6 +8408,7 @@ dependencies = [ "objc2 0.6.0", "objc2-app-kit", "objc2-foundation 0.3.0", + "objc2-ui-kit", "objc2-web-kit", "percent-encoding", "plist", @@ -8753,6 +8754,8 @@ dependencies = [ "gtk", "http 1.2.0", "jni", + "objc2 0.6.0", + "objc2-ui-kit", "raw-window-handle", "serde", "serde_json", @@ -10816,9 +10819,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.51.1" +version = "0.51.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48846531c50ee2e209a396ddd24af04ca1584be814e750fb81b395c8e7983ff9" +checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2" dependencies = [ "base64 0.22.1", "block2 0.6.0", diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 537808185..783de89e2 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -561,6 +561,11 @@ "description": "on macOS and iOS there is a link preview on long pressing links, this is enabled by default.\n see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview", "default": true, "type": "boolean" + }, + "disableInputAccessoryView": { + "description": "Allows disabling the input accessory view on iOS.\n\n The accessory view is the view that appears above the keyboard when a text input element is focused.\n It usually displays a view with \"Done\", \"Next\" buttons.", + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/crates/tauri-runtime-wry/Cargo.toml b/crates/tauri-runtime-wry/Cargo.toml index 1beecbefc..c4dcbd96b 100644 --- a/crates/tauri-runtime-wry/Cargo.toml +++ b/crates/tauri-runtime-wry/Cargo.toml @@ -17,7 +17,7 @@ rustc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -wry = { version = "0.51", default-features = false, features = [ +wry = { version = "0.51.2", default-features = false, features = [ "drag-drop", "protocol", "os-webview", diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index b559059f8..343b4e332 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -30,7 +30,7 @@ use tauri_runtime::{ UserAttentionType, UserEvent, WebviewDispatch, WebviewEventId, WindowDispatch, WindowEventId, }; -#[cfg(any(target_os = "macos", target_os = "ios"))] +#[cfg(target_vendor = "apple")] use objc2::rc::Retained; #[cfg(target_os = "macos")] use tao::platform::macos::{EventLoopWindowTargetExtMacOS, WindowBuilderExtMacOS}; @@ -42,9 +42,11 @@ use tao::platform::windows::{WindowBuilderExtWindows, WindowExtWindows}; use webview2_com::FocusChangedEventHandler; #[cfg(windows)] use windows::Win32::Foundation::HWND; +#[cfg(target_os = "ios")] +use wry::WebViewBuilderExtIos; #[cfg(windows)] use wry::WebViewBuilderExtWindows; -#[cfg(any(target_os = "macos", target_os = "ios"))] +#[cfg(target_vendor = "apple")] use wry::{WebViewBuilderExtDarwin, WebViewExtDarwin}; use tao::{ @@ -2987,7 +2989,7 @@ impl Runtime for Wry { } #[cfg(target_os = "ios")] - fn run_return) + 'static>(mut self, callback: F) -> i32 { + fn run_return) + 'static>(self, callback: F) -> i32 { self.run(callback); 0 } @@ -4627,9 +4629,16 @@ fn create_webview( webview_builder.with_allow_link_preview(webview_attributes.allow_link_preview); } + #[cfg(target_os = "ios")] + { + if let Some(input_accessory_view_builder) = webview_attributes.input_accessory_view_builder { + webview_builder = webview_builder + .with_input_accessory_view_builder(move |webview| input_accessory_view_builder.0(webview)); + } + } + #[cfg(target_os = "macos")] { - use wry::WebViewBuilderExtDarwin; if let Some(position) = &webview_attributes.traffic_light_position { webview_builder = webview_builder.with_traffic_light_inset(*position); } diff --git a/crates/tauri-runtime/Cargo.toml b/crates/tauri-runtime/Cargo.toml index e618edb49..8c2242458 100644 --- a/crates/tauri-runtime/Cargo.toml +++ b/crates/tauri-runtime/Cargo.toml @@ -47,6 +47,13 @@ gtk = { version = "0.18", features = ["v3_24"] } [target."cfg(target_os = \"android\")".dependencies] jni = "0.21" +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] +objc2 = "0.6" +objc2-ui-kit = { version = "0.3.0", default-features = false, features = [ + "UIView", + "UIResponder", +] } + [target."cfg(target_os = \"macos\")".dependencies] url = "2" diff --git a/crates/tauri-runtime/src/webview.rs b/crates/tauri-runtime/src/webview.rs index ca834bcaf..fb186695f 100644 --- a/crates/tauri-runtime/src/webview.rs +++ b/crates/tauri-runtime/src/webview.rs @@ -34,6 +34,12 @@ type OnPageLoadHandler = dyn Fn(Url, PageLoadEvent) + Send; type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync; +#[cfg(target_os = "ios")] +type InputAccessoryViewBuilderFn = dyn Fn(&objc2_ui_kit::UIView) -> Option> + + Send + + Sync + + 'static; + /// Download event. pub enum DownloadEvent<'a> { /// Download requested. @@ -193,7 +199,7 @@ impl> PartialEq for DetachedWebview { } /// The attributes used to create an webview. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct WebviewAttributes { pub url: WebviewUrl, pub user_agent: Option, @@ -236,6 +242,37 @@ pub struct WebviewAttributes { /// on macOS and iOS there is a link preview on long pressing links, this is enabled by default. /// see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview pub allow_link_preview: bool, + /// Allows overriding the the keyboard accessory view on iOS. + /// Returning `None` effectively removes the view. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub input_accessory_view_builder: Option, +} + +#[cfg(target_os = "ios")] +#[non_exhaustive] +pub struct InputAccessoryViewBuilder(pub Box); + +#[cfg(target_os = "ios")] +impl std::fmt::Debug for InputAccessoryViewBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("InputAccessoryViewBuilder").finish() + } +} + +#[cfg(target_os = "ios")] +impl InputAccessoryViewBuilder { + pub fn new(builder: Box) -> Self { + Self(builder) + } } impl From<&WindowConfig> for WebviewAttributes { @@ -281,6 +318,12 @@ impl From<&WindowConfig> for WebviewAttributes { } builder.javascript_disabled = config.javascript_disabled; builder.allow_link_preview = config.allow_link_preview; + #[cfg(target_os = "ios")] + if config.disable_input_accessory_view { + builder + .input_accessory_view_builder + .replace(InputAccessoryViewBuilder::new(Box::new(|_webview| None))); + } builder } } @@ -315,6 +358,8 @@ impl WebviewAttributes { background_throttling: None, javascript_disabled: false, allow_link_preview: true, + #[cfg(target_os = "ios")] + input_accessory_view_builder: None, } } diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 537808185..783de89e2 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -561,6 +561,11 @@ "description": "on macOS and iOS there is a link preview on long pressing links, this is enabled by default.\n see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview", "default": true, "type": "boolean" + }, + "disableInputAccessoryView": { + "description": "Allows disabling the input accessory view on iOS.\n\n The accessory view is the view that appears above the keyboard when a text input element is focused.\n It usually displays a view with \"Done\", \"Next\" buttons.", + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index ac20ac8de..0ecf015b7 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -1777,6 +1777,16 @@ pub struct WindowConfig { /// see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview #[serde(default = "default_true", alias = "allow-link-preview")] pub allow_link_preview: bool, + /// Allows disabling the input accessory view on iOS. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + #[serde( + default, + alias = "disable-input-accessory-view", + alias = "disable_input_accessory_view" + )] + pub disable_input_accessory_view: bool, } impl Default for WindowConfig { @@ -1834,6 +1844,7 @@ impl Default for WindowConfig { background_throttling: None, javascript_disabled: false, allow_link_preview: true, + disable_input_accessory_view: false, } } } @@ -3163,6 +3174,7 @@ mod build { let background_throttling = opt_lit(self.background_throttling.as_ref()); let javascript_disabled = self.javascript_disabled; let allow_link_preview = self.allow_link_preview; + let disable_input_accessory_view = self.disable_input_accessory_view; literal_struct!( tokens, @@ -3218,7 +3230,8 @@ mod build { background_color, background_throttling, javascript_disabled, - allow_link_preview + allow_link_preview, + disable_input_accessory_view ); } } diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 1133cc3ae..af8e91629 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -97,11 +97,14 @@ tray-icon = { version = "0.20", default-features = false, features = [ gtk = { version = "0.18", features = ["v3_24"] } webkit2gtk = { version = "=2.0.1", features = ["v2_40"], optional = true } +# darwin +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2 = "0.6" + # macOS [target.'cfg(target_os = "macos")'.dependencies] embed_plist = "1.2" plist = "1" -objc2 = "0.6" objc2-foundation = { version = "0.3", default-features = false, features = [ "std", "NSData", @@ -145,6 +148,9 @@ jni = "0.21" [target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] libc = "0.2" swift-rs = "1" +objc2-ui-kit = { version = "0.3.0", default-features = false, features = [ + "UIView", +] } [build-dependencies] glob = "0.3" diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index 42a0eb1d0..d2a4a21b6 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -390,8 +390,6 @@ impl AppHandle { /// /// Needs to be called from Main Thread pub async fn fetch_data_store_identifiers(&self) -> crate::Result> { - use std::sync::Mutex; - let (tx, rx) = tokio::sync::oneshot::channel::, tauri_runtime::Error>>(); let lock: Arc>> = Arc::new(Mutex::new(Some(tx))); let runtime_handle = self.runtime_handle.clone(); @@ -415,8 +413,6 @@ impl AppHandle { /// /// Needs to be called from Main Thread pub async fn remove_data_store(&self, uuid: [u8; 16]) -> crate::Result<()> { - use std::sync::Mutex; - let (tx, rx) = tokio::sync::oneshot::channel::>(); let lock: Arc>> = Arc::new(Mutex::new(Some(tx))); let runtime_handle = self.runtime_handle.clone(); diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs index 85ae2df3c..ad6b8a8c3 100644 --- a/crates/tauri/src/webview/mod.rs +++ b/crates/tauri/src/webview/mod.rs @@ -992,6 +992,36 @@ fn main() { .allow_link_preview(allow_link_preview); self } + + /// Allows overriding the the keyboard accessory view on iOS. + /// Returning `None` effectively removes the view. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub fn with_input_accessory_view_builder< + F: Fn(&objc2_ui_kit::UIView) -> Option> + + Send + + Sync + + 'static, + >( + mut self, + builder: F, + ) -> Self { + self + .webview_attributes + .input_accessory_view_builder + .replace(tauri_runtime::webview::InputAccessoryViewBuilder::new( + Box::new(builder), + )); + self + } } /// Webview. diff --git a/crates/tauri/src/webview/plugin.rs b/crates/tauri/src/webview/plugin.rs index c8c0de09c..4824dd5e7 100644 --- a/crates/tauri/src/webview/plugin.rs +++ b/crates/tauri/src/webview/plugin.rs @@ -54,6 +54,8 @@ mod desktop_commands { javascript_disabled: bool, #[serde(default = "default_true")] allow_link_preview: bool, + #[serde(default)] + pub disable_input_accessory_view: bool, } #[cfg(feature = "unstable")] @@ -72,6 +74,15 @@ mod desktop_commands { builder.webview_attributes.background_throttling = config.background_throttling; builder.webview_attributes.javascript_disabled = config.javascript_disabled; builder.webview_attributes.allow_link_preview = config.allow_link_preview; + #[cfg(target_os = "ios")] + if config.disable_input_accessory_view { + builder + .webview_attributes + .input_accessory_view_builder + .replace(tauri_runtime::InputAccessoryViewBuilder::new(Box::new( + |_webview| None, + ))); + } builder } } diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs index a333ea347..1cf402838 100644 --- a/crates/tauri/src/webview/webview_window.rs +++ b/crates/tauri/src/webview/webview_window.rs @@ -27,10 +27,7 @@ use crate::{ UserAttentionType, }, }; -use tauri_utils::{ - config::{BackgroundThrottlingPolicy, Color, WebviewUrl, WindowConfig}, - Theme, -}; +use tauri_utils::config::{BackgroundThrottlingPolicy, Color, WebviewUrl, WindowConfig}; use url::Url; use crate::{ @@ -825,8 +822,6 @@ impl> WebviewWindowBuilder<'_, R, M> { /// # Examples /// /// ```rust - /// use tauri::{WebviewWindowBuilder, Runtime}; - /// /// const INIT_SCRIPT: &str = r#" /// if (window.location.origin === 'https://tauri.app') { /// console.log("hello world from js init script"); @@ -869,8 +864,6 @@ impl> WebviewWindowBuilder<'_, R, M> { /// # Examples /// /// ```rust - /// use tauri::{WebviewWindowBuilder, Runtime}; - /// /// const INIT_SCRIPT: &str = r#" /// if (window.location.origin === 'https://tauri.app') { /// console.log("hello world from js init script"); @@ -1115,6 +1108,58 @@ impl> WebviewWindowBuilder<'_, R, M> { self.webview_builder = self.webview_builder.disable_javascript(); self } + + /// Allows overriding the the keyboard accessory view on iOS. + /// Returning `None` effectively removes the view. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Examples + /// + /// ``` + /// fn main() { + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut builder = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())); + /// #[cfg(target_os = "ios")] + /// { + /// window_builder = window_builder.with_input_accessory_view_builder(|_webview| unsafe { + /// let mtm = objc2_foundation::MainThreadMarker::new_unchecked(); + /// let button = objc2_ui_kit::UIButton::buttonWithType(objc2_ui_kit::UIButtonType(1), mtm); + /// button.setTitle_forState( + /// Some(&objc2_foundation::NSString::from_str("Tauri")), + /// objc2_ui_kit::UIControlState(0), + /// ); + /// Some(button.downcast().unwrap()) + /// }); + /// } + /// let webview = builder.build()?; + /// Ok(()) + /// }); + /// } + /// ``` + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub fn with_input_accessory_view_builder< + F: Fn(&objc2_ui_kit::UIView) -> Option> + + Send + + Sync + + 'static, + >( + mut self, + builder: F, + ) -> Self { + self.webview_builder = self + .webview_builder + .with_input_accessory_view_builder(builder); + self + } } /// A type that wraps a [`Window`] together with a [`Webview`]. @@ -1919,7 +1964,7 @@ impl WebviewWindow { } /// Set the window theme. - pub fn set_theme(&self, theme: Option) -> crate::Result<()> { + pub fn set_theme(&self, theme: Option) -> crate::Result<()> { self.window.set_theme(theme) } } diff --git a/packages/api/src/webview.ts b/packages/api/src/webview.ts index c1d3ea426..da31a72eb 100644 --- a/packages/api/src/webview.ts +++ b/packages/api/src/webview.ts @@ -800,6 +800,13 @@ interface WebviewOptions { * see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview */ allowLinkPreview?: boolean + /** + * Allows disabling the input accessory view on iOS. + * + * The accessory view is the view that appears above the keyboard when a text input element is focused. + * It usually displays a view with "Done", "Next" buttons. + */ + disableInputAccessoryView?: boolean } export { Webview, getCurrentWebview, getAllWebviews } diff --git a/packages/api/src/window.ts b/packages/api/src/window.ts index 17d381091..0750d11c2 100644 --- a/packages/api/src/window.ts +++ b/packages/api/src/window.ts @@ -2411,6 +2411,13 @@ interface WindowOptions { * see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview */ allowLinkPreview?: boolean + /** + * Allows disabling the input accessory view on iOS. + * + * The accessory view is the view that appears above the keyboard when a text input element is focused. + * It usually displays a view with "Done", "Next" buttons. + */ + disableInputAccessoryView?: boolean } function mapMonitor(m: Monitor | null): Monitor | null {