Find in page: the shell's find bar per pane (Ctrl+F, the app menu, the palette) over the engine's own find
Host: `tab_find {id, action, query}` (shell-only; start, next, previous, stop). On Windows it drives WebView2's own find (`ICoreWebView2Find` from `ICoreWebView2_28`, find options from `ICoreWebView2Environment15`; its built-in find bar suppressed, all matches highlighted) and reports the engine's match count and active match to the shell as `tab-find` events. On macOS, Linux and an older WebView2 Runtime native code evaluates the engine's `window.find` in the tab instead (next and previous work, no count); WKWebView's and WebKitGTK's native find are not wired here because this tree cannot be compiled or run for them on this machine. The query is one typed line (1..=256 characters, no control characters), goes to the engine and is kept nowhere. Tabs gain no command: a tab forwards the existing key name `find` (Ctrl+F) through tab_shortcut.
Frame: FindBar.tsx takes the address field's place in its pane's toolbar while open, so the page does not move and in split view the bar is the searched pane's. "3/17" where the engine counts, "No matches" for none; Enter / F3 next, Shift+Enter / Shift+F3 previous, Esc closes, clears the highlights and gives the page the keyboard. `features.find` (on by default) gates the menu row, the palette entry and the keys. Only what the page has rendered can be found, as in a browser.
Also: this makes `pub mod find` in lib.rs (already in HEAD) resolve again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
832396801c
commit
b31fa6b1d2
17 changed files with 449 additions and 19 deletions
|
|
@ -145,6 +145,115 @@ pub fn forget_tab_zoom<R: Runtime>(app: &AppHandle<R>, id: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
/// `tab_find`: searches tab `id`'s page ([`crate::find`]). The engine's own find where this build
|
||||
/// can drive it (WebView2's `ICoreWebView2Find`, which reports counts through `tab-find` events);
|
||||
/// otherwise the evaluated `window.find` fallback. The query is kept nowhere.
|
||||
pub fn tab_find<R: Runtime>(app: &AppHandle<R>, id: &str, action: &str, query: Option<&str>) -> Result<(), String> {
|
||||
if !policy::valid_tab_id(id) {
|
||||
return Err("VALIDATION: invalid tab id".into());
|
||||
}
|
||||
let request = crate::find::request(action, query)?;
|
||||
let tab = tab_webview(app, id).ok_or("TAB: not found")?;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let fallback = tab.clone();
|
||||
let (app, id) = (app.clone(), id.to_owned());
|
||||
tab.with_webview(move |platform| {
|
||||
// The engine has no find interface (an older WebView2 Runtime): the evaluated fallback.
|
||||
if !find_windows::run(&app, &platform, &id, &request) {
|
||||
let _ = fallback.eval(crate::find::fallback_script(&request));
|
||||
}
|
||||
})
|
||||
.map_err(|error| format!("TAB: {error}"))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
tab.eval(crate::find::fallback_script(&request)).map_err(|error| format!("TAB: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// WebView2's own find (`ICoreWebView2Find`, WebView2 Runtime with `ICoreWebView2_28`): the find
|
||||
/// session, its highlights and its counts are the engine's; its own find bar stays hidden.
|
||||
#[cfg(windows)]
|
||||
mod find_windows {
|
||||
use super::{emit_shell, AppHandle, Runtime};
|
||||
use crate::find::{FindAction, FindRequest, FindState, EVENT_TAB_FIND};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::{ICoreWebView2Environment15, ICoreWebView2Find, ICoreWebView2_28};
|
||||
use webview2_com::{FindActiveMatchIndexChangedEventHandler, FindMatchCountChangedEventHandler, FindStartCompletedHandler};
|
||||
use windows::core::{Interface, HSTRING};
|
||||
|
||||
/// Tab webviews whose find object already reports its counts (one pair of handlers each).
|
||||
fn watched() -> std::sync::MutexGuard<'static, HashSet<String>> {
|
||||
static WATCHED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
|
||||
WATCHED.get_or_init(Default::default).lock().unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
/// The tab's webview is gone: a new one under the same id registers its own handlers.
|
||||
pub(super) fn forget(id: &str) {
|
||||
watched().remove(id);
|
||||
}
|
||||
|
||||
fn report<R: Runtime>(app: &AppHandle<R>, id: &str, find: &ICoreWebView2Find) {
|
||||
let (mut count, mut index) = (0i32, -1i32);
|
||||
// SAFETY: COM property reads on the live find object, on the main thread.
|
||||
unsafe {
|
||||
let _ = find.MatchCount(&mut count);
|
||||
let _ = find.ActiveMatchIndex(&mut index);
|
||||
}
|
||||
emit_shell(app, EVENT_TAB_FIND, FindState { id: id.to_owned(), count, index });
|
||||
}
|
||||
|
||||
/// Runs `request` with the engine's find on the main thread; false when this runtime has none.
|
||||
pub(super) fn run<R: Runtime>(app: &AppHandle<R>, platform: &tauri::webview::PlatformWebview, id: &str, request: &FindRequest) -> bool {
|
||||
// SAFETY: COM calls on this webview's live controller and environment, on the main thread
|
||||
// (with_webview runs the closure there; the handlers run there too).
|
||||
unsafe {
|
||||
let Ok(core) = platform.controller().CoreWebView2() else { return false };
|
||||
let (Ok(core), Ok(environment)) = (core.cast::<ICoreWebView2_28>(), platform.environment().cast::<ICoreWebView2Environment15>()) else { return false };
|
||||
let Ok(find) = core.Find() else { return false };
|
||||
if watched().insert(id.to_owned()) {
|
||||
let mut token = 0i64;
|
||||
let (count_app, count_id) = (app.clone(), id.to_owned());
|
||||
let counted = FindMatchCountChangedEventHandler::create(Box::new(move |sender, _| {
|
||||
if let Some(find) = sender { report(&count_app, &count_id, &find); }
|
||||
Ok(())
|
||||
}));
|
||||
let _ = find.add_MatchCountChanged(&counted, &mut token);
|
||||
let (index_app, index_id) = (app.clone(), id.to_owned());
|
||||
let moved = FindActiveMatchIndexChangedEventHandler::create(Box::new(move |sender, _| {
|
||||
if let Some(find) = sender { report(&index_app, &index_id, &find); }
|
||||
Ok(())
|
||||
}));
|
||||
let _ = find.add_ActiveMatchIndexChanged(&moved, &mut token);
|
||||
}
|
||||
match (request.action, request.query.as_deref()) {
|
||||
(FindAction::Start, Some(query)) => {
|
||||
let Ok(options) = environment.CreateFindOptions() else { return false };
|
||||
let _ = options.SetFindTerm(&HSTRING::from(query));
|
||||
let _ = options.SetSuppressDefaultFindDialog(true);
|
||||
let _ = options.SetShouldHighlightAllMatches(true);
|
||||
let _ = options.SetIsCaseSensitive(false);
|
||||
let _ = options.SetShouldMatchWord(false);
|
||||
// A new query is a new session, from the first match.
|
||||
let _ = find.Stop();
|
||||
let (done_app, done_id, done_find) = (app.clone(), id.to_owned(), find.clone());
|
||||
let started = FindStartCompletedHandler::create(Box::new(move |_| {
|
||||
report(&done_app, &done_id, &done_find);
|
||||
Ok(())
|
||||
}));
|
||||
let _ = find.Start(&options, &started);
|
||||
}
|
||||
(FindAction::Next, _) => { let _ = find.FindNext(); }
|
||||
(FindAction::Previous, _) => { let _ = find.FindPrevious(); }
|
||||
(FindAction::Stop, _) | (FindAction::Start, None) => { let _ = find.Stop(); }
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A browser shortcut pressed inside tab `id`, forwarded to the shell (`tab-shortcut`).
|
||||
pub fn forward_shortcut<R: Runtime>(app: &AppHandle<R>, id: String, shortcut: String) -> Result<(), String> {
|
||||
if !tabs::valid_shortcut(&shortcut) {
|
||||
|
|
@ -803,6 +912,11 @@ pub fn forget_tab<R: Runtime>(app: &AppHandle<R>, label: &str) {
|
|||
}
|
||||
#[cfg(not(windows))]
|
||||
let _ = app;
|
||||
// Its find session went with the webview: a new one under the same id registers its own handlers.
|
||||
#[cfg(windows)]
|
||||
if let Some(id) = policy::tab_id(label) {
|
||||
find_windows::forget(id);
|
||||
}
|
||||
placements().forget(label);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue