Skip to content

Commit 7caad38

Browse files
committed
Refactor state management for WebView to use shared native-based state
- Remove redundant properties (`lastRequestedUrl`, `hasLoadedOnce`) and simplify loading logic in `WebViewState`. - Introduce `WebViewState` in Rust to centralize loading and URL tracking. - Enhance Rust-WebView integration with thread-safe shared state for navigation and page load events. - Update Kotlin binding to reflect native state updates directly.
1 parent 96600d3 commit 7caad38

2 files changed

Lines changed: 97 additions & 48 deletions

File tree

  • wrywebview-compose/src/jvmMain/kotlin/io/github/kdroidfilter/composewebview
  • wrywebview/src/main/rust

wrywebview-compose/src/jvmMain/kotlin/io/github/kdroidfilter/composewebview/WebViewState.kt

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import io.github.kdroidfilter.composewebview.wry.WryWebViewPanel
1111
@Stable
1212
class WebViewState(initialUrl: String) {
1313
internal var panel: WryWebViewPanel? = null
14-
private var lastRequestedUrl: String = initialUrl
15-
private var hasLoadedOnce: Boolean = false
1614

1715
/**
1816
* The target URL to navigate to. Changing this will trigger navigation.
@@ -21,13 +19,14 @@ class WebViewState(initialUrl: String) {
2119

2220
/**
2321
* The current URL displayed in the WebView.
24-
* Updated after navigation completes.
22+
* Updated from native state.
2523
*/
2624
var currentUrl: String by mutableStateOf("")
2725
internal set
2826

2927
/**
3028
* Whether the WebView is currently loading content.
29+
* Updated from native state via page load handlers.
3130
*/
3231
var isLoading: Boolean by mutableStateOf(true)
3332
internal set
@@ -48,23 +47,20 @@ class WebViewState(initialUrl: String) {
4847
* Navigate back in the browsing history.
4948
*/
5049
fun goBack() {
51-
isLoading = true
5250
panel?.goBack()
5351
}
5452

5553
/**
5654
* Navigate forward in the browsing history.
5755
*/
5856
fun goForward() {
59-
isLoading = true
6057
panel?.goForward()
6158
}
6259

6360
/**
6461
* Reload the current page.
6562
*/
6663
fun reload() {
67-
isLoading = true
6864
panel?.reload()
6965
}
7066

@@ -73,38 +69,23 @@ class WebViewState(initialUrl: String) {
7369
*/
7470
fun loadUrl(newUrl: String) {
7571
url = newUrl
76-
lastRequestedUrl = newUrl
77-
isLoading = true
7872
panel?.loadUrl(newUrl)
7973
}
8074

8175
/**
82-
* Refresh the state by querying the WebView.
76+
* Refresh the state by querying the native WebView state.
8377
*/
8478
internal fun refreshState() {
8579
panel?.let { p ->
8680
if (p.isReady()) {
87-
// Update current URL
81+
// Get current URL from native state
8882
p.getCurrentUrl()?.let { newUrl ->
89-
if (newUrl.isNotEmpty() && newUrl != "about:blank") {
90-
val urlChanged = newUrl != currentUrl
83+
if (newUrl.isNotEmpty()) {
9184
currentUrl = newUrl
92-
93-
// If URL changed or we got a valid URL for the first time, we're done loading
94-
if (urlChanged || !hasLoadedOnce) {
95-
hasLoadedOnce = true
96-
isLoading = false
97-
}
98-
}
99-
}
100-
101-
// Also check native loading state
102-
if (isLoading && hasLoadedOnce) {
103-
val nativeLoading = p.isLoading()
104-
if (!nativeLoading) {
105-
isLoading = false
10685
}
10786
}
87+
// Get loading state from native state
88+
isLoading = p.isLoading()
10889
}
10990
}
11091
}

wrywebview/src/main/rust/lib.rs

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::HashMap;
2-
use std::sync::atomic::{AtomicU64, Ordering};
3-
use std::sync::{Mutex, OnceLock};
2+
use std::sync::atomic::{AtomicU64, AtomicBool, Ordering};
3+
use std::sync::{Mutex, OnceLock, Arc};
44
use std::thread::ThreadId;
55

66
use wry::dpi::{LogicalPosition, LogicalSize};
@@ -73,10 +73,25 @@ impl HasWindowHandle for RawWindow {
7373
}
7474
}
7575

76-
#[derive(Clone, Copy)]
76+
struct WebViewState {
77+
is_loading: AtomicBool,
78+
current_url: Mutex<String>,
79+
}
80+
7781
struct WebViewEntry {
7882
ptr: *mut WebView,
7983
thread_id: ThreadId,
84+
state: Arc<WebViewState>,
85+
}
86+
87+
impl Clone for WebViewEntry {
88+
fn clone(&self) -> Self {
89+
WebViewEntry {
90+
ptr: self.ptr,
91+
thread_id: self.thread_id,
92+
state: Arc::clone(&self.state),
93+
}
94+
}
8095
}
8196

8297
// The raw pointer is only dereferenced on the creating thread (checked at runtime).
@@ -299,15 +314,49 @@ fn create_webview_inner(
299314
#[cfg(target_os = "linux")]
300315
ensure_gtk_initialized()?;
301316

317+
// Create shared state for tracking loading and URL
318+
let state = Arc::new(WebViewState {
319+
is_loading: AtomicBool::new(true),
320+
current_url: Mutex::new(url.clone()),
321+
});
322+
323+
let state_for_nav = Arc::clone(&state);
324+
let state_for_load = Arc::clone(&state);
325+
302326
let webview = WebViewBuilder::new()
303327
.with_url(&url)
304328
.with_bounds(make_bounds(0, 0, width, height))
329+
.with_navigation_handler(move |new_url| {
330+
eprintln!("[wrywebview] navigation_handler url={}", new_url);
331+
// Navigation started
332+
state_for_nav.is_loading.store(true, Ordering::SeqCst);
333+
if let Ok(mut current) = state_for_nav.current_url.lock() {
334+
*current = new_url.clone();
335+
}
336+
true // Allow navigation
337+
})
338+
.with_on_page_load_handler(move |event, url| {
339+
match event {
340+
wry::PageLoadEvent::Started => {
341+
eprintln!("[wrywebview] page_load_handler event=Started url={}", url);
342+
state_for_load.is_loading.store(true, Ordering::SeqCst);
343+
}
344+
wry::PageLoadEvent::Finished => {
345+
eprintln!("[wrywebview] page_load_handler event=Finished url={}", url);
346+
state_for_load.is_loading.store(false, Ordering::SeqCst);
347+
if let Ok(mut current) = state_for_load.current_url.lock() {
348+
*current = url.clone();
349+
}
350+
}
351+
}
352+
})
305353
.build_as_child(&window)?;
306354

307355
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
308356
let entry = WebViewEntry {
309357
ptr: Box::into_raw(Box::new(webview)),
310358
thread_id: std::thread::current().id(),
359+
state,
311360
};
312361

313362
let mut map = webviews()
@@ -379,6 +428,10 @@ pub fn set_bounds(
379428

380429
fn load_url_inner(id: u64, url: String) -> Result<(), WebViewError> {
381430
eprintln!("[wrywebview] load_url id={} url={}", id, url);
431+
// Mark as loading before starting navigation
432+
if let Ok(state) = get_state(id) {
433+
state.is_loading.store(true, Ordering::SeqCst);
434+
}
382435
with_webview(id, |webview| webview.load_url(&url).map_err(WebViewError::from))
383436
}
384437

@@ -393,6 +446,9 @@ pub fn load_url(id: u64, url: String) -> Result<(), WebViewError> {
393446

394447
fn go_back_inner(id: u64) -> Result<(), WebViewError> {
395448
eprintln!("[wrywebview] go_back id={}", id);
449+
if let Ok(state) = get_state(id) {
450+
state.is_loading.store(true, Ordering::SeqCst);
451+
}
396452
with_webview(id, |webview| {
397453
webview.evaluate_script("window.history.back()").map_err(WebViewError::from)
398454
})
@@ -409,6 +465,9 @@ pub fn go_back(id: u64) -> Result<(), WebViewError> {
409465

410466
fn go_forward_inner(id: u64) -> Result<(), WebViewError> {
411467
eprintln!("[wrywebview] go_forward id={}", id);
468+
if let Ok(state) = get_state(id) {
469+
state.is_loading.store(true, Ordering::SeqCst);
470+
}
412471
with_webview(id, |webview| {
413472
webview.evaluate_script("window.history.forward()").map_err(WebViewError::from)
414473
})
@@ -425,6 +484,9 @@ pub fn go_forward(id: u64) -> Result<(), WebViewError> {
425484

426485
fn reload_inner(id: u64) -> Result<(), WebViewError> {
427486
eprintln!("[wrywebview] reload id={}", id);
487+
if let Ok(state) = get_state(id) {
488+
state.is_loading.store(true, Ordering::SeqCst);
489+
}
428490
with_webview(id, |webview| {
429491
webview.evaluate_script("window.location.reload()").map_err(WebViewError::from)
430492
})
@@ -439,39 +501,45 @@ pub fn reload(id: u64) -> Result<(), WebViewError> {
439501
run_on_main_thread(move || reload_inner(id))
440502
}
441503

442-
fn get_url_inner(id: u64) -> Result<String, WebViewError> {
504+
fn focus_inner(id: u64) -> Result<(), WebViewError> {
505+
eprintln!("[wrywebview] focus id={}", id);
443506
with_webview(id, |webview| {
444-
Ok(webview.url().map(|u| u.to_string()).unwrap_or_default())
507+
// Focus the webview by focusing the document
508+
webview.evaluate_script("document.documentElement.focus(); window.focus();").map_err(WebViewError::from)
445509
})
446510
}
447511

448512
#[uniffi::export]
449-
pub fn get_url(id: u64) -> Result<String, WebViewError> {
513+
pub fn focus(id: u64) -> Result<(), WebViewError> {
450514
#[cfg(target_os = "linux")]
451515
{
452-
return run_on_gtk_thread(move || get_url_inner(id));
516+
return run_on_gtk_thread(move || focus_inner(id));
453517
}
454-
run_on_main_thread(move || get_url_inner(id))
518+
run_on_main_thread(move || focus_inner(id))
455519
}
456520

457-
fn is_loading_inner(id: u64) -> Result<bool, WebViewError> {
458-
with_webview(id, |webview| {
459-
// Check if we can get the URL - if we can and it's not about:blank, we're not loading
460-
// This is a workaround since evaluate_script doesn't return values synchronously
461-
match webview.url() {
462-
Ok(url) => Ok(url.as_str() == "about:blank"),
463-
Err(_) => Ok(true), // If we can't get URL, assume still loading
464-
}
465-
})
521+
fn get_state(id: u64) -> Result<Arc<WebViewState>, WebViewError> {
522+
let map = webviews()
523+
.lock()
524+
.map_err(|_| WebViewError::Internal("webview registry lock poisoned".to_string()))?;
525+
let entry = map
526+
.get(&id)
527+
.ok_or(WebViewError::WebViewNotFound(id))?;
528+
Ok(Arc::clone(&entry.state))
529+
}
530+
531+
#[uniffi::export]
532+
pub fn get_url(id: u64) -> Result<String, WebViewError> {
533+
let state = get_state(id)?;
534+
let url = state.current_url.lock()
535+
.map_err(|_| WebViewError::Internal("url lock poisoned".to_string()))?;
536+
Ok(url.clone())
466537
}
467538

468539
#[uniffi::export]
469540
pub fn is_loading(id: u64) -> Result<bool, WebViewError> {
470-
#[cfg(target_os = "linux")]
471-
{
472-
return run_on_gtk_thread(move || is_loading_inner(id));
473-
}
474-
run_on_main_thread(move || is_loading_inner(id))
541+
let state = get_state(id)?;
542+
Ok(state.is_loading.load(Ordering::SeqCst))
475543
}
476544

477545
fn destroy_webview_inner(id: u64) -> Result<(), WebViewError> {

0 commit comments

Comments
 (0)