psychon / x11rb

X11 bindings for the rust programming language, similar to xcb being the X11 C bindings
Apache License 2.0
372 stars 41 forks source link

Add an xshowdamage example #26

Open psychon opened 5 years ago

psychon commented 5 years ago

Similar to #25, but with less code to worry about: https://gitlab.freedesktop.org/xorg/app/xshowdamage/blob/master/xshowdamage.c

This also needs something like poll or select (which the libc crate provides).

psychon commented 2 years ago

So, I did this and noticed that the resulting program doesn't work properly. I came up with a hack to make it do things (ignore_override_redirect), but was worried that I might be doing something wrong in my port of findArgbVisual. Only at the very end did I ever run xshowdamage.c. Seems like this either doesn't work properly any more or doesn't really work under i3. Bummer.

Anyway, here is the Rust version and I do not feel like opening a PR for this:

// This is a (rough) port of xshowdamage app from xorg.
// The original file has Copyright (C) 2005 Novell, Inc., is authored by David Reveman
// <davidr@novell.com> and is licensed under a MIT license.

use std::process::exit;

use x11rb::connection::Connection;
use x11rb::errors::ReplyOrIdError;
use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification};
use x11rb::protocol::damage::{self, ConnectionExt as _};
use x11rb::protocol::render::{self, ConnectionExt as _};
use x11rb::protocol::shape::{self, ConnectionExt as _};
use x11rb::protocol::xproto::{self, ConnectionExt as _};
use x11rb::protocol::Event;
use x11rb::wrapper::ConnectionExt as _;

x11rb::atom_manager! {
    pub Atoms: AtomsCookie {
        _NET_WM_STATE,
        _NET_WM_STATE_FULLSCREEN,
        _NET_WM_STATE_ABOVE,
        _NET_WM_STATE_STICKY,
        _NET_WM_STATE_SKIP_TASKBAR,
        _NET_WM_STATE_SKIP_PAGER,
    }
}

#[derive(Debug, Default)]
struct DamageBox {
    x: i16,
    y: i16,
    width: u16,
    height: u16,
    alpha: u16,
}

#[derive(Debug, Default)]
struct State {
    boxes: Vec<DamageBox>,
    window: xproto::Window,
    width: u16,
    height: u16,
    pixmap: xproto::Pixmap,
    picture: render::Picture,
    gc: xproto::Gcontext,
    redraw: bool,
}

impl State {
    fn new(
        conn: &impl Connection,
        screen: &xproto::Screen,
        window: xproto::Window,
        pict_format: render::Pictformat,
    ) -> Result<Self, ReplyOrIdError> {
        let (width, height) = (screen.width_in_pixels, screen.height_in_pixels);
        let pixmap = conn.generate_id()?;
        let picture = conn.generate_id()?;
        let gc = conn.generate_id()?;
        conn.create_pixmap(32, pixmap, window, width, height)?;
        conn.render_create_picture(picture, pixmap, pict_format, &Default::default())?;
        conn.create_gc(
            gc,
            window,
            &xproto::CreateGCAux::new().graphics_exposures(0),
        )?;
        Ok(Self {
            boxes: vec![],
            window,
            width,
            height,
            pixmap,
            picture,
            gc,
            redraw: true,
        })
    }

    fn watch_window(
        &mut self,
        conn: &impl Connection,
        window: xproto::Window,
    ) -> Result<(), ReplyOrIdError> {
        if window == self.window {
            return Ok(());
        }
        // XXX: Was xshowdamage written for non-reparenting WMs?!? Nothing happens when this is active.
        let ignore_override_redirect = true;
        if ignore_override_redirect
            || !conn
                .get_window_attributes(window)?
                .reply()?
                .override_redirect
        {
            let id = conn.generate_id()?;
            conn.damage_create(id, window, damage::ReportLevel::RAW_RECTANGLES)?;
        }
        Ok(())
    }

