Skip to content

Commit b9bfc60

Browse files
committed
Implement Linux-specific GTK thread management and improve bounds handling
1 parent a900356 commit b9bfc60

7 files changed

Lines changed: 128 additions & 15 deletions

File tree

demo/src/jvmMain/kotlin/io/github/kdroidfilter/composewebview/App.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.Preview
1111
fun App() {
1212
MaterialTheme {
1313
WryWebView(
14-
url = "https://netfree.link",
14+
url = "https://jetbrains.com/",
1515
modifier = Modifier.fillMaxSize(),
1616
)
1717

wrywebview-compose/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import io.github.kdroidfilter.composewebview.WryWebView
2323
@Composable
2424
fun App() {
2525
WryWebView(
26-
url = "https://netfree.link",
26+
url = "https://sample.com",
2727
modifier = Modifier.fillMaxSize(),
2828
)
2929
}

wrywebview-compose/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ kotlin {
1111
jvmMain.dependencies {
1212
implementation(compose.runtime)
1313
implementation(compose.ui)
14-
implementation(compose.desktop.currentOs)
1514
api(project(":wrywebview"))
1615
}
1716
}

wrywebview/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wrywebview/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ wry = "0.53.5"
1616

1717
[target.'cfg(target_os = "linux")'.dependencies]
1818
gtk = "0.18"
19+
glib = "0.18"
1920

2021
[target.'cfg(target_os = "macos")'.dependencies]
2122
dispatch2 = "0.3.0"

wrywebview/src/main/kotlin/io/github/kdroidfilter/composewebview/wry/WryWebViewPanel.kt

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class WryWebViewPanel(initialUrl: String) : JPanel() {
1919
private var windowsTimer: Timer? = null
2020
private var skikoInitialized: Boolean = false
2121
private var lastBounds: Bounds? = null
22+
private var pendingBounds: Bounds? = null
23+
private var boundsTimer: Timer? = null
2224

2325
init {
2426
layout = BorderLayout()
@@ -107,6 +109,7 @@ class WryWebViewPanel(initialUrl: String) : JPanel() {
107109
private fun destroyIfNeeded() {
108110
stopGtkPump()
109111
stopWindowsPump()
112+
stopBoundsTimer()
110113
webviewId?.let {
111114
log("destroy id=$it")
112115
NativeBindings.destroyWebview(it)
@@ -118,19 +121,36 @@ class WryWebViewPanel(initialUrl: String) : JPanel() {
118121
}
119122

120123
private fun updateBounds() {
121-
webviewId?.let {
122-
val bounds = boundsInParent()
123-
if (bounds == lastBounds) return
124-
lastBounds = bounds
125-
log("setBounds id=$it pos=(${bounds.x}, ${bounds.y}) size=${bounds.width}x${bounds.height}")
126-
NativeBindings.setBounds(it, bounds.x, bounds.y, bounds.width, bounds.height)
124+
val id = webviewId ?: return
125+
val bounds = boundsInParent()
126+
if (IS_LINUX) {
127+
pendingBounds = bounds
128+
if (boundsTimer == null) {
129+
boundsTimer = Timer(16) {
130+
val currentId = webviewId ?: return@Timer
131+
val toSend = pendingBounds ?: return@Timer
132+
pendingBounds = null
133+
if (toSend != lastBounds) {
134+
lastBounds = toSend
135+
log("setBounds id=$currentId pos=(${toSend.x}, ${toSend.y}) size=${toSend.width}x${toSend.height}")
136+
NativeBindings.setBounds(currentId, toSend.x, toSend.y, toSend.width, toSend.height)
137+
}
138+
if (pendingBounds == null) {
139+
stopBoundsTimer()
140+
}
141+
}.apply { start() }
142+
}
143+
return
127144
}
145+
if (bounds == lastBounds) return
146+
lastBounds = bounds
147+
log("setBounds id=$id pos=(${bounds.x}, ${bounds.y}) size=${bounds.width}x${bounds.height}")
148+
NativeBindings.setBounds(id, bounds.x, bounds.y, bounds.width, bounds.height)
128149
}
129150

130151
private fun startGtkPumpIfNeeded() {
131152
if (!IS_LINUX || gtkTimer != null) return
132-
log("startGtkPump")
133-
gtkTimer = Timer(16) { NativeBindings.pumpGtkEvents() }.apply { start() }
153+
log("startGtkPump (noop, handled in native GTK thread)")
134154
}
135155

136156
private fun stopGtkPump() {
@@ -165,6 +185,12 @@ class WryWebViewPanel(initialUrl: String) : JPanel() {
165185
createTimer = null
166186
}
167187

188+
private fun stopBoundsTimer() {
189+
boundsTimer?.stop()
190+
boundsTimer = null
191+
pendingBounds = null
192+
}
193+
168194
private fun componentHandle(component: Component): ULong {
169195
return try {
170196
Native.getComponentID(component).toULong()

wrywebview/src/main/rust/lib.rs

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ use wry::dpi::{LogicalPosition, LogicalSize};
77
use wry::raw_window_handle::{HandleError, HasWindowHandle, RawWindowHandle, WindowHandle};
88
use wry::{Rect, WebView, WebViewBuilder};
99

10+
#[cfg(target_os = "linux")]
11+
use std::sync::mpsc;
12+
#[cfg(target_os = "linux")]
13+
use std::time::Duration;
1014
#[cfg(target_os = "linux")]
1115
use std::os::raw::c_ulong;
1216
#[cfg(target_os = "linux")]
@@ -86,6 +90,73 @@ fn webviews() -> &'static Mutex<HashMap<u64, WebViewEntry>> {
8690
WEBVIEWS.get_or_init(|| Mutex::new(HashMap::new()))
8791
}
8892

93+
#[cfg(target_os = "linux")]
94+
type GtkTask = Box<dyn FnOnce() + Send + 'static>;
95+
96+
#[cfg(target_os = "linux")]
97+
struct GtkRunner {
98+
sender: mpsc::Sender<GtkTask>,
99+
init_error: Option<String>,
100+
}
101+
102+
#[cfg(target_os = "linux")]
103+
static GTK_RUNNER: OnceLock<GtkRunner> = OnceLock::new();
104+
105+
#[cfg(target_os = "linux")]
106+
fn gtk_runner() -> Result<&'static GtkRunner, WebViewError> {
107+
let runner = GTK_RUNNER.get_or_init(|| {
108+
let (task_tx, task_rx) = mpsc::channel::<GtkTask>();
109+
let (init_tx, init_rx) = mpsc::sync_channel::<Result<(), String>>(1);
110+
std::thread::spawn(move || {
111+
let init_result = gtk::init().map_err(|err| err.to_string());
112+
let _ = init_tx.send(init_result.clone());
113+
if init_result.is_err() {
114+
return;
115+
}
116+
loop {
117+
while let Ok(task) = task_rx.try_recv() {
118+
task();
119+
}
120+
while gtk::events_pending() {
121+
gtk::main_iteration_do(false);
122+
}
123+
std::thread::sleep(Duration::from_millis(8));
124+
}
125+
});
126+
let init_result = init_rx
127+
.recv()
128+
.unwrap_or_else(|_| Err("gtk init thread failed".to_string()));
129+
GtkRunner {
130+
sender: task_tx,
131+
init_error: init_result.err(),
132+
}
133+
});
134+
if let Some(err) = runner.init_error.as_ref() {
135+
return Err(WebViewError::GtkInit(err.clone()));
136+
}
137+
Ok(runner)
138+
}
139+
140+
#[cfg(target_os = "linux")]
141+
fn run_on_gtk_thread<F, R>(f: F) -> Result<R, WebViewError>
142+
where
143+
F: FnOnce() -> Result<R, WebViewError> + Send + 'static,
144+
R: Send + 'static,
145+
{
146+
let runner = gtk_runner()?;
147+
let (result_tx, result_rx) = mpsc::sync_channel(1);
148+
runner
149+
.sender
150+
.send(Box::new(move || {
151+
let result = f();
152+
let _ = result_tx.send(result);
153+
}))
154+
.map_err(|_| WebViewError::Internal("gtk runner stopped".to_string()))?;
155+
result_rx
156+
.recv()
157+
.map_err(|_| WebViewError::Internal("gtk runner stopped".to_string()))?
158+
}
159+
89160
fn with_webview<F, R>(id: u64, f: F) -> Result<R, WebViewError>
90161
where
91162
F: FnOnce(&WebView) -> Result<R, WebViewError>,
@@ -254,6 +325,10 @@ pub fn create_webview(
254325
height: i32,
255326
url: String,
256327
) -> Result<u64, WebViewError> {
328+
#[cfg(target_os = "linux")]
329+
{
330+
return run_on_gtk_thread(move || create_webview_inner(parent_handle, width, height, url));
331+
}
257332
run_on_main_thread(move || create_webview_inner(parent_handle, width, height, url))
258333
}
259334

@@ -291,7 +366,12 @@ pub fn set_bounds(
291366
return Ok(());
292367
}
293368

294-
#[cfg(not(target_os = "macos"))]
369+
#[cfg(target_os = "linux")]
370+
{
371+
return run_on_gtk_thread(move || set_bounds_inner(id, x, y, width, height));
372+
}
373+
374+
#[cfg(target_os = "windows")]
295375
{
296376
run_on_main_thread(move || set_bounds_inner(id, x, y, width, height))
297377
}
@@ -304,6 +384,10 @@ fn load_url_inner(id: u64, url: String) -> Result<(), WebViewError> {
304384

305385
#[uniffi::export]
306386
pub fn load_url(id: u64, url: String) -> Result<(), WebViewError> {
387+
#[cfg(target_os = "linux")]
388+
{
389+
return run_on_gtk_thread(move || load_url_inner(id, url));
390+
}
307391
run_on_main_thread(move || load_url_inner(id, url))
308392
}
309393

@@ -334,16 +418,18 @@ fn destroy_webview_inner(id: u64) -> Result<(), WebViewError> {
334418

335419
#[uniffi::export]
336420
pub fn destroy_webview(id: u64) -> Result<(), WebViewError> {
421+
#[cfg(target_os = "linux")]
422+
{
423+
return run_on_gtk_thread(move || destroy_webview_inner(id));
424+
}
337425
run_on_main_thread(move || destroy_webview_inner(id))
338426
}
339427

340428
#[uniffi::export]
341429
pub fn pump_gtk_events() {
342430
#[cfg(target_os = "linux")]
343431
{
344-
while gtk::events_pending() {
345-
gtk::main_iteration_do(false);
346-
}
432+
// Events are pumped continuously on the dedicated GTK thread.
347433
}
348434
}
349435

0 commit comments

Comments
 (0)