use std::{
cell::{Cell, RefCell},
ffi::c_ulong,
num::NonZeroU32,
rc::Rc,
sync::Arc,
};
use gpui::{DevicePixels, Pixels, Point, Size, point, px, size};
use gtk::{gdk, glib, prelude::*};
use raw_window_handle::{RawWindowHandle, XcbWindowHandle, XlibWindowHandle};
use tauri::Wry;
use super::dispatcher::LoopWaker;
pub(crate) fn xid(window: RawWindowHandle) -> Option<c_ulong> {
match window {
RawWindowHandle::Xlib(handle) => Some(handle.window),
RawWindowHandle::Xcb(handle) => Some(handle.window.get().into()),
_ => None,
}
}
fn with_xid(toplevel: RawWindowHandle, xid: c_ulong) -> Option<RawWindowHandle> {
Some(match toplevel {
RawWindowHandle::Xlib(handle) => {
let mut child = XlibWindowHandle::new(xid);
child.visual_id = handle.visual_id;
child.into()
}
RawWindowHandle::Xcb(handle) => {
let mut child = XcbWindowHandle::new(NonZeroU32::new(u32::try_from(xid).ok()?)?);
child.visual_id = handle.visual_id;
child.into()
}
_ => return None,
})
}
fn has_menu_bar(vbox: >k::Box) -> bool {
vbox.children()
.iter()
.any(|child| child.is::<gtk::MenuBar>())
}
pub(crate) struct X11Window {
pub toplevel: gtk::Window,
pub toplevel_xid: c_ulong,
toplevel_raw: RawWindowHandle,
vbox: gtk::Box,
added: Option<glib::SignalHandlerId>,
area: RefCell<Option<gtk::DrawingArea>>,
signals: Rc<Signals>,
}
#[derive(Default)]
struct Signals {
menu_bar_added: Cell<bool>,
menu_bar_seen: Cell<bool>,
resized: Cell<bool>,
exposed: Cell<bool>,
crossed: Cell<bool>,
}
pub(crate) struct Changes {
pub menu_bar_added: bool,
pub resized: bool,
pub exposed: bool,
pub crossed: bool,
}
pub(crate) struct Pointer {
pub over: bool,
pub position: Point<Pixels>,
}
impl X11Window {
pub fn new(
window: &tauri::Window<Wry>,
toplevel_raw: RawWindowHandle,
waker: &Arc<LoopWaker>,
) -> Option<Self> {
let toplevel_xid = xid(toplevel_raw)?;
let toplevel = window.gtk_window().ok()?.upcast::<gtk::Window>();
let vbox = window.default_vbox().ok()?;
let signals = Rc::new(Signals::default());
signals.menu_bar_added.set(has_menu_bar(&vbox));
signals.menu_bar_seen.set(signals.menu_bar_added.get());
let added = vbox.connect_size_allocate({
let signals = signals.clone();
let waker = waker.clone();
move |vbox, _| {
if !signals.menu_bar_seen.get() && has_menu_bar(vbox) {
signals.menu_bar_seen.set(true);
signals.menu_bar_added.set(true);
waker.wake();
}
}
});
Some(Self {
toplevel,
toplevel_xid,
toplevel_raw,
vbox,
added: Some(added),
area: RefCell::default(),
signals,
})
}
pub fn has_area(&self) -> bool {
self.area.borrow().is_some()
}
pub fn take_changes(&self) -> Changes {
let signals = &self.signals;
Changes {
menu_bar_added: signals.menu_bar_added.take(),
resized: signals.resized.take(),
exposed: signals.exposed.take(),
crossed: signals.crossed.take(),
}
}
pub fn create_area(&self, waker: &Arc<LoopWaker>) -> Option<RawWindowHandle> {
let area = gtk::DrawingArea::new();
area.set_app_paintable(true);
unsafe {
gtk::ffi::gtk_widget_set_double_buffered(
area.upcast_ref::<gtk::Widget>().as_ptr(),
glib::ffi::GFALSE,
)
};
area.add_events(gdk::EventMask::ENTER_NOTIFY_MASK | gdk::EventMask::LEAVE_NOTIFY_MASK);
let signal = |set: fn(&Signals)| {
let signals = self.signals.clone();
let waker = waker.clone();
move || {
set(&signals);
waker.wake();
}
};
let resized = signal(|s| s.resized.set(true));
area.connect_size_allocate(move |_, _| resized());
let exposed = signal(|s| s.exposed.set(true));
area.connect_draw(move |_, _| {
exposed();
glib::Propagation::Stop
});
let entered = signal(|s| s.crossed.set(true));
area.connect_enter_notify_event(move |_, _| {
entered();
glib::Propagation::Proceed
});
let left = signal(|s| s.crossed.set(true));
area.connect_leave_notify_event(move |_, _| {
left();
glib::Propagation::Proceed
});
self.vbox.pack_start(&area, true, true, 0);
area.show();
area.realize();
let xid = area
.window()
.filter(|window| window.ensure_native())
.and_then(|window| window.downcast::<gdkx11::X11Window>().ok())
.map(|window| window.xid())
.and_then(|xid| with_xid(self.toplevel_raw, xid));
if xid.is_none() {
log::warn!("tauri-plugin-gpui: no native X11 window below the menu bar");
self.vbox.remove(&area);
return None;
}
self.toplevel.check_resize();
self.toplevel.display().flush();
self.signals.resized.set(false);
self.signals.crossed.set(true);
*self.area.borrow_mut() = Some(area);
xid
}
pub fn remove_area(&self) {
if let Some(area) = self.area.take()
&& area.parent().as_ref() == Some(self.vbox.upcast_ref())
{
self.vbox.remove(&area);
}
}
pub fn content_size(&self, scale_factor: f32) -> Option<Size<DevicePixels>> {
let area = self.area.borrow();
let allocation = area.as_ref()?.allocation();
let physical = |logical: i32| DevicePixels((logical.max(1) as f32 * scale_factor) as i32);
Some(size(
physical(allocation.width()),
physical(allocation.height()),
))
}
pub fn chrome_height(&self) -> f32 {
self.area
.borrow()
.as_ref()
.map_or(0., |area| area.allocation().y().max(0) as f32)
}
pub fn pointer(&self) -> Option<Pointer> {
let area = self.area.borrow();
let window = area.as_ref()?.window()?;
let pointer = window.display().default_seat()?.pointer()?;
let (under, x, y) = pointer.window_at_position_double();
if under.as_ref() == Some(&window) {
return Some(Pointer {
over: true,
position: point(px(x as f32), px(y as f32)),
});
}
let (_, x, y, _) = window.device_position_double(&pointer);
Some(Pointer {
over: false,
position: point(px(x as f32), px(y as f32)),
})
}
}
impl Drop for X11Window {
fn drop(&mut self) {
if let Some(added) = self.added.take()
&& unsafe {
glib::gobject_ffi::g_signal_handler_is_connected(
self.vbox.as_ptr().cast(),
added.as_raw(),
) != 0
}
{
self.vbox.disconnect(added);
}
self.remove_area();
}
}