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
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(())
|
|
}
|