use std::{
cell::{Cell, RefCell},
collections::HashMap,
ffi::c_ulong,
rc::Rc,
};
use gdkx11::{X11Display, ffi::gdk_x11_window_foreign_new_for_display};
use gpui::{Bounds, Pixels};
use gtk::{
glib::translate::{FromGlibPtrFull as _, ToGlibPtr as _, from_glib_none},
prelude::*,
};
use tauri::{Webview, Wry};
use x11_dl::xlib::{CurrentTime, Display, RevertToParent, Xlib};
struct Host {
xlib: Xlib,
gdk_display: X11Display,
display: *mut Display,
window: c_ulong,
parent: c_ulong,
focus: c_ulong,
gtk_window: gtk::Window,
widget: gtk::Widget,
shown: Cell<Option<Bounds<Pixels>>>,
focused: Rc<Cell<bool>>,
alive: Rc<Cell<bool>>,
}
impl Drop for Host {
fn drop(&mut self) {
self.alive.set(false);
if self.widget.parent().as_ref() == Some(self.gtk_window.upcast_ref()) {
self.gtk_window.remove(&self.widget);
}
self.gdk_display.error_trap_push();
self.release_focus();
unsafe { self.gtk_window.destroy() };
unsafe { (self.xlib.XDestroyWindow)(self.display, self.window) };
self.gdk_display.error_trap_pop_ignored();
}
}
thread_local! {
static HOSTS: RefCell<HashMap<String, Host>> = RefCell::default();
}
pub(super) fn focus_parent(label: &str) {
HOSTS.with(|hosts| {
if let Some(host) = hosts.borrow().get(label) {
host.return_focus();
host.flush();
}
});
}
pub(super) fn focus(label: &str) -> bool {
HOSTS.with(|hosts| {
let hosts = hosts.borrow();
let Some(host) = hosts.get(label).filter(|host| host.shown.get().is_some()) else {
return false;
};
host.take_focus();
true
})
}
pub(super) fn forget(label: &str) {
let _ = HOSTS.try_with(|hosts| hosts.borrow_mut().remove(label));
}
pub(super) fn apply(
webview: &Webview<Wry>,
parent: c_ulong,
toplevel: Option<c_ulong>,
painted: Option<Bounds<Pixels>>,
) {
let label = webview.label().to_string();
let result = webview.with_webview(move |platform| {
let widget = platform.inner().upcast::<gtk::Widget>();
HOSTS.with(|hosts| {
let mut hosts = hosts.borrow_mut();
if hosts
.get(&label)
.is_some_and(|host| host.widget != widget || host.parent != parent)
{
hosts.remove(&label);
}
if !hosts.contains_key(&label) {
match Host::new(parent, toplevel.unwrap_or(parent), &widget) {
Some(host) => {
hosts.insert(label.clone(), host);
}
None => return,
}
}
let host = &hosts[&label];
match painted {
Some(bounds) => host.show(bounds),
None => host.hide(),
}
});
});
if let Err(error) = result {
log::warn!("failed to update webview: {error}");
}
}
impl Host {
fn new(parent: c_ulong, focus: c_ulong, widget: >k::Widget) -> Option<Self> {
let xlib = Xlib::open()
.inspect_err(|error| log::warn!("embedded webview: cannot load Xlib: {error}"))
.ok()?;
let gdk_display = gtk::gdk::Display::default()?;
let Some(x11_display) = gdk_display.downcast_ref::<X11Display>() else {
log::warn!("embedded webview: GTK is not using its X11 backend");
return None;
};
let display = unsafe { gdkx11::ffi::gdk_x11_display_get_xdisplay(x11_display.as_ptr()) }
as *mut Display;
let window = unsafe { (xlib.XCreateSimpleWindow)(display, parent, 0, 0, 1, 1, 0, 0, 0) };
let gdk_window = unsafe {
gtk::gdk::Window::from_glib_full(gdk_x11_window_foreign_new_for_display(
x11_display.as_ptr() as *mut _,
window,
))
};
let gtk_window = gtk::Window::new(gtk::WindowType::Toplevel);
gtk_window.connect_realize(move |w| {
w.set_window(gdk_window.clone());
w.register_window(&gdk_window);
});
gtk_window.set_has_window(true);
gtk_window.realize();
if let Some(container) = widget
.parent()
.and_then(|parent| parent.downcast::<gtk::Container>().ok())
{
container.remove(widget);
}
gtk_window.add(widget);
let toplevel = gtk_window_for(x11_display, focus);
if let Some(toplevel) = &toplevel {
share_accel_groups(toplevel, >k_window);
}
let set_input_focus = xlib.XSetInputFocus;
let focused = Rc::new(Cell::new(false));
let alive = Rc::new(Cell::new(true));
widget.connect_button_press_event({
let (focused, alive) = (focused.clone(), alive.clone());
let toplevel = toplevel.as_ref().map(|toplevel| toplevel.downgrade());
let host = gtk_window.downgrade();
move |_, _| {
if alive.get() {
focused.set(true);
if let (Some(toplevel), Some(host)) =
(toplevel.as_ref().and_then(|t| t.upgrade()), host.upgrade())
{
share_accel_groups(&toplevel, &host);
}
unsafe { set_input_focus(display, window, RevertToParent, CurrentTime) };
}
gtk::glib::Propagation::Proceed
}
});
Some(Self {
xlib,
gdk_display: x11_display.clone(),
display,
window,
parent,
focus,
gtk_window,
widget: widget.clone(),
shown: Cell::new(None),
focused,
alive,
})
}
fn show(&self, bounds: Bounds<Pixels>) {
let was_shown = self.shown.replace(Some(bounds)).is_some();
let (x, y) = (f32::from(bounds.origin.x), f32::from(bounds.origin.y));
let (width, height) = (f32::from(bounds.size.width), f32::from(bounds.size.height));
let (width, height) = (
(width.round() as i32).max(1),
(height.round() as i32).max(1),
);
if !was_shown {
self.widget.show();
self.gtk_window.show_all();
}
if let Some(window) = self.gtk_window.window() {
window.move_resize(x.round() as i32, y.round() as i32, width, height);
}
self.gtk_window
.size_allocate(>k::Allocation::new(0, 0, width, height));
if !was_shown {
unsafe { (self.xlib.XMapWindow)(self.display, self.window) };
}
self.flush();
}
fn hide(&self) {
if self.shown.take().is_none() {
return;
}
self.release_focus();
unsafe { (self.xlib.XUnmapWindow)(self.display, self.window) };
self.flush();
}
fn release_focus(&self) {
let mut window = 0;
let mut revert = 0;
unsafe { (self.xlib.XGetInputFocus)(self.display, &mut window, &mut revert) };
self.focused.set(window == self.window);
self.return_focus();
}
fn take_focus(&self) {
self.focused.set(true);
if let Some(toplevel) = gtk_window_for(&self.gdk_display, self.focus) {
share_accel_groups(&toplevel, &self.gtk_window);
}
if let Some(window) = self.gtk_window.window() {
window.set_events(
window.events()
| gtk::gdk::EventMask::KEY_PRESS_MASK
| gtk::gdk::EventMask::KEY_RELEASE_MASK
| gtk::gdk::EventMask::FOCUS_CHANGE_MASK,
);
}
unsafe {
(self.xlib.XSetInputFocus)(self.display, self.window, RevertToParent, CurrentTime)
};
send_focus_in(&self.gtk_window);
self.widget.grab_focus();
self.flush();
}
fn return_focus(&self) {
if self.focused.replace(false) {
unsafe {
(self.xlib.XSetInputFocus)(self.display, self.focus, RevertToParent, CurrentTime)
};
}
}
fn flush(&self) {
unsafe { (self.xlib.XFlush)(self.display) };
}
}
fn send_focus_in(window: >k::Window) {
let Some(gdk_window) = window.window() else {
return;
};
let mut event = gtk::gdk::Event::new(gtk::gdk::EventType::FocusChange);
unsafe {
let raw =
gtk::glib::translate::ToGlibPtrMut::<*mut gtk::gdk::ffi::GdkEvent>::to_glib_none_mut(
&mut event,
)
.0 as *mut gtk::gdk::ffi::GdkEventFocus;
(*raw).in_ = 1;
(*raw).window = gtk::glib::translate::IntoGlibPtr::into_glib_ptr(gdk_window);
}
window.send_focus_change(&event);
}
fn gtk_window_for(display: &X11Display, xid: c_ulong) -> Option<gtk::Window> {
let window = gdkx11::X11Window::lookup_for_display(display, xid)?;
let mut widget = std::ptr::null_mut();
unsafe {
gtk::gdk::ffi::gdk_window_get_user_data(
window.upcast_ref::<gtk::gdk::Window>().to_glib_none().0,
&mut widget,
)
};
if widget.is_null() {
return None;
}
let widget: gtk::Widget = unsafe { from_glib_none(widget.cast::<gtk::ffi::GtkWidget>()) };
widget.downcast().ok()
}
fn share_accel_groups(toplevel: >k::Window, host: >k::Window) {
let wanted = gtk::accel_groups_from_object(toplevel);
let attached = gtk::accel_groups_from_object(host);
for group in attached.iter().filter(|group| !wanted.contains(group)) {
host.remove_accel_group(group);
}
for group in wanted.iter().filter(|group| !attached.contains(group)) {
host.add_accel_group(group);
}
}