You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

428 lines
13 KiB

use std::sync::{atomic::AtomicPtr, Arc, Mutex};
use x11rb::{
connect,
connection::Connection,
protocol::{
xproto::{
self, Atom, AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent,
ConnectionExt, EventMask, Window,
},
Event,
},
rust_connection::RustConnection,
};
use crate::{Bounds, UnderlayEvent};
const WM_NAME_BUFSIZE: u32 = 512;
static TARGET: AtomicPtr<Arc<Mutex<Window>>> = AtomicPtr::new(std::ptr::null_mut());
struct Target {
title: String,
wid: Window,
is_focused: bool,
is_destroyed: bool,
is_fullscreen: bool,
}
struct AtomCollection {
net_active_window: Atom,
net_wm_name: Atom,
utf8_string: Atom,
net_wm_state: Atom,
net_wm_state_fullscreen: Atom,
// net_wm_state_skip_taskbar: Atom,
// net_wm_state_skip_pager: Atom,
}
impl AtomCollection {
fn new(conn: &RustConnection) -> Option<Self> {
Some(Self {
net_active_window: get_atom(conn, "_NET_ACTIVE_WINDOW")?,
net_wm_name: get_atom(conn, "_NET_WM_NAME")?,
utf8_string: get_atom(conn, "UTF8_STRING")?,
net_wm_state: get_atom(conn, "_NET_WM_STATE")?,
net_wm_state_fullscreen: get_atom(conn, "_NET_WM_STATE_FULLSCREEN")?,
// net_wm_state_skip_taskbar: Self::_get(conn, "_NET_WM_STATE_SKIP_TASKBAR")?,
// net_wm_state_skip_pager: Self::_get(conn, "_NET_WM_STATE_SKIP_PAGER")?,
})
}
}
fn get_atom<T: Into<String>>(conn: &RustConnection, name: T) -> Option<Atom> {
Some(
conn.intern_atom(false, name.into().as_bytes())
.ok()?
.reply()
.ok()?
.atom,
)
}
//Not very pretty and a bit wasteful making a new connection and getting the atom every time..
pub fn focus_target() {
unsafe {
if let Ok(target) = (*TARGET.load(std::sync::atomic::Ordering::SeqCst)).lock() {
if *target != x11rb::NONE {
if let Ok((conn, _screen_num)) = connect(None) {
let net_active_window = get_atom(&conn, "_NET_ACTIVE_WINDOW").unwrap();
let event = ClientMessageEvent {
response_type: xproto::CLIENT_MESSAGE_EVENT,
format: 32,
sequence: 0,
window: *target,
type_: net_active_window,
data: ClientMessageData::from([1, x11rb::CURRENT_TIME, 0, 0, 0]),
};
if let Ok(cookie) = conn.send_event(
false,
conn.setup().roots[_screen_num].root,
EventMask::SUBSTRUCTURE_NOTIFY | EventMask::SUBSTRUCTURE_REDIRECT,
event,
) {
if let Err(e) = cookie.check() {
log::error!("Error focusing target: {e:?}");
}
}
conn.flush().ok();
}
}
}
}
}
struct Runtime {
conn: RustConnection,
root: Window,
target: Target,
atoms: AtomCollection,
underlay_tx: std::sync::mpsc::Sender<UnderlayEvent>,
active: Window,
}
impl Runtime {
fn get_active_window(&self) -> Option<Window> {
let cookie = self
.conn
.get_property(
false,
self.root,
self.atoms.net_active_window,
AtomEnum::WINDOW,
0,
1,
)
.ok()?;
let reply = cookie.reply().ok()?;
let mut value32_iter = reply.value32()?;
value32_iter.next()
}
///Using this instead to allow setting the globally accessible target too for focus_target.
fn set_target(&mut self, wid: Window) {
self.target.wid = wid;
unsafe {
if let Ok(mut target) = (*TARGET.load(std::sync::atomic::Ordering::Relaxed)).lock() {
*target = wid;
}
}
}
fn get_title(&self, wid: Window) -> Option<String> {
if wid == x11rb::NONE {
return None;
}
let cookie = self
.conn
.get_property(
false,
wid,
self.atoms.net_wm_name,
self.atoms.utf8_string,
0,
WM_NAME_BUFSIZE,
)
.ok()?;
let reply = cookie.reply().ok()?;
let data = reply.value8()?.collect();
String::from_utf8(data).ok()
}
fn get_content_bounds(&self, wid: Window) -> Option<Bounds> {
let geometry_cookie = self.conn.get_geometry(wid).ok()?;
let geometry_reply = geometry_cookie.reply().ok()?;
let translation_cookie = self.conn.translate_coordinates(wid, self.root, 0, 0).ok()?;
let translation_reply = translation_cookie.reply().ok()?;
Some(Bounds {
x: translation_reply.dst_x as i32,
y: translation_reply.dst_y as i32,
width: geometry_reply.width as i32,
height: geometry_reply.height as i32,
})
}
fn is_fullscreen_window(&self, wid: Window) -> Option<bool> {
let cookie = self
.conn
.get_property(false, wid, self.atoms.net_wm_state, AtomEnum::ATOM, 0, 0)
.ok()?;
let reply = cookie.reply().ok()?;
let len = reply.value32()?.count();
let cookie = self
.conn
.get_property(
false,
wid,
self.atoms.net_wm_state,
AtomEnum::ATOM,
0,
len as u32,
)
.ok()?;
let reply = cookie.reply().ok()?;
let mut is_fullscreen = false;
for state in reply.value32()? {
is_fullscreen = state == self.atoms.net_wm_state_fullscreen;
}
Some(is_fullscreen)
}
fn handle_moveresize_xevent(&self) {
if let Some(bounds) = self.get_content_bounds(self.target.wid) {
self.underlay_tx
.send(UnderlayEvent::MoveResize(bounds))
.ok();
}
}
fn handle_fullscreen_xevent(&mut self) {
if let Some(is_fullscreen) = self.is_fullscreen_window(self.target.wid) {
if self.target.is_fullscreen != is_fullscreen {
self.target.is_fullscreen = is_fullscreen;
self.underlay_tx
.send(UnderlayEvent::X11FullscreenEvent { is_fullscreen })
.ok();
}
}
}
fn check_and_handle_window(&mut self, wid: Window) {
if self.target.wid != x11rb::NONE {
if self.target.wid == wid {
if !self.target.is_focused {
self.target.is_focused = true;
self.underlay_tx.send(UnderlayEvent::Focus).ok();
}
return;
}
if self.target.is_focused {
self.target.is_focused = false;
self.underlay_tx.send(UnderlayEvent::Blur).ok();
}
if self.target.is_destroyed {
self.set_target(x11rb::NONE);
self.target.is_destroyed = false;
self.underlay_tx.send(UnderlayEvent::Detach).ok();
}
}
log::trace!(
"got title: {}",
self.get_title(wid).unwrap_or("ERROR".into())
);
match self.get_title(wid) {
Some(title) => {
if title != self.target.title {
return;
}
}
_ => return,
}
log::trace!("Found target!");
if self.target.wid != x11rb::NONE {
self.conn
.change_window_attributes(
self.target.wid,
&ChangeWindowAttributesAux::new().event_mask(EventMask::NO_EVENT),
)
.ok();
}
self.set_target(wid);
self.conn
.change_window_attributes(
wid,
&ChangeWindowAttributesAux::new()
.event_mask(EventMask::PROPERTY_CHANGE | EventMask::STRUCTURE_NOTIFY),
)
.ok();
if let Some(is_fullscreen) = self.is_fullscreen_window(self.target.wid) {
if let Some(bounds) = self.get_content_bounds(self.target.wid) {
if is_fullscreen != self.target.is_fullscreen {
self.target.is_fullscreen = is_fullscreen;
}
self.underlay_tx
.send(UnderlayEvent::Attach {
bounds,
has_access: None,
is_fullscreen: Some(is_fullscreen),
})
.ok();
self.target.is_focused = true;
self.underlay_tx.send(UnderlayEvent::Focus).ok();
//Early return to not unset target wid
return;
}
}
self.set_target(x11rb::NONE);
}
fn handle_event(&mut self, event: Event) {
match event {
Event::DestroyNotify(event) => {
log::trace!("Got DestroyNotify: {event:?}");
if event.window == self.target.wid {
self.target.is_destroyed = true;
self.check_and_handle_window(x11rb::NONE);
}
}
Event::ConfigureNotify(event) => {
log::trace!("Got ConfigureNotify: {event:?}");
if event.window == self.target.wid {
self.handle_moveresize_xevent();
}
}
Event::PropertyNotify(event) => {
log::trace!("Got PropertyNotify: {event:?}");
if event.window == self.root && event.atom == self.atoms.net_active_window {
let old = self.active;
self.active = self.get_active_window().unwrap_or(x11rb::NONE);
if old != self.target.wid {
self.conn
.change_window_attributes(
old,
&ChangeWindowAttributesAux::new().event_mask(EventMask::NO_EVENT),
)
.ok();
}
if self.active != x11rb::NONE && self.active != self.target.wid {
self.conn
.change_window_attributes(
self.active,
&ChangeWindowAttributesAux::new()
.event_mask(EventMask::PROPERTY_CHANGE),
)
.ok();
}
self.check_and_handle_window(self.active);
} else if event.window == self.target.wid && event.atom == self.atoms.net_wm_state {
self.handle_fullscreen_xevent();
} else if event.window == self.active && event.atom == self.atoms.net_wm_name {
self.check_and_handle_window(self.active);
}
}
_ => (),
}
}
}
pub fn init(
window_title: String,
tx: std::sync::mpsc::Sender<crate::UnderlayEvent>,
) -> Result<(), String> {
std::thread::spawn(move || {
let (conn, screen_num) = match connect(None) {
Ok(conn) => conn,
Err(_) => {
log::error!("Could not connect");
return;
}
};
let root = conn.setup().roots[screen_num].root;
let atoms = match AtomCollection::new(&conn) {
Some(atoms) => atoms,
None => {
log::error!("Could not initialize atoms");
return;
}
};
conn.change_window_attributes(
root,
&ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE),
)
.ok();
let mut runtime = Runtime {
conn,
root,
target: Target {
title: window_title,
wid: x11rb::NONE,
is_focused: false,
is_destroyed: false,
is_fullscreen: false,
},
atoms,
underlay_tx: tx,
active: x11rb::NONE,
};
runtime.active = runtime.get_active_window().unwrap_or(x11rb::NONE);
if runtime.active != x11rb::NONE {
runtime
.conn
.change_window_attributes(
runtime.active,
&ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE),
)
.ok();
runtime.check_and_handle_window(runtime.active);
}
runtime.conn.flush().ok();
TARGET.store(
Box::into_raw(Box::new(Arc::new(Mutex::new(x11rb::NONE)))),
std::sync::atomic::Ordering::Relaxed,
);
log::trace!("Initialized, listning for events");
while let Ok(event) = runtime.conn.wait_for_event() {
log::trace!("Got event");
runtime.handle_event(event);
runtime.conn.flush().ok();
}
});
Ok(())
}