rustcairoxcbpangopangocairo

Cairo XCB surface only renders text with pango when written to a png


I've successfully created a window with xcb-rs. When I try to write text to that window with pangocairo, it only renders if I write the surface to a png. If I try to, for example, draw a rectangle with cairo (not using pango), it doesn't render at all.

I'm running this on Arch Linux with Linux version 6.9.2, xorg-server 21.1.13, i3 4.23, and picom (for transparency) 11.2.

My code is verbose because XCB is verbose, but this is as minimal as I could make it.

use anyhow::Result;
use cairo::XCBConnection;
use temp::xreq::*;
use xcb::{composite, x, Xid};

fn main() -> Result<()> {
    // generate necessary ids, set up constants
    let (conn, screen_idx) = xcb::Connection::connect(None)?;
    let window: x::Window = conn.generate_id();
    let colormap: x::Colormap = conn.generate_id();

    let screen = conn.get_setup().roots().nth(screen_idx as usize).unwrap();
    let width = screen.width_in_pixels();
    let height = 32;

    let depth = 32;
    let mut visual = find_visual(screen, depth);

    // create colormap
    conn.check_request(conn.send_request_checked(&x::CreateColormap {
        alloc: x::ColormapAlloc::None,
        mid: colormap,
        visual: visual.visual_id(),
        window: screen.root(),
    }))?;

    // create window
    conn.check_request(conn.send_request_checked(&x::CreateWindow {
        depth,
        wid: window,
        parent: screen.root(),
        x: 0,
        y: 0,
        width,
        height,
        border_width: 0,
        class: x::WindowClass::InputOutput,
        visual: visual.visual_id(),
        value_list: &[
            x::Cw::BackPixel(0x00000000),
            x::Cw::BorderPixel(0x00000000),
            x::Cw::Colormap(colormap),
        ],
    }))?;

    // map window
    conn.check_request(conn.send_request_checked(&x::MapWindow { window }))?;

    // create cairo surface for the window
    let surface = cairo::XCBSurface::create(
        unsafe { &XCBConnection::from_raw_none(std::mem::transmute(conn.get_raw_conn())) },
        &cairo::XCBDrawable(window.resource_id()),
        unsafe { &cairo::XCBVisualType::from_raw_none(std::mem::transmute(&mut visual as *mut _)) },
        width.into(),
        height.into(),
    )?;

    // draw text and write image to png
    let context = cairo::Context::new(&surface)?;
    let pango_context = pangocairo::pango::Context::new();
    pango_context.set_font_map(Some(&pangocairo::FontMap::new()));
    pango_context.load_font(&pangocairo::pango::FontDescription::from_string(
        "Fira Code",
    ));
    context.set_source_rgba(1.0, 0.0, 0.0, 1.0);
    let layout = pangocairo::pango::Layout::new(&pango_context);
    layout.set_text("test");
    pangocairo::functions::show_layout(&context, &layout);
    let mut file = std::fs::File::create("/dev/null")?;
    surface.write_to_png(&mut file)?;

    // prevent program termination
    loop {}
}

And the referenced utility functions (but I'm more confident in these):

use std::fmt::Debug;

use anyhow::{Context, Result};
use xcb::x::{self, Atom};

pub fn send<X: xcb::RequestWithoutReply + Debug>(conn: &xcb::Connection, req: &X) -> Result<()> {
    Ok(conn
        .check_request(conn.send_request_checked(req))
        .with_context(|| format!("sending xcb request failed: {:?}", req))?)
}

pub fn query<X: xcb::RequestWithReply + Debug>(
    conn: &xcb::Connection,
    req: &X,
) -> Result<<<X as xcb::Request>::Cookie as xcb::CookieWithReplyChecked>::Reply>
where
    <X as xcb::Request>::Cookie: xcb::CookieWithReplyChecked,
{
    Ok(conn
        .wait_for_reply(conn.send_request(req))
        .with_context(|| format!("sending xcb query failed: {:?}", req))?)
}

pub fn intern_named_atom(conn: &xcb::Connection, atom: &[u8]) -> Result<xcb::x::Atom> {
    Ok(conn
        .wait_for_reply(conn.send_request(&x::InternAtom {
            only_if_exists: true,
            name: atom,
        }))?
        .atom())
}

pub fn change_property<P: x::PropEl>(
    conn: &xcb::Connection,
    window: x::Window,
    property: Atom,
    r#type: Atom,
    data: &[P],
) -> Result<()> {
    Ok(conn
        .check_request(conn.send_request_checked(&x::ChangeProperty {
            mode: x::PropMode::Replace,
            window,
            property,
            r#type,
            data,
        }))
        .with_context(|| format!("changing property failed: {:?}", property))?)
}

pub fn change_window_property<P: x::PropEl>(
    conn: &xcb::Connection,
    window: x::Window,
    property: Atom,
    data: &[P],
) -> Result<()> {
    change_property(conn, window, property, x::ATOM_ATOM, data)
}

pub fn find_visual(screen: &x::Screen, depth: u8) -> x::Visualtype {
    for allowed_depth in screen.allowed_depths() {
        if allowed_depth.depth() == depth {
            for visual in allowed_depth.visuals() {
                if visual.class() == x::VisualClass::TrueColor {
                    return *visual;
                }
            }
        }
    }
    panic!("No visual type found");
}

When I run this code, the window appears and renders the string "test" in red. When I remove the line to write the surface to a file, the window still appears, but the text doesn't appear.


Solution

  • Welcome to X11, where you have to redraw your window whenever you are told to. For each of these cases you get an Expose event. I haven't actually tried it (sorry), but I am certain that the problem is this piece of code:

    loop {}
    

    Instead, you need something like:

    while let Some(event) = conn.wait_for_event() {
       if xcb::EXPOSE == event.response_type() & !0x80 {
          // Redraw the window contents.
          // If you want to get fancy, you can look at the details of the event
          // and either only redraw the requested area or only deal with expose
          // events with count == 0
       }
       conn.flush();
    }
    

    Why does this have the effect for you that no drawing is actually visible? Well, you have a race condition: When your program sends a MapWindow request, the window does not immediately become visible. Instead, the window manager gets informed about this and "arranges things" (e.g. adding a titlebar). Only afterwards does the window become visible. But since your program is drawing to the window immediately, it is drawing to a not-visible window. In this case, the X11 server just discards the drawing commands and does not do anything with them.

    The other problem is that you need to call surface.flush() to make cairo actually send the drawing requests to the X11 server... sometimes. In 90% of cases you get away without doing this. And it also depends on the capabilities of the X11 server, so not doing this is a common bug.

    What happens is: If you do some drawing commands that cannot be done by the X11 server, cairo basically downloads a screenshot and does the drawing locally to an in-memory image. Since uploading / downloading this all the time would be expensive, the in-memory image is kept until you flush() the surface.

    The other thing you need: After cairo generates its X11 requests, you need to make sure that xcb actually sends them to the X11 server and does not simply have them in its output buffer. This is what conn.flush() does.