Files
another-boids-in-rust/vendor/x11rb/examples/simple_window_manager.rs

435 lines
15 KiB
Rust

// A very simple reparenting window manager.
// This WM does NOT follow ICCCM!
extern crate x11rb;
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashSet};
use std::process::exit;
use x11rb::connection::Connection;
use x11rb::errors::{ReplyError, ReplyOrIdError};
use x11rb::protocol::xproto::*;
use x11rb::protocol::{ErrorKind, Event};
use x11rb::{COPY_DEPTH_FROM_PARENT, CURRENT_TIME};
const TITLEBAR_HEIGHT: u16 = 20;
const DRAG_BUTTON: Button = 1;
/// The state of a single window that we manage
#[derive(Debug)]
struct WindowState {
window: Window,
frame_window: Window,
x: i16,
y: i16,
width: u16,
}
impl WindowState {
fn new(window: Window, frame_window: Window, geom: &GetGeometryReply) -> WindowState {
WindowState {
window,
frame_window,
x: geom.x,
y: geom.y,
width: geom.width,
}
}
fn close_x_position(&self) -> i16 {
std::cmp::max(0, self.width - TITLEBAR_HEIGHT) as _
}
}
/// The state of the full WM
#[derive(Debug)]
struct WmState<'a, C: Connection> {
conn: &'a C,
screen_num: usize,
black_gc: Gcontext,
windows: Vec<WindowState>,
pending_expose: HashSet<Window>,
wm_protocols: Atom,
wm_delete_window: Atom,
sequences_to_ignore: BinaryHeap<Reverse<u16>>,
// If this is Some, we are currently dragging the given window with the given offset relative
// to the mouse.
drag_window: Option<(Window, (i16, i16))>,
}
impl<'a, C: Connection> WmState<'a, C> {
fn new(conn: &'a C, screen_num: usize) -> Result<WmState<'a, C>, ReplyOrIdError> {
let screen = &conn.setup().roots[screen_num];
let black_gc = conn.generate_id()?;
let font = conn.generate_id()?;
conn.open_font(font, b"9x15")?;
let gc_aux = CreateGCAux::new()
.graphics_exposures(0)
.background(screen.white_pixel)
.foreground(screen.black_pixel)
.font(font);
conn.create_gc(black_gc, screen.root, &gc_aux)?;
conn.close_font(font)?;
let wm_protocols = conn.intern_atom(false, b"WM_PROTOCOLS")?;
let wm_delete_window = conn.intern_atom(false, b"WM_DELETE_WINDOW")?;
Ok(WmState {
conn,
screen_num,
black_gc,
windows: Vec::default(),
pending_expose: HashSet::default(),
wm_protocols: wm_protocols.reply()?.atom,
wm_delete_window: wm_delete_window.reply()?.atom,
sequences_to_ignore: Default::default(),
drag_window: None,
})
}
/// Scan for already existing windows and manage them
fn scan_windows(&mut self) -> Result<(), ReplyOrIdError> {
// Get the already existing top-level windows.
let screen = &self.conn.setup().roots[self.screen_num];
let tree_reply = self.conn.query_tree(screen.root)?.reply()?;
// For each window, request its attributes and geometry *now*
let mut cookies = Vec::with_capacity(tree_reply.children.len());
for win in tree_reply.children {
let attr = self.conn.get_window_attributes(win)?;
let geom = self.conn.get_geometry(win)?;
cookies.push((win, attr, geom));
}
// Get the replies and manage windows
for (win, attr, geom) in cookies {
if let (Ok(attr), Ok(geom)) = (attr.reply(), geom.reply()) {
if !attr.override_redirect && attr.map_state != MapState::UNMAPPED {
self.manage_window(win, &geom)?;
}
} else {
// Just skip this window
}
}
Ok(())
}
/// Add a new window that should be managed by the WM
fn manage_window(
&mut self,
win: Window,
geom: &GetGeometryReply,
) -> Result<(), ReplyOrIdError> {
println!("Managing window {win:?}");
let screen = &self.conn.setup().roots[self.screen_num];
assert!(self.find_window_by_id(win).is_none());
let frame_win = self.conn.generate_id()?;
let win_aux = CreateWindowAux::new()
.event_mask(
EventMask::EXPOSURE
| EventMask::SUBSTRUCTURE_NOTIFY
| EventMask::BUTTON_PRESS
| EventMask::BUTTON_RELEASE
| EventMask::POINTER_MOTION
| EventMask::ENTER_WINDOW,
)
.background_pixel(screen.white_pixel);
self.conn.create_window(
COPY_DEPTH_FROM_PARENT,
frame_win,
screen.root,
geom.x,
geom.y,
geom.width,
geom.height + TITLEBAR_HEIGHT,
1,
WindowClass::INPUT_OUTPUT,
0,
&win_aux,
)?;
self.conn.grab_server()?;
self.conn.change_save_set(SetMode::INSERT, win)?;
let cookie = self
.conn
.reparent_window(win, frame_win, 0, TITLEBAR_HEIGHT as _)?;
self.conn.map_window(win)?;
self.conn.map_window(frame_win)?;
self.conn.ungrab_server()?;
self.windows.push(WindowState::new(win, frame_win, geom));
// Ignore all events caused by reparent_window(). All those events have the sequence number
// of the reparent_window() request, thus remember its sequence number. The
// grab_server()/ungrab_server() is done so that the server does not handle other clients
// in-between, which could cause other events to get the same sequence number.
self.sequences_to_ignore
.push(Reverse(cookie.sequence_number() as u16));
Ok(())
}
/// Draw the titlebar of a window
fn redraw_titlebar(&self, state: &WindowState) -> Result<(), ReplyError> {
let close_x = state.close_x_position();
self.conn.poly_line(
CoordMode::ORIGIN,
state.frame_window,
self.black_gc,
&[
Point { x: close_x, y: 0 },
Point {
x: state.width as _,
y: TITLEBAR_HEIGHT as _,
},
],
)?;
self.conn.poly_line(
CoordMode::ORIGIN,
state.frame_window,
self.black_gc,
&[
Point {
x: close_x,
y: TITLEBAR_HEIGHT as _,
},
Point {
x: state.width as _,
y: 0,
},
],
)?;
let reply = self
.conn
.get_property(
false,
state.window,
AtomEnum::WM_NAME,
AtomEnum::STRING,
0,
u32::MAX,
)?
.reply()?;
self.conn
.image_text8(state.frame_window, self.black_gc, 1, 10, &reply.value)?;
Ok(())
}
/// Do all pending work that was queued while handling some events
fn refresh(&mut self) {
while let Some(&win) = self.pending_expose.iter().next() {
self.pending_expose.remove(&win);
if let Some(state) = self.find_window_by_id(win) {
if let Err(err) = self.redraw_titlebar(state) {
eprintln!(
"Error while redrawing window {:x?}: {:?}",
state.window, err
);
}
}
}
}
fn find_window_by_id(&self, win: Window) -> Option<&WindowState> {
self.windows
.iter()
.find(|state| state.window == win || state.frame_window == win)
}
fn find_window_by_id_mut(&mut self, win: Window) -> Option<&mut WindowState> {
self.windows
.iter_mut()
.find(|state| state.window == win || state.frame_window == win)
}
/// Handle the given event
fn handle_event(&mut self, event: Event) -> Result<(), ReplyOrIdError> {
let mut should_ignore = false;
if let Some(seqno) = event.wire_sequence_number() {
// Check sequences_to_ignore and remove entries with old (=smaller) numbers.
while let Some(&Reverse(to_ignore)) = self.sequences_to_ignore.peek() {
// Sequence numbers can wrap around, so we cannot simply check for
// "to_ignore <= seqno". This is equivalent to "to_ignore - seqno <= 0", which is what we
// check instead. Since sequence numbers are unsigned, we need a trick: We decide
// that values from [MAX/2, MAX] count as "<= 0" and the rest doesn't.
if to_ignore.wrapping_sub(seqno) <= u16::MAX / 2 {
// If the two sequence numbers are equal, this event should be ignored.
should_ignore = to_ignore == seqno;
break;
}
self.sequences_to_ignore.pop();
}
}
println!("Got event {event:?}");
if should_ignore {
println!(" [ignored]");
return Ok(());
}
match event {
Event::UnmapNotify(event) => self.handle_unmap_notify(event),
Event::ConfigureRequest(event) => self.handle_configure_request(event)?,
Event::MapRequest(event) => self.handle_map_request(event)?,
Event::Expose(event) => self.handle_expose(event),
Event::EnterNotify(event) => self.handle_enter(event)?,
Event::ButtonPress(event) => self.handle_button_press(event),
Event::ButtonRelease(event) => self.handle_button_release(event)?,
Event::MotionNotify(event) => self.handle_motion_notify(event)?,
_ => {}
}
Ok(())
}
fn handle_unmap_notify(&mut self, event: UnmapNotifyEvent) {
let root = self.conn.setup().roots[self.screen_num].root;
let conn = self.conn;
self.windows.retain(|state| {
if state.window != event.window {
return true;
}
conn.change_save_set(SetMode::DELETE, state.window).unwrap();
conn.reparent_window(state.window, root, state.x, state.y)
.unwrap();
conn.destroy_window(state.frame_window).unwrap();
false
});
}
fn handle_configure_request(&mut self, event: ConfigureRequestEvent) -> Result<(), ReplyError> {
if let Some(state) = self.find_window_by_id_mut(event.window) {
let _ = state;
unimplemented!();
}
// Allow clients to change everything, except sibling / stack mode
let aux = ConfigureWindowAux::from_configure_request(&event)
.sibling(None)
.stack_mode(None);
println!("Configure: {aux:?}");
self.conn.configure_window(event.window, &aux)?;
Ok(())
}
fn handle_map_request(&mut self, event: MapRequestEvent) -> Result<(), ReplyOrIdError> {
self.manage_window(
event.window,
&self.conn.get_geometry(event.window)?.reply()?,
)
}
fn handle_expose(&mut self, event: ExposeEvent) {
self.pending_expose.insert(event.window);
}
fn handle_enter(&mut self, event: EnterNotifyEvent) -> Result<(), ReplyError> {
if let Some(state) = self.find_window_by_id(event.event) {
// Set the input focus (ignoring ICCCM's WM_PROTOCOLS / WM_TAKE_FOCUS)
self.conn
.set_input_focus(InputFocus::PARENT, state.window, CURRENT_TIME)?;
// Also raise the window to the top of the stacking order
self.conn.configure_window(
state.frame_window,
&ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
)?;
}
Ok(())
}
fn handle_button_press(&mut self, event: ButtonPressEvent) {
if event.detail != DRAG_BUTTON || u16::from(event.state) != 0 {
return;
}
if let Some(state) = self.find_window_by_id(event.event) {
if self.drag_window.is_none() && event.event_x < state.close_x_position() {
let (x, y) = (-event.event_x, -event.event_y);
self.drag_window = Some((state.frame_window, (x, y)));
}
}
}
fn handle_button_release(&mut self, event: ButtonReleaseEvent) -> Result<(), ReplyError> {
if event.detail == DRAG_BUTTON {
self.drag_window = None;
}
if let Some(state) = self.find_window_by_id(event.event) {
if event.event_x >= state.close_x_position() {
let event = ClientMessageEvent::new(
32,
state.window,
self.wm_protocols,
[self.wm_delete_window, 0, 0, 0, 0],
);
self.conn
.send_event(false, state.window, EventMask::NO_EVENT, event)?;
}
}
Ok(())
}
fn handle_motion_notify(&mut self, event: MotionNotifyEvent) -> Result<(), ReplyError> {
if let Some((win, (x, y))) = self.drag_window {
let (x, y) = (x + event.root_x, y + event.root_y);
// Sigh, X11 and its mixing up i16 and i32
let (x, y) = (x as i32, y as i32);
self.conn
.configure_window(win, &ConfigureWindowAux::new().x(x).y(y))?;
}
Ok(())
}
}
fn become_wm<C: Connection>(conn: &C, screen: &Screen) -> Result<(), ReplyError> {
// Try to become the window manager. This causes an error if there is already another WM.
let change = ChangeWindowAttributesAux::default()
.event_mask(EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY);
let res = conn.change_window_attributes(screen.root, &change)?.check();
if let Err(ReplyError::X11Error(ref error)) = res {
if error.error_kind == ErrorKind::Access {
eprintln!("Another WM is already running.");
exit(1);
} else {
res
}
} else {
res
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let (conn, screen_num) = connect(None)?;
// The following is only needed for start_timeout_thread(), which is used for 'tests'
let conn1 = std::sync::Arc::new(conn);
let conn = &*conn1;
let screen = &conn.setup().roots[screen_num];
become_wm(conn, screen)?;
let mut wm_state = WmState::new(conn, screen_num)?;
wm_state.scan_windows()?;
util::start_timeout_thread(conn1.clone(), screen.root);
loop {
wm_state.refresh();
conn.flush()?;
let event = conn.wait_for_event()?;
let mut event_option = Some(event);
while let Some(event) = event_option {
if let Event::ClientMessage(_) = event {
// This is start_timeout_thread() signaling us to close (most likely).
return Ok(());
}
wm_state.handle_event(event)?;
event_option = conn.poll_for_event()?;
}
}
}
include!("integration_test_util/connect.rs");
include!("integration_test_util/util.rs");