    fn handle_event(&mut self, conn: &impl Connection, event: Event) -> Result<(), ReplyOrIdError> {
        match event {
            Event::Expose(_) => {
                // XXX: This code is unreachable since we do not ask for expose events, but the
                // original code also has this...?!
                self.redraw = true;
                Ok(())
            }
            Event::MapNotify(notify) => {
                if notify.window != self.window {
                    self.watch_window(conn, notify.window)?;
                }
                Ok(())
            }
            Event::DamageNotify(notify) => {
                if notify.drawable != self.window {
                    self.boxes.push(DamageBox {
                        x: notify.geometry.x + notify.area.x,
                        y: notify.geometry.y + notify.area.y,
                        width: notify.area.width,
                        height: notify.area.height,
                        alpha: 0xffff,
                    });
                    self.redraw = true;
                }
                Ok(())
            }
            Event::Error(err) => {
                eprintln!("Got error {:?}", err);
                Ok(())
            }
            _ => Ok(()),
        }
    }

    fn redraw_boxes(&mut self, conn: &impl Connection) -> Result<(), ReplyOrIdError> {
        if !self.redraw {
            return Ok(());
        }
        self.redraw = false;

        // Clear the pixmap
        conn.render_fill_rectangles(
            render::PictOp::SRC,
            self.picture,
            render::Color {
                red: 0,
                green: 0,
                blue: 0,
                alpha: 0,
            },
            &[xproto::Rectangle {
                x: 0,
                y: 0,
                width: self.width,
                height: self.height,
            }],
        )?;

        // Draw boxes
        for entry in &self.boxes {
            conn.render_fill_rectangles(
                render::PictOp::OVER,
                self.picture,
                render::Color {
                    red: entry.alpha,
                    green: 0,
                    blue: 0,
                    alpha: entry.alpha,
                },
                &[xproto::Rectangle {
                    x: entry.x,
                    y: entry.y,
                    width: entry.width,
                    height: entry.height,
                }],
            )?;
        }

        // Copy to the window
        conn.copy_area(
            self.pixmap,
            self.window,
            self.gc,
            0,
            0,
            0,
            0,
            self.width,
            self.height,
        )?;

        Ok(())
    }
}

/// Check whether the X11 server supports all the X11 extensions that we need
fn check_server(
    conn: &impl Connection,
    screen: &xproto::Screen,
) -> Result<(xproto::Visualid, render::Pictformat), ReplyOrIdError> {
    let extensions = [
        damage::X11_EXTENSION_NAME,
        render::X11_EXTENSION_NAME,
        shape::X11_EXTENSION_NAME,
    ];
    for ext in &extensions {
        conn.prefetch_extension_information(ext)?;
    }
    for ext in &extensions {
        if conn.extension_information(ext)?.is_none() {
            eprintln!("{} extension is not supported", ext);
            exit(1);
        }
    }

    // We do not actually care about the version that the server supports, so ignore the reply
    conn.damage_query_version(damage::X11_XML_VERSION.0, damage::X11_XML_VERSION.1)?;

    let pict_formats = conn.render_query_pict_formats()?.reply()?;

    // Find all 32 bit true color visuals that correspond to a RENDER direct format with alpha.
    // The original code uses XGetVisualInfo() and XRenderFindVisualFormat() to simplify this.
    let pict_format_for_visual = |visual: xproto::Visualid| -> Option<render::Pictformat> {
        pict_formats
            .screens
            .iter()
            .flat_map(|pict_screen| {
                pict_screen
                    .depths
                    .iter()
                    .flat_map(|pict_depth| {
                        pict_depth
                            .visuals
                            .iter()
                            .filter(|pict_visual| pict_visual.visual == visual)
                            .map(|pict_visual| pict_visual.format)
                            .next()
                    })
                    .next()
            })
            .next()
    };
    let find_picture_format_info = |pict_format: render::Pictformat| {
        pict_formats
            .formats
            .iter()
            .filter(|format| format.id == pict_format)
            .next()
    };
    let result = screen
        .allowed_depths
        .iter()
        .filter(|depth| depth.depth == 32)
        .filter_map(|depth| {
            depth
                .visuals
                .iter()
                .filter(|visual| visual.class == xproto::VisualClass::TRUE_COLOR)
                .map(|visual| visual.visual_id)
                .filter_map(|visual_id| {
                    pict_format_for_visual(visual_id)
                        .and_then(find_picture_format_info)
                        .filter(|info| {
                            info.type_ == render::PictType::DIRECT && info.direct.alpha_mask != 0
                        })
                        .map(|info| (visual_id, info.id))
                })
                .next()
        })
        .next();
    match result {
        Some(result) => Ok(result),
        None => {
            eprintln!("The X11 server does not support transparent windows");
            exit(1);
        }
    }
}

