From 5c63de0a7bb2954709cdb2e9b0058bdc352aee9e Mon Sep 17 00:00:00 2001 From: isark Date: Sat, 8 Jul 2023 22:30:04 +0200 Subject: [PATCH] X11 implementation --- .gitignore | 1 + Cargo.toml | 10 +- src/examples/main.rs | 7 +- src/lib.rs | 7 +- src/linux/mod.rs | 357 ++++++++++++++++++++++++++++++++++++++++++- src/windows/mod.rs | 4 +- 6 files changed, 375 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 4fffb2f..5b70a89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +.vscode diff --git a/Cargo.toml b/Cargo.toml index 08bef83..a477123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -name = "Underlayer" +name = "underlayer" path = "src/lib.rs" [[example]] @@ -17,8 +17,14 @@ path = "src/examples/main.rs" crossbeam = "0.8.2" log = "0.4.19" raw-window-handle = "0.5.2" +x11rb = "0.12.0" + +[target.'cfg(windows)'.dependencies] windows = {version = "0.48.0", features = ["Win32_UI_Accessibility", "Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_System_Com", "Win32_System_Ole"]} +[target.'cfg(unix)'.dependencies] + + + [dev-dependencies] -ctor = "0.2.3" simple_logger = "4.2.0" diff --git a/src/examples/main.rs b/src/examples/main.rs index 599e3e6..4fddb2c 100644 --- a/src/examples/main.rs +++ b/src/examples/main.rs @@ -1,4 +1,4 @@ -use Underlayer::{UnderlayEvent, register}; +use underlayer::{register, UnderlayEvent}; fn init() { simple_logger::init().expect("Could not initialize simple_logger"); @@ -6,9 +6,8 @@ fn init() { fn main() { init(); - let rx: std::sync::mpsc::Receiver = register("Untitled - Notepad"); + let rx: std::sync::mpsc::Receiver = register("Untitled 1 - Mousepad"); - let mut value = rx.recv(); while let Ok(event) = value { log::info!("Got event: {event:?}"); @@ -16,4 +15,4 @@ fn main() { } log::error!("{value:?}"); -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 9e93569..a134e14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,11 +16,16 @@ pub struct Bounds { #[derive(Debug, Clone, Copy)] pub enum UnderlayEvent { - Attach { bounds: Bounds, has_access: bool }, + Attach { + bounds: Bounds, + has_access: bool, + is_fullscreen: Option, + }, Detach, MoveResize(Bounds), Focus, Blur, + X11FullscreenEvent{ is_fullscreen: bool } } pub fn register>(window_title: T) -> Receiver { diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 37d6580..9962d2b 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -1,6 +1,359 @@ +use x11rb::{ + connect, + connection::Connection, + protocol::{ + xproto::{Atom, AtomEnum, ChangeWindowAttributesAux, ConnectionExt, EventMask, Window}, + Event, + }, + rust_connection::RustConnection, +}; +use crate::{Bounds, UnderlayEvent}; -pub fn init(window_title: String, tx: std::sync::mpsc::Sender) -> Result<(), String> { +const WM_NAME_BUFSIZE: u32 = 512; + +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 { + Some(Self { + net_active_window: Self::_get(conn, "_NET_ACTIVE_WINDOW")?, + net_wm_name: Self::_get(conn, "_NET_WM_NAME")?, + utf8_string: Self::_get(conn, "UTF8_STRING")?, + net_wm_state: Self::_get(conn, "_NET_WM_STATE")?, + net_wm_state_fullscreen: Self::_get(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>(conn: &RustConnection, name: T) -> Option { + Some( + conn.intern_atom(false, name.into().as_bytes()) + .ok()? + .reply() + .ok()? + .atom, + ) + } +} + +struct Runtime { + conn: RustConnection, + root: Window, + target: Target, + atoms: AtomCollection, + underlay_tx: std::sync::mpsc::Sender, + active: Window, +} + +impl Runtime { + fn get_active_window(&self) -> Option { + let cookie = self.conn + .get_property( + false, + self.root, + self.atoms.net_active_window, + AtomEnum::WINDOW, + 0, + 1, + ) + .ok()?; + + let reply = cookie.reply().ok()?; - Err(format!("Failed init underlayer for window: {}", window_title)) + let mut value32_iter = reply.value32()?; + + value32_iter.next() + } + + fn get_title(&self, wid: Window) -> Option { + 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 { + 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 { + 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.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.target.wid = 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.target.wid = 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: true, + 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.target.wid = 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, +) -> 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(); + + 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(()) } diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 446010e..5845332 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -373,7 +373,7 @@ impl Runtime { if let Some(bounds) = self.get_content_bounds() { self.underlay_tx - .send(UnderlayEvent::Attach { bounds, has_access }) + .send(UnderlayEvent::Attach { bounds, has_access, is_fullscreen: None }) .ok(); self.target.is_focused = true; @@ -522,4 +522,4 @@ fn register_message>(message: T) -> u32 { let message_wide = windows::core::HSTRING::from(message.into()); let result = unsafe { RegisterWindowMessageW(PCWSTR(message_wide.as_wide().as_ptr())) }; result -} +} \ No newline at end of file