fn create_window(
    conn: &impl Connection,
    screen: &xproto::Screen,
    visual_id: xproto::Visualid,
    atoms: &Atoms,
) -> Result<xproto::Window, ReplyOrIdError> {
    // Create the window
    let colormap = xproto::ColormapWrapper::create_colormap(
        conn,
        xproto::ColormapAlloc::NONE,
        screen.root,
        visual_id,
    )?;
    let window = conn.generate_id()?;
    conn.create_window(
        32,
        window,
        screen.root,
        0,
        0,
        screen.width_in_pixels,
        screen.height_in_pixels,
        0,
        xproto::WindowClass::INPUT_OUTPUT,
        visual_id,
        &xproto::CreateWindowAux::new()
            .event_mask(xproto::EventMask::EXPOSURE)
            .background_pixel(0)
            .border_pixel(0)
            .colormap(colormap.colormap()),
    )?;

    // Set hints on the window
    let mut hints = WmHints::new();
    hints.input = Some(false);
    hints.set(conn, window)?;

    let mut hints = WmSizeHints::new();
    hints.position = Some((WmSizeHintsSpecification::ProgramSpecified, 0, 0));
    hints.size = Some((
        WmSizeHintsSpecification::ProgramSpecified,
        screen.width_in_pixels.into(),
        screen.height_in_pixels.into(),
    ));
    hints.set(conn, window, xproto::AtomEnum::WM_NORMAL_HINTS)?;

    conn.change_property32(
        xproto::PropMode::REPLACE,
        window,
        atoms._NET_WM_STATE,
        xproto::AtomEnum::ATOM,
        &[
            atoms._NET_WM_STATE_FULLSCREEN,
            atoms._NET_WM_STATE_ABOVE,
            atoms._NET_WM_STATE_STICKY,
            atoms._NET_WM_STATE_SKIP_TASKBAR,
            atoms._NET_WM_STATE_SKIP_PAGER,
        ],
    )?;

    // Make the window click-through by setting an empty input shape
    conn.shape_rectangles(
        shape::SO::SET,
        shape::SK::INPUT,
        xproto::ClipOrdering::YX_BANDED,
        window,
        0,
        0,
        &[],
    )?;

    conn.map_window(window)?;

    Ok(window)
}

fn select_input_on_all_children(
    conn: &impl Connection,
    window: xproto::Window,
) -> Result<(), ReplyOrIdError> {
    conn.change_window_attributes(
        window,
        &xproto::ChangeWindowAttributesAux::new()
            .event_mask(xproto::EventMask::SUBSTRUCTURE_NOTIFY),
    )?;

    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // The original program supported an optional window-id argument. That was not ported.

    let (conn, screen_num) = x11rb::connect(None)?;
    let screen = &conn.setup().roots[screen_num];

    let atoms = Atoms::new(&conn)?;
    let (visual_id, picture_format) = check_server(&conn, screen)?;
    let atoms = atoms.reply()?;

    let window = create_window(&conn, screen, visual_id, &atoms)?;

    // The original code has an "if" here. Since we do not implement the watch window option, we always do this
    select_input_on_all_children(&conn, screen.root)?;

    let mut state = State::new(&conn, screen, window, picture_format)?;
    // Watch all existing windows
    for child in conn.query_tree(screen.root)?.reply()?.children {
        state.watch_window(&conn, child)?;
    }
    loop {
        while let Some(event) = conn.poll_for_event()? {
            state.handle_event(&conn, event)?;
        }

        state.redraw_boxes(&conn)?;

        if state.boxes.is_empty() {
            conn.flush()?;
            state.handle_event(&conn, conn.wait_for_event()?)?;
        } else {
            // Make boxes more transparent
            state
                .boxes
                .iter_mut()
                .for_each(|element| element.alpha /= 2);
            // Remove invisible ones
            state.boxes.retain(|element| element.alpha > 0x00ff);

            state.redraw = true;
            conn.flush()?;
            std::thread::sleep(std::time::Duration::from_millis(40));
        }
    }
}

(Oh and: Instead of poll()ing for events, my Rust version just sleeps for 40 ms and does not do any kind of earlier wakeup. Seems to work about equally well.)

Edit: Ideas for improvements.