Utilities for X11 image capture and acceptance testing #22

Merged
trurl merged 1 commits from trurl/wmaker:refactor/riir.headless-utils into refactor/riir 2026-03-26 18:47:37 -04:00
25 changed files with 1244 additions and 17 deletions

4
.gitignore vendored
View File

@@ -152,7 +152,9 @@ WPrefs.app/WPrefs.desktop
# Rust stuff.
/**/target/**
WINGs/wings-rs/src/WINGsP.rs
WINGs/wings-rs-tests/Cargo.lock
WINGs/wings-rs/Cargo.lock
WINGs/wings-rs/src/WINGsP.rs
wmaker-rs/Cargo.lock
wrlib-rs/src/ffi.rs
wutil-rs/Cargo.lock

View File

@@ -2,7 +2,7 @@
AUTOMAKE_OPTIONS =
SUBDIRS = WINGs wings-rs . po Documentation Resources
SUBDIRS = WINGs wings-rs wings-rs-tests . po Documentation Resources
DIST_SUBDIRS = $(SUBDIRS) Tests Examples Extras
libWINGs_la_LDFLAGS = -version-info @WINGS_VERSION@

View File

@@ -903,6 +903,7 @@ version = "0.1.0"
dependencies = [
"insta",
"insta-image",
"libc",
"png",
"tempdir",
"wings-rs",

View File

@@ -0,0 +1,18 @@
[package]
name = "wings-rs-tests"
version = "0.1.0"
edition = "2024"
[dependencies]
libc = "0.2"
insta = { git = "https://github.com/mitsuhiko/insta.git", branch = "master" }
insta-image = { git = "https://git.sdf.org/trurl/insta-image.git", branch = "main", features = ["png"] }
png = "0.18"
tempdir = "0.3.7"
wings-rs = { path = "../wings-rs" }
wutil-rs = { path = "../../wutil-rs" }
x11 = "2.21.0"
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3

View File

@@ -0,0 +1,40 @@
AUTOMAKE_OPTIONS =
RUST_SOURCES = \
src/lib.rs \
src/headless/mod.rs \
src/headless/xvfb.rs \
src/headless/xwd.rs \
src/headless/snapshots/wings_rs_tests__headless__xvfb__tests__blank_screen.snap \
src/headless/snapshots/wings_rs_tests__headless__xvfb__tests__blank_screen.snap.png \
src/headless/snapshots/wings_rs_tests__headless__xvfb__tests__snowlamp_in_window.snap \
src/headless/snapshots/wings_rs_tests__headless__xvfb__tests__snowlamp_in_window.snap.png \
src/headless/snapshots/wings_rs_tests__headless__xvfb__tests__xeyes.snap \
src/headless/snapshots/wings_rs_tests__headless__xvfb__tests__xeyes.snap.png \
src/headless/snapshots/wings_rs_tests__headless__xwd__tests__snowlamp_encoded.snap \
src/headless/snapshots/wings_rs_tests__headless__xwd__tests__snowlamp_encoded.snap.png
RUST_EXTRA = \
Cargo.lock \
Cargo.toml
wutil_c_lib = $(top_builddir)/WINGs/libWUtil.la
wings_c_lib = $(top_builddir)/WINGs/libWINGs.la
wings_rs_lib = $(top_builddir)/WINGs/wings-rs/target/debug/libwings_rs.a
wings_rs_test_lib = target/debug/libwings_rs_tests.rlib
rustlib: $(RUST_SOURCES) $(RUST_EXTRA)
cargo build
check-local: rustlib
clean-local:
$(CARGO) clean
all: rustlib
# Tests run with cargo-nextest because it puts each application test in its own
# process, and WINGs currently assumes that it is running in a single-threaded
# environment.
test: rustlib
LD_LIBRARY_PATH=../.libs $(CARGO) nextest run

View File

@@ -0,0 +1,6 @@
fn main() {
println!("cargo:rustc-link-lib=static=X11");
println!("cargo:rustc-link-lib=static=xcb");
println!("cargo:rustc-link-lib=static=Xau");
println!("cargo:rustc-link-lib=static=Xdmcp");
}

View File

@@ -0,0 +1,133 @@
//! Provides utilites for integration tests that render to a headless X11 server.
//!
//! ## X11 `DISPLAY` value allocation
//!
//! When starting headless X servers, some care needs to be taken in selecting
//! display numbers. An X server needs to have a unique display number and will
//! not start if it is assigned a display number that is already in use.
//!
//! Local X servers usually leave a lockfile in `/tmp`, like `/tmp/.X0-lock` for
//! display 0. Avoiding the use of display numbers that have a corresponding
//! lockfile will get you part of the way towards avoiding collisions, but
//! further coordination is provided by the [`Lock`] and
//! [`DisplayNumberRegistry`] structs. These interfaces provide a mechanism for
//! claiming a display number in a way that coordinates across processes and
//! should be more efficient than simply scanning `/tmp` each time a new display
//! number is needed.
use std::{
fs,
io,
path::PathBuf,
sync::atomic::{self, AtomicU16},
};
pub mod xvfb;
pub mod xwd;
/// Represents a lock on a display number, which is released on drop.
pub struct Lock {
trurl marked this conversation as resolved Outdated
Outdated
Review

I think the return statements here are redundant: the match is the only expression in this function.

I think the `return` statements here are redundant: the match is the only expression in this function.
Outdated
Review

Right you are. Thanks!

Right you are. Thanks!
display: u16,
path: PathBuf,
}
/// Errors that may occur when trying to lock a display number.
pub enum LockError {
Io(io::Error),
Locked,
}
impl Lock {
pub(crate) fn new(display: u16, path: PathBuf) -> Self {
Lock {
display,
path
}
trurl marked this conversation as resolved Outdated
Outdated
Review

So this gets into some of the weird semantics between POSIX locks and flock, but do you want to lock against threads in your own process, or other processes? Or both? The use of Atomics suggests that the former is at least on your radar.

I think what I would consider doing is having a "display allocator" that always starts counting from the lower bound, and attempts to allocate a "display" number by creating a temporary file, and keeps track of what numbers have been allocated (e.g., in a bit vector or something), all protected by a mutex. It doesn't have to be terribly general purpose: you won't have more than a handful running at a time; you could make the "allocator" a bitmap in a u128. The method would basically be,

  • lock the mutex
  • find the position of the first unset bit in the allocator variable,
  • If all bits are allocated, panic!().
  • Set the display candidate number to the bit position + 32.
  • Attempt to create a lock file as /tmp/.X{n}-lock (if that fails, continue)
  • Set the bit
  • Unlock the mutex
    The allocated display number is now bitno + 32.

And then the drop is,

  • Lock the mutex
  • Clear the bit (display number - 32) in the allocator variable
  • Close and unlink the lock file (/tmp/.X{dispno}-lock)
  • Unlock the mutex
So this gets into some of the weird semantics between POSIX locks and `flock`, but do you want to lock against threads in your own process, or other processes? Or both? The use of Atomics suggests that the former is at least on your radar. I think what I would consider doing is having a "display allocator" that always starts counting from the lower bound, and attempts to allocate a "display" number by creating a temporary file, and keeps track of what numbers have been allocated (e.g., in a bit vector or something), all protected by a mutex. It doesn't have to be terribly general purpose: you won't have more than a handful running at a time; you could make the "allocator" a bitmap in a u128. The method would basically be, - lock the mutex - find the position of the first unset bit in the allocator variable, - If all bits are allocated, panic!(). - Set the display candidate number to the bit position + 32. - Attempt to create a lock file as `/tmp/.X{n}-lock` (if that fails, continue) - Set the bit - Unlock the mutex The allocated display number is now bitno + 32. And then the drop is, - Lock the mutex - Clear the bit (display number - 32) in the allocator variable - Close and unlink the lock file (`/tmp/.X{dispno}-lock`) - Unlock the mutex -
Outdated
Review

(Ok, so: the default Asahi Linux config powers your system completely off if you hit the power button, and Apple put the power button right next to backspace. I'll do my best to rewrite everything I just lost in a reply here, but please forgive me if it's a little terse.)

I think we're on the same page. I should definitely write better rustdoc comments.

I am trying to guard against other processes and threads in the same process. (https://nexte.st/, for example, runs each test in its own process.)

Attempt to create a lock file as /tmp/.X{n}-lock (if that fails, continue)

This looks good! It doesn't look like Xvfb cares if you have an empty /tmp/X{n}-lock, and OpenOptions::create_new should enable atomically checking and claiming a DISPLAY value. This simplifies things substantially (with no need for locking). Thank you!

Regarding the use of a bitfield: I don't think we need to reclaim DISPLAY values within the same process. Tracking the likely next unallocated DISPLAY value for the process should help to avoid having to scan a range of lockfiles that are in use each time a new DISPLAY value is needed. I created over 50 stale /tmp/.X{n}-lock files while developing this library, and failed test runs could also conceivably leave them around. Using an atomic reduces the cost of scanning a chunk of DISPLAY value space redundantly each time we want to provide a new value. So I'm in favor of simply incrementing an atomic.

(Ok, so: the default Asahi Linux config powers your system completely off if you hit the power button, and Apple put the power button right next to backspace. I'll do my best to rewrite everything I just lost in a reply here, but please forgive me if it's a little terse.) I think we're on the same page. I should definitely write better rustdoc comments. I am trying to guard against other processes and threads in the same process. (https://nexte.st/, for example, runs each test in its own process.) > Attempt to create a lock file as `/tmp/.X{n}-lock` (if that fails, continue) This looks good! It doesn't look like `Xvfb` cares if you have an empty `/tmp/X{n}-lock`, and `OpenOptions::create_new` should enable atomically checking and claiming a `DISPLAY` value. This simplifies things substantially (with no need for locking). Thank you! Regarding the use of a bitfield: I don't think we need to reclaim `DISPLAY` values within the same process. Tracking the likely next unallocated `DISPLAY` value for the process should help to avoid having to scan a range of lockfiles that are in use each time a new `DISPLAY` value is needed. I created over 50 stale `/tmp/.X{n}-lock` files while developing this library, and failed test runs could also conceivably leave them around. Using an atomic reduces the cost of scanning a chunk of `DISPLAY` value space redundantly each time we want to provide a new value. So I'm in favor of simply incrementing an atomic.
}
/// Returns the locked `DISPLAY` value.
pub fn display(&self) -> u16 {
self.display
}
}
impl Drop for Lock {
fn drop(&mut self) {
// `file` should be unlinked already, but we explicitly try to delete it
// and unwrap the result so that errors aren't dropped silently.
match fs::remove_file(&self.path) {
Ok(_) => (),
Err(e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => panic!("could not unlink lock file: {:?}", e),
}
}
}
// Shared across threads in this process to help keep us from repeatedly asking
// for the same DISPLAY value.
static NEXT_DISPLAY: AtomicU16 = AtomicU16::new(32);
/// Coordinates on the value of `DISPLAY` to use when creating new X11 servers.
///
/// Methods on `DisplayNumberRegistry` may be called across threads or in
/// different processes to ensure that X servers are created with unique
/// display numbers.
pub struct DisplayNumberRegistry;
impl DisplayNumberRegistry {
trurl marked this conversation as resolved Outdated
Outdated
Review

So...A couple of things you can do to make this a bit more robust.

  1. Use your own subdirectory under /tmp for your lock files, to avoid conflicts against an actual X server.
  2. Open the file with O_EXCL; there's some wrapper in fs::create that does this, and lets you see the moral equivalent of EEXISTS. O_EXCL is atomic on Unix, and prevents
  3. You could also flock the file once it's open, but that's probably overkill.
So...A couple of things you can do to make this a bit more robust. 1. Use your own subdirectory under /tmp for your lock files, to avoid conflicts against an actual X server. 2. Open the file with `O_EXCL`; there's some wrapper in `fs::create` that does this, and lets you see the moral equivalent of EEXISTS. O_EXCL is atomic on Unix, and prevents 3. You could also `flock` the file once it's open, but that's probably overkill.
Outdated
Review

Oh! You do the first. I got confused by searching for /tmp/.X{}-lock, but that's to try and avoid racing against another process starting an X server.

The big issue I'd worry about, I suppose, is either one of the local servers, or an ssh server process providing X forwarding. Starting displays at 32, as you do, will almost certainly never collide with anything.

Oh! You do the first. I got confused by searching for /tmp/.X{}-lock, but that's to try and avoid racing against another process starting an X server. The big issue I'd worry about, I suppose, is either one of the local servers, or an `ssh` server process providing X forwarding. Starting displays at 32, as you do, will almost certainly never collide with anything.
Outdated
Review

As a point of reference, https://man.archlinux.org/man/xvfb-run.1.en starts at display number 99, so we could go higher. But I think 32 is fine.

RE checking/claiming a lockfile atomically: we both left longer comments further down, so I'll resolve this one.

As a point of reference, https://man.archlinux.org/man/xvfb-run.1.en starts at display number 99, so we could go higher. But I think 32 is fine. RE checking/claiming a lockfile atomically: we both left longer comments further down, so I'll resolve this one.
/// Returns a lock on the first local display number (the `N` in `:N` for
/// the X client `DISPLAY` environment variable) that is not currently in
/// use.
///
/// If no display numbers are available, returns `None`.
///
/// To avoid collisions with other processes, attempts are made to sniff out
/// which display numbers are already in use by looking for lock files
/// matching the pattern `/tmp/.X*-lock`. If such a file exists, its
/// display number will not be used.
///
/// When an available display number is found, an empty lockfile is
trurl marked this conversation as resolved Outdated
Outdated
Review

Given that you do Not want to collide with an actual X server process (or ssh daemon simulating one), why not use these names as the lock file directly? It's not just that you have your own namespace to protect colliding with an already-running server, but also that a server started subsequently to you running could collide with you (though since it involves binding a socket, that is unlikely). But an O_EXCL create here would pretty much guarantee isolation between actual X servers and you.

Given that you do Not want to collide with an actual X server process (or ssh daemon simulating one), why not use these names as the lock file directly? It's not just that you have your own namespace to protect colliding with an already-running server, but also that a server started subsequently to you running could collide with you (though since it involves binding a socket, that is unlikely). But an `O_EXCL` create here would pretty much guarantee isolation between actual X servers and you.
Outdated
Review

Done.

Done.
/// atomically created (and marking the display number as claimed to other
/// well-behaved processes). When `Xvfb` is run using that display number,
/// it silently overwrites the empty lockfile.
///
/// Any extant lockfile should be deleted by the `Drop` impl for [`Lock`],
/// although tests that panic may leave stale lockfiles behind.
pub fn next_unused_display() -> io::Result<Lock> {
loop {
let prev = NEXT_DISPLAY.fetch_add(1, atomic::Ordering::SeqCst);
if prev == u16::MAX {
return Err(io::Error::other("display numbers exhausted; check /tmp/.X{n}-lock"));
}
let next = prev + 1;
let path = PathBuf::from(format!("/tmp/.X{}-lock", next));
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path) {
Ok(_) => return Ok(Lock::new(next, path)),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_unused_display() {
assert!(DisplayNumberRegistry::next_unused_display().is_ok());
assert_ne!(
DisplayNumberRegistry::next_unused_display()
.unwrap()
.display,
DisplayNumberRegistry::next_unused_display()
.unwrap()
.display
);
}
}

View File

@@ -0,0 +1,7 @@
---
source: src/headless/xvfb.rs
assertion_line: 387
expression: xwd.into_png().unwrap()
extension: png
snapshot_kind: binary
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,7 @@
---
source: src/headless/xvfb.rs
assertion_line: 416
expression: compressed
extension: png
snapshot_kind: binary
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,7 @@
---
source: src/headless/xvfb.rs
assertion_line: 402
expression: compressed
extension: png
snapshot_kind: binary
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,7 @@
---
source: src/headless/xwd.rs
assertion_line: 321
expression: snowlamp_xwd.into_png().unwrap()
extension: png
snapshot_kind: binary
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

View File

@@ -0,0 +1,471 @@
//! Provides utilities for headlessly running and taking screenshots of X11
//! programs in unit and integration tests.
//!
//! This uses [Xvfb](https://x.org/releases/X11R7.7/doc/man/man1/Xvfb.1.xhtml).
//!
//! Tests of this module invoke `xeyes` and `xwud` subprocesses. For all tests
//! to pass, you may need to install these X11 utilities and make them available
//! on your `PATH`.
use std::{
ffi::CString,
fs, io,
process::{Child, Command, ExitStatus, Stdio},
thread,
time::{Duration, Instant},
};
use tempdir::TempDir;
use super::xwd;
use super::Lock;
/// Arguments for `Xvfb`. Use `XvfbArgs::default()` for default values.
#[derive(Clone, Copy, Debug)]
pub struct XvfbArgs {
/// Width of the X11 screen.
pub width: u32,
/// Height of the X11 screen.
pub height: u32,
/// Bit depth of the X11 screen.
///
/// This should probably be one of 8, 16, or 24. (And if you don't choose 24
/// bits, you should have a good reason why.)
pub depth: u8,
}
impl Default for XvfbArgs {
fn default() -> Self {
XvfbArgs {
width: 640,
height: 480,
depth: 24,
}
}
}
/// A captive `Xvfb` process, with affordances for running subprocesses that
/// connect to it and taking screenshots.
///
/// When dropped, child processes will be killed automatically.
pub struct XvfbProcess {
lock: Lock,
framebuffer_directory: TempDir,
process: Child,
subprocesses: Vec<(Child, SubprocessMonitor)>,
is_shutdown: bool,
}
impl XvfbProcess {
/// Attempts to start `Xvfb` with default options.
///
/// Returns `None` if an error occurs while starting the `Xvfb` process.
pub fn start_default(lock: Lock) -> SubprocessResult<Self> {
XvfbProcess::start(lock, XvfbArgs::default())
}
/// Starts an `Xvfb` process with options specified by `args`.
///
/// Returns `None` if an error occurs while starting the `Xvfb` process.
///
/// This function connects to the `Xvfb` server briefly to try to ensure
/// that Xvfb is ready for clients to connect to it.
pub fn start(lock: Lock, args: XvfbArgs) -> SubprocessResult<Self> {
let framebuffer_directory = TempDir::new("wings_rs_xvfb").map_err(|e| {
SubprocessError::new("Xvfb temp dir".into(), SubprocessErrorType::Spawn(e))
})?;
#[cfg(target_os = "linux")]
unsafe {
// Kill children if this parent process dies catastrophically (e.g.,
// a test raises SIGABRT).
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
}
let process = Command::new("Xvfb")
.arg(format!(":{}", lock.display))
.arg("-screen")
.arg("0")
.arg(format!("{}x{}x{}", args.width, args.height, args.depth))
.arg("-fbdir")
.arg(format!("{}", framebuffer_directory.path().display()))
.stderr(Stdio::piped())
.spawn()
.map_err(|e| SubprocessError::new("Xvfb".into(), SubprocessErrorType::Spawn(e)))?;
let process = XvfbProcess {
lock,
framebuffer_directory,
process,
subprocesses: Vec::new(),
is_shutdown: false,
};
let now = Instant::now();
let display_name = CString::new(
format!(":{}", process.lock.display)
).expect("could not construct display name");
while now.elapsed() < Duration::from_secs(1) {
unsafe {
let display = x11::xlib::XOpenDisplay(display_name.as_ptr());
if !display.is_null() {
x11::xlib::XCloseDisplay(display);
break;
}
}
thread::sleep(Duration::from_millis(10));
}
return Ok(process);
}
/// Returns the number in the `DISPLAY` environment variable to connect to
/// this X11 server.
pub fn display(&self) -> u16 {
self.lock.display
}
/// Provides access to the `Xvfb` child process wrapped by `self`.
pub fn process(&mut self) -> &mut Child {
&mut self.process
}
/// Attempts to clean up all child processes. Returns any errors encountered, or `Ok` if none.
///
/// Child processes are cleaned up with the following escalating steps:
///
/// * Call [`SubprocessMonitor::on_xvfb_shutdown`] on each child's monitor.
/// * Kill each of them directly (with [`Child::kill`]).
/// * Kill the `Xvfb` process (which may cause any children still connected to that display server to exit).
pub fn shutdown(&mut self) -> Result<(), Vec<SubprocessError>> {
if self.is_shutdown {
return Ok(());
}
self.is_shutdown = true;
let mut errors = Vec::new();
for (child, monitor) in self.subprocesses.iter_mut() {
if let Err(e) = monitor.on_xvfb_shutdown(child) {
errors.push(e);
}
match child.try_wait() {
Ok(Some(status)) => {
if let Err(e) = monitor.check_subprocess_exit(status) {
errors.push(e);
}
}
Ok(None) => {
if let Err(e) = child.kill() {
errors.push(SubprocessError::new(
monitor.name.clone(),
SubprocessErrorType::Kill(e),
));
}
}
Err(e) => errors.push(SubprocessError::new(
monitor.name.clone(),
SubprocessErrorType::Status(e),
)),
}
}
if let Err(e) = self.process.kill() {
errors.push(SubprocessError::new(
"Xvfb".into(),
SubprocessErrorType::Kill(e),
));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Runs a simple command in this display server. No command line parsing is
/// done, so `run("ls -la")` will probably not do what you expect (because
/// there is no binary on your `PATH` called `ls -la`). Use [`XvfbProcess::run_args`]
/// to provide arguments to `command`.
pub fn run(&mut self, command: impl Into<String>) -> Result<(), SubprocessError> {
let name = command.into();
self.run_args(name.clone(), &name, |_c| {})
}
/// Runs a command that you can provide arguments to in this display
/// server.
///
/// The `f` parameter can be used to modify the `Command` before it is
/// run (e.g., by adding arguments). For example:
///
/// ```
/// # use wings_rs_tests::headless::DisplayNumberRegistry;
/// # use wings_rs_tests::headless::xvfb::XvfbProcess;
/// # fn main() {
/// # let mut xvfb = XvfbProcess::start_default(DisplayNumberRegistry::next_unused_display().unwrap()).unwrap();
/// xvfb.run_args(
/// "demo of xeyes, angel-style",
/// "/usr/bin/xeyes",
/// |cmd| { cmd.arg("-biblicallyAccurate"); },
/// ).unwrap();
/// # }
/// ```
pub fn run_args(
&mut self,
name: impl Into<String>,
command: impl AsRef<std::ffi::OsStr>,
f: impl FnOnce(&mut Command),
) -> Result<(), SubprocessError> {
let mut command = Command::new(command);
command.env("DISPLAY", format!(":{}", self.lock.display));
f(&mut command);
let mut monitor = SubprocessMonitor::new(name.into(), command);
monitor.with_subprocess_exit(SubprocessMonitor::require_clean_exit());
let child = monitor.start()?;
self.subprocesses.push((child, monitor));
Ok(())
}
/// Checks all subprocesses started with the [`XvfbProcess::run`] family of
/// methods and returns errors for any that have an error status, or `Ok` if
/// none do.
pub fn check(&mut self) -> Result<(), Vec<SubprocessError>> {
let errors: Vec<_> = self
.subprocesses
.iter_mut()
.filter_map(|(child, monitor)| match child.try_wait() {
Ok(Some(status)) => monitor.check_subprocess_exit(status).err(),
_ => None,
})
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Returns a screenshot of the current framebuffer contents in `xwd(1)`
/// format.
///
/// For the same in a more generally useful format, try [`XvfbProcess::png_screenshot`].
pub fn xwd_screenshot(&self) -> io::Result<xwd::XwdImage> {
let path = self.framebuffer_directory.path().join("Xvfb_screen0");
Ok(xwd::XwdImage::read(&mut fs::read(path)?.into_iter()))
}
/// Returns a screenshot of the current framebuffer contents in PNG format.
///
/// Panics on errors.
pub fn png_screenshot(&self) -> Vec<u8> {
self.xwd_screenshot().unwrap().into_png().unwrap()
}
}
impl Drop for XvfbProcess {
fn drop(&mut self) {
self.shutdown().unwrap();
}
}
pub type SubprocessResult<T> = Result<T, SubprocessError>;
/// Errors that may occur when a subprocess fails to run or exit cleanly.
#[derive(Debug)]
pub struct SubprocessError {
/// Meaningful name that can be used to determine what subprocess failed.
pub name: String,
pub err: SubprocessErrorType,
}
impl SubprocessError {
pub fn new(name: String, err: SubprocessErrorType) -> Self {
SubprocessError { name, err }
}
}
/// Error details for `SubprocessError`.
#[derive(Debug)]
pub enum SubprocessErrorType {
/// Subprocess exited with bad exit code.
BadExit(ExitStatus),
/// Could not spawn child process (with `io::Error` as the reason).
Spawn(io::Error),
/// Could not kill child process (with `io::Error` as the reason).
Kill(io::Error),
/// Could not query child process for exit status (with `io::Error` as the reason).
Status(io::Error),
}
/// Manages spawning a subprocess, monitoring it during execution, and killing it when `Xvfb` shuts down.
pub struct SubprocessMonitor {
name: String,
command: Command,
check_subprocess_exit: Option<Box<dyn FnMut(&str, ExitStatus) -> SubprocessResult<()>>>,
on_xvfb_shutdown: Option<Box<dyn FnMut(&mut Child) -> SubprocessResult<()>>>,
}
impl SubprocessMonitor {
/// Creates a new monitor but does not start any subprocess yet.
///
/// This monitor may be configured further by calling mutators like
/// [`SubprocessMonitor::with_subprocess_exit`]. Once configuration is
/// completely, start the subprocess with [`SubprocessMonitor::start`].
pub fn new(name: impl Into<String>, command: Command) -> Self {
SubprocessMonitor {
name: name.into(),
command,
check_subprocess_exit: None,
on_xvfb_shutdown: None,
}
}
/// Sets the callback to execute when [`SubprocessMonitor::check_subprocess_exit`] is called.
pub fn with_subprocess_exit(
&mut self,
f: Box<dyn FnMut(&str, ExitStatus) -> SubprocessResult<()>>,
) -> &mut Self {
self.check_subprocess_exit = Some(f);
self
}
/// Sets the callback to execute when
/// [`SubprocessMonitor::on_xvfb_shutdown`] is called. It may check process
/// status, clean up, etc.
pub fn with_xvfb_shutdown(
&mut self,
f: Box<dyn FnMut(&mut Child) -> SubprocessResult<()>>,
) -> &mut Self {
self.on_xvfb_shutdown = Some(f);
self
}
/// Attempts to spawn a child process from the `Command` provided to
/// [`SubprocessMonitor::new`].
pub fn start(&mut self) -> SubprocessResult<Child> {
self.command
.spawn()
.map_err(|e| SubprocessError::new(self.name.clone(), SubprocessErrorType::Spawn(e)))
}
/// Called when subprocess exit status is polled. This may be used to signal
/// an error because the exit status is bad.
pub fn check_subprocess_exit(&mut self, status: ExitStatus) -> SubprocessResult<()> {
if let Some(f) = &mut self.check_subprocess_exit {
(f)(&self.name, status)
} else {
Ok(())
}
}
/// Called immediately before the parent display server is being shut
/// down. This may be used to send `SIGINT` or otherwise clean up the child
/// before it is killed more forcibly.
pub fn on_xvfb_shutdown(&mut self, child: &mut Child) -> SubprocessResult<()> {
if let Some(f) = &mut self.on_xvfb_shutdown {
(f)(child)
} else {
Ok(())
}
}
/// Returns a callback suitable for passing to [`SubprocessMonitor::check_subprocess_exit`]
/// which requires that the process has exited cleanly.
pub fn require_clean_exit() -> Box<dyn FnMut(&str, ExitStatus) -> SubprocessResult<()>> {
Box::new(|name: &str, status: ExitStatus| {
if status.success() {
Ok(())
} else {
Err(SubprocessError::new(
String::from(name),
SubprocessErrorType::BadExit(status),
))
}
})
}
}
#[cfg(test)]
mod tests {
use crate::headless::DisplayNumberRegistry;
use super::XvfbProcess;
use insta_image::assert_png_snapshot;
use std::{
thread,
time::Duration,
};
/// Starts Xvfb and returns a managing object, or panics.
fn start_xvfb() -> XvfbProcess {
XvfbProcess::start_default(
DisplayNumberRegistry::next_unused_display().expect("cannot find a value for DISPLAY"),
)
.expect("cannot start Xvfb")
}
/// Reads a single frame of PNG data from the PNG image in `data`. Panics on
/// errors.
fn read_png_metadata(data: &[u8]) -> png::OutputInfo {
let mut reader = png::Decoder::new(std::io::Cursor::new(data))
.read_info()
.unwrap();
let mut buf = vec![0; reader.output_buffer_size().unwrap()];
reader.next_frame(&mut buf).unwrap()
}
/// Launches `xeyes(1)` with colors to make sure we're doing the right thing
/// with our color channels when encoding a screenshot tas PNG.
fn xeyes_color(xvfb: &mut XvfbProcess) {
xvfb.run_args("xeyes", "xeyes", |c| {
c.arg("-outline")
.arg("blue")
.arg("-center")
.arg("red")
.arg("-fg")
.arg("green");
})
.unwrap();
thread::sleep(Duration::from_millis(100));
}
#[test]
fn png_blank_screenshot() {
let xvfb = start_xvfb();
let xwd = xvfb.xwd_screenshot().unwrap();
assert_eq!(
xwd.visual_class(),
crate::headless::xwd::VisualClass::TrueColor
);
let args = crate::headless::xvfb::XvfbArgs::default();
assert_eq!(xwd.header.depth, args.depth as u32);
assert_eq!(xwd.header.width, args.width);
assert_eq!(xwd.header.height, args.height);
assert_png_snapshot!("blank_screen", xwd.into_png().unwrap());
}
#[test]
fn png_xeyes_screenshot() {
let mut xvfb = start_xvfb();
xeyes_color(&mut xvfb);
let compressed = xvfb.png_screenshot();
let image_data = read_png_metadata(&compressed);
let args = crate::headless::xvfb::XvfbArgs::default();
assert_eq!(image_data.width, args.width);
assert_eq!(image_data.height, args.height);
assert_png_snapshot!("xeyes", compressed);
}
#[test]
fn png_lamp_image() {
let mut xvfb = start_xvfb();
xvfb.run_args("xwud", "xwud", |c| {
c.arg("-in").arg("src/headless/snowlamp.xwd");
})
.unwrap();
thread::sleep(Duration::from_millis(100));
let compressed = xvfb.png_screenshot();
assert_png_snapshot!("snowlamp_in_window", compressed);
}
}

View File

@@ -0,0 +1,325 @@
//! Provides basic support for rendering dumps of X11 windows made with [the `xwud` utility](https://gitlab.freedesktop.org/xorg/app/xwud/-/blob/master/xwud.c?ref_type=heads).
use std::{
ffi::CString,
io::Cursor,
mem,
};
const SUPPORTED_XWD_VERSION: u32 = 7;
/// Header for `xwd`-style dumps of X11 windows.
///
/// This is taken from `/usr/include/X11/XWDFile.h` on a Debian system.
#[derive(Clone)]
#[repr(C)]
pub struct XwdHeader {
/// Total header size (including null-terminated window name).
pub size: u32,
/// Version of `xwd` that this dump came from.
pub version: u32,
pub _format: u32,
/// Bit depth of the X11 server.
pub depth: u32,
/// Image width.
pub width: u32,
/// Image height.
pub height: u32,
pub _x_offset: u32,
pub _byte_order: u32,
pub _bitmap_unit: u32,
pub _bitmap_bit_order: u32,
pub _bitmap_pad: u32,
pub bits_per_pixel: u32,
pub _bytes_per_line: u32,
/// X11 visual class for this window. See [`XwdImage::visual_class`] for a
/// more human-interpretable value.
pub visual_class: u32,
/// Bitmask for the red channel in pixel data.
pub red_mask: u32,
/// Bitmask for the green channel in pixel data.
pub green_mask: u32,
/// Bitmask for the blue channel in pixel data.
pub blue_mask: u32,
/// Number of bits per RGB tuple in color data. (E.g., 24-bit color may
/// actually be padded to 4 bytes.)
pub bits_per_rgb: u32,
pub _colormap_entries: u32,
/// Number of colors in the colormap for the window that this dump came from.
///
/// Note that a colormap may exist but be unused (if the visual class is not
/// one that uses a colormap).
pub ncolors: u32,
pub _window_width: u32,
pub _window_height: u32,
pub _window_x: u32,
pub _window_y: u32,
pub _window_border_width: u32,
}
const HEADER_SIZE: u32 = mem::size_of::<XwdHeader>() as u32;
/// Color data for an entry in a colormap.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(C)]
pub struct XwdColor {
pixel: u32,
red: u16,
green: u16,
blue: u16,
flags: u8,
pad: u8,
}
fn next_u32(bytes: &mut impl Iterator<Item = u8>) -> u32 {
let bytes = [
bytes.next().unwrap(),
bytes.next().unwrap(),
bytes.next().unwrap(),
bytes.next().unwrap(),
];
u32::from_be_bytes(bytes)
}
fn next_u16(bytes: &mut impl Iterator<Item = u8>) -> u16 {
let bytes = [bytes.next().unwrap(), bytes.next().unwrap()];
u16::from_be_bytes(bytes)
}
/// Describes how pixel data is represented in an `xwd` dump.
///
/// See descriptions of visual classes at
/// <https://tronche.com/gui/x/xlib/window/visual-types.html> and [the official
/// documentation](https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html#visual_information)
/// for more information.
#[derive(Clone, Copy, Eq, Debug, PartialEq)]
pub enum VisualClass {
StaticGray,
GrayScale,
StaticColor,
PseudoColor,
TrueColor,
DirectColor,
}
impl XwdHeader {
/// Decodes an `XwdHeader` from `bytes`, panicking on failure.
///
/// Leaves `bytes` pointing to the start of the window name.
pub fn read(bytes: &mut impl Iterator<Item = u8>) -> XwdHeader {
// This layout might change in the future, but let's just go with it for now.
XwdHeader {
size: next_u32(bytes),
version: next_u32(bytes),
_format: next_u32(bytes),
depth: next_u32(bytes),
width: next_u32(bytes),
height: next_u32(bytes),
_x_offset: next_u32(bytes),
_byte_order: next_u32(bytes),
_bitmap_unit: next_u32(bytes),
_bitmap_bit_order: next_u32(bytes),
_bitmap_pad: next_u32(bytes),
bits_per_pixel: next_u32(bytes),
_bytes_per_line: next_u32(bytes),
visual_class: next_u32(bytes),
red_mask: next_u32(bytes),
green_mask: next_u32(bytes),
blue_mask: next_u32(bytes),
bits_per_rgb: next_u32(bytes),
_colormap_entries: next_u32(bytes),
ncolors: next_u32(bytes),
_window_width: next_u32(bytes),
_window_height: next_u32(bytes),
_window_x: next_u32(bytes),
_window_y: next_u32(bytes),
_window_border_width: next_u32(bytes),
}
}
}
impl XwdColor {
/// Reads an `XwdColor` from `bytes`, panicking on failure.
///
/// Leaves `bytes` pointing to the next byte after the end of the color
/// definition.
pub fn read(bytes: &mut impl Iterator<Item = u8>) -> XwdColor {
XwdColor {
pixel: next_u32(bytes),
red: next_u16(bytes),
green: next_u16(bytes),
blue: next_u16(bytes),
flags: bytes.next().unwrap(),
pad: bytes.next().unwrap(),
}
}
}
/// A decoded `xwd` dump.
pub struct XwdImage {
/// Header with image metadata.
pub header: XwdHeader,
/// Name of the X11 window that the image came from.
pub name: CString,
/// Colormap data for the X11 window that the image came from.
///
/// Note that this may be populated even if it is unused (because the
/// window's visual class is one that does not use a colormap).
pub colors: Vec<XwdColor>,
/// Pixel data for the image.
///
/// This may be indices pointing into `colors`, or it may be actual pixel
/// values.
pub pixels: Vec<u8>,
}
impl XwdImage {
/// Reads an `XwdImage` from `bytes`, panicking on failure.
pub fn read(bytes: &mut impl Iterator<Item = u8>) -> XwdImage {
let header = XwdHeader::read(bytes);
if header.version != SUPPORTED_XWD_VERSION {
panic!(
"header version {} is not supported. only version {} files are supported.",
header.version, SUPPORTED_XWD_VERSION
);
}
let name = Self::read_name(&header, bytes);
let colors = Self::read_colors(&header, bytes);
let pixels = bytes.collect();
XwdImage {
header,
name,
colors,
pixels,
}
}
/// Reads a colormap for the image described by `header` from `bytes`.
///
/// `bytes` must point at the start of the colormap data. Leaves `bytes`
/// pointing at the first byte of the next item in the data stream.
pub fn read_colors(header: &XwdHeader, bytes: &mut impl Iterator<Item = u8>) -> Vec<XwdColor> {
let mut colors = Vec::with_capacity(header.ncolors as usize);
for _ in 0..header.ncolors {
colors.push(XwdColor::read(bytes));
}
colors
}
/// Reads the X11 window name from `bytes`.
///
/// `bytes` must point at the start of the window name. Leaves `bytes`
/// pointing at the first byte of the next item in the data stream.
///
/// Note that the window name is a null-terminated string, but `header.size`
/// (which is supposed to be equal to the size of the header structure plus
/// the window name) may actually be longer than `strlen(name) +
/// sizeof(HeaderType)`. When that is the case, the extra padding after the
/// first null byte in the window name will be passed over silently.
pub fn read_name(header: &XwdHeader, bytes: &mut impl Iterator<Item = u8>) -> CString {
if header.size <= HEADER_SIZE {
panic!(
"Invalid header size {} (smaller than header struct size {})",
header.size, HEADER_SIZE
);
}
let mut buf = Vec::new();
let mut found_nul = false;
for _ in 0..header.size - HEADER_SIZE {
let b = bytes.next().unwrap();
if b == 0 {
if found_nul {
continue;
} else {
buf.push(b);
found_nul = true;
}
} else {
buf.push(b);
}
}
CString::from_vec_with_nul(buf).unwrap()
}
/// Returns the visual class for this window dump.
pub fn visual_class(&self) -> VisualClass {
match self.header.visual_class {
0 => VisualClass::StaticGray,
1 => VisualClass::GrayScale,
2 => VisualClass::StaticColor,
3 => VisualClass::PseudoColor,
4 => VisualClass::TrueColor,
5 => VisualClass::DirectColor,
_ => panic!(
"unrecognized X11 visual class: {}",
self.header.visual_class
),
}
}
/// Encodes this image as a PNG, consumes `self` in the process (because the
/// image data is reused during the encoding process).
///
/// Note that only [`VisualClass::TrueColor`] images with a specific pixel
/// format (24-bit color packed into 32-bit segments with BGR ordering) are
/// supported.
pub fn into_png(self) -> Result<Vec<u8>, png::EncodingError> {
match self.visual_class() {
VisualClass::TrueColor => into_png_true_color(self.header, self.pixels),
_ => todo!("X11 visual classes other than TrueColor are not yet supported"),
}
}
}
fn into_png_true_color(
header: XwdHeader,
mut pixels: Vec<u8>,
) -> Result<Vec<u8>, png::EncodingError> {
if header.depth != 24 {
todo!("pixmap color depths other than 24-bit are not yet supported");
}
if !(header.red_mask == 0xff0000
&& header.green_mask == 0x00ff00
&& header.blue_mask == 0x0000ff)
{
todo!("color orderings other than RGB are not yet supported");
}
if header.bits_per_rgb == 32 {
todo!("bits per rgb triplet other than 32 are not yet supported");
}
let mut out = Cursor::new(Vec::<u8>::new());
let mut encoder = png::Encoder::new(&mut out, header.width, header.height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.set_compression(png::Compression::High);
for pixel in pixels.chunks_mut(4) {
let blue = pixel[0];
let green = pixel[1];
let red = pixel[2];
pixel[0] = red;
pixel[1] = green;
pixel[2] = blue;
pixel[3] = 0xFF;
}
let mut writer = encoder.write_header()?;
writer.write_image_data(&pixels)?;
writer.finish()?;
Ok(out.into_inner())
}
#[cfg(test)]
mod tests {
use super::XwdImage;
use insta_image::assert_png_snapshot;
#[test]
fn png_encode() {
let snowlamp_xwd = XwdImage::read(&mut include_bytes!("snowlamp.xwd").into_iter().copied());
assert_png_snapshot!("snowlamp_encoded", snowlamp_xwd.into_png().unwrap());
}
}

View File

@@ -0,0 +1,185 @@
pub mod headless;
use headless::{xvfb::XvfbProcess, DisplayNumberRegistry};
use std::{
env,
ffi::{c_char, c_int, CStr, CString},
ptr::{self, NonNull},
sync::Mutex,
};
use wings_rs::WINGsP::{WMHandleEvent, WMInitializeApplication, WMReleaseApplication, WMScreen};
static APPLICATION_COUNT: Mutex<u32> = Mutex::new(0);
/// Stubbed wrapper for WMApplication.
pub struct HeadlessApplication {
/// The headless X server process.
pub xvfb: XvfbProcess,
/// An open client handle to the headless X server.
pub display: NonNull<x11::xlib::Display>,
/// A fully initialized `WMScreen` for an application running on the
/// headless X server.
pub screen: NonNull<WMScreen>,
}
impl HeadlessApplication {
/// Starts a headless X server and calls `WMInitializeApplication` if no
/// WINGs application is active.
pub fn new() -> Self {
static PROGNAME: &'static CStr = c"Test@eqweq_ewq$eqw";
let xvfb = XvfbProcess::start_default(
DisplayNumberRegistry::next_unused_display()
.expect("cannot allocate a value for DISPLAY"),
)
.expect("cannot start Xvfb");
{
let mut application_count = APPLICATION_COUNT.lock().unwrap();
if *application_count == 0 {
unsafe {
let mut argv: Vec<*mut c_char> =
vec![PROGNAME.as_ptr().cast::<c_char>() as *mut c_char];
let mut argc: c_int = 1;
WMInitializeApplication(
PROGNAME.as_ptr().cast::<c_char>(),
&mut argc as *mut _,
argv.as_mut_ptr(),
);
}
}
*application_count += 1;
}
let display_str = CString::new(format!(":{}", xvfb.display())).unwrap();
let display =
NonNull::new(unsafe { x11::xlib::XOpenDisplay(display_str.as_ptr()) }).unwrap();
let screen = WMScreen::new(&display).unwrap();
HeadlessApplication {
xvfb,
display,
screen,
}
}
/// Pumps the WUtil and X11 event queues, hackily.
///
/// Runs WUtil event handlers that should be run by `now`, pumps the X11
/// event queue, dispatches the next pending event (if any), and runs
/// WUtil idle handlers (if no X11 events are available).
///
/// Returns `true` if more events are pending. (This does not account for
/// WUtils timer events that might need to fire, since the future time when
/// `pump_event_queue` will next be called cannot be known.)
///
/// This is somewhat hacky (because it does not match the WINGs main loop
/// logic exactly), so it should only be used by tests.
pub fn pump_event_queue(&mut self, now: std::time::Instant) -> bool {
let display = self.display.as_ptr();
wutil_rs::handlers::with_global_handlers(|handlers| handlers.check_timer_handlers(now));
unsafe {
x11::xlib::XSync(display, 0);
if x11::xlib::XPending(display) > 0 {
let mut event = x11::xlib::XEvent { type_: 0 };
x11::xlib::XNextEvent(display, &mut event as *mut _);
WMHandleEvent(&mut event as *mut _);
} else {
wutil_rs::handlers::run_global_idle_handlers();
}
x11::xlib::XSync(display, 0);
if x11::xlib::XPending(display) > 0 {
return true;
}
wutil_rs::handlers::with_global_handlers(|handlers| handlers.has_idle_handlers())
}
}
}
impl Drop for HeadlessApplication {
fn drop(&mut self) {
unsafe {
x11::xlib::XCloseDisplay(self.display.as_ptr());
// Leak self.screen, since WINGs doesn't provide a cleanup function.
}
let mut application_count = APPLICATION_COUNT.lock().unwrap();
if *application_count <= 1 {
*application_count = 0;
unsafe {
WMReleaseApplication();
}
} else {
*application_count -= 1;
}
self.xvfb.shutdown().unwrap();
}
}
/// Simple wrapper for demo WINGs applications.
///
/// This handles calling `WMInitializeApplication`, which operates on a
/// singleton behind the scenes and wants to be passed command line arguments.
///
/// When the last instantiated `LiveApplication` is dropped,
/// `WMReleaseApplication` is called automatically.
pub struct LiveApplication {
/// Live X11 `Display` that the application is running on.
pub display: NonNull<x11::xlib::Display>,
/// Fully intialized `WMScreen` for the application.
///
/// This is leaked when the `LiveApplication` is dropped because WINGs does
/// not provide a deletion function.
pub screen: NonNull<WMScreen>,
}
impl LiveApplication {
/// Creates a new application wrapper and initializes the global
/// `WMApplication` if necessary.
pub fn new(name: &str) -> Self {
let mut application_count = APPLICATION_COUNT.lock().unwrap();
if *application_count > 0 {
panic!("application already started!");
}
let name = CString::new(name).expect("invalid program name");
unsafe {
let mut argv: Vec<CString> = vec![name];
let name_ptr = argv[0].as_ptr().cast::<c_char>();
for arg in env::args() {
argv.push(CString::new(arg).expect("invalid argument string"));
}
let mut argv_ptrs: Vec<*mut c_char> = argv
.iter_mut()
.map(|a| a.as_ptr().cast::<c_char>() as *mut c_char)
.collect();
let mut argc: c_int = argv_ptrs.len().try_into().expect("invalid argument count");
WMInitializeApplication(name_ptr, &mut argc as *mut _, argv_ptrs.as_mut_ptr());
}
*application_count = 1;
let display = NonNull::new(unsafe { x11::xlib::XOpenDisplay(ptr::null_mut()) })
.expect("could not connect to X11 display");
let screen = WMScreen::new(&display).unwrap();
LiveApplication { display, screen }
}
}
impl Drop for LiveApplication {
fn drop(&mut self) {
unsafe {
x11::xlib::XCloseDisplay(self.display.as_ptr());
// Leak self.screen, since WINGs doesn't provide a cleanup function.
}
let mut application_count = APPLICATION_COUNT.lock().unwrap();
if *application_count <= 1 {
*application_count = 0;
unsafe {
WMReleaseApplication();
}
} else {
*application_count -= 1;
}
}
}

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["staticlib"]
crate-type = ["staticlib", "rlib"]
[dependencies]
libc = "0.2.177"

View File

@@ -13,7 +13,15 @@ RUST_EXTRA = \
Cargo.toml
src/WINGsP.rs: ../WINGs/WINGsP.h ../../wrlib/wraster.h ../WINGs/WINGs.h ../WINGs/WUtil.h Makefile patch_WINGsP.sh
$(BINDGEN) ../WINGs/WINGsP.h --ignore-functions --allowlist-type "^W_.+|^WM(View|Array|DragOperationType|Point|Data|OpenPanel|SavePanel|HashTable|DraggingInfo|SelectionProcs|Rect|EventProc|Widget|Size|Color|Pixmap|FilePanel)|R(Context|ContextAttributes|Image|RenderingMode|ScalingFilter|StdColormapMode|ImageFormat|Color)|_WINGsConfiguration" --no-recursive-allowlist -o src/WINGsP.rs -- @PANGO_CFLAGS@ -I../../wrlib -I.. && ./patch_WINGsP.sh src/WINGsP.rs
$(BINDGEN) ../WINGs/WINGsP.h \
--no-recursive-allowlist \
--allowlist-type "^W_.+|^WM(View|Array|DragOperationType|Point|Data|OpenPanel|SavePanel|HashTable|DraggingInfo|SelectionProcs|Rect|EventProc|Widget|Size|Color|Pixmap|FilePanel)|R(Context|ContextAttributes|Image|RenderingMode|ScalingFilter|StdColormapMode|ImageFormat|Color)|_WINGsConfiguration" \
--allowlist-type "^WM(FontPanel|Screen|Button)" \
--allowlist-function "^WMCreateScreen|^WM(Get|Show)FontPanel|^WMCreateCommandButton|^WM(Initialize|Release)Application|^WMScreenMainLoop|^WMHandleEvent" \
-o src/WINGsP.rs -- \
@PANGO_CFLAGS@ \
-I../../wrlib \
-I.. && ./patch_WINGsP.sh src/WINGsP.rs
target/debug/libwings_rs.a: $(RUST_SOURCES) $(RUST_EXTRA)
$(CARGO) build

View File

@@ -1,15 +1,20 @@
use crate::{
WINGsP::W_Screen,
WINGsP::{WMScreen, WMCreateScreen},
font::{Font, FontName},
};
use std::{
collections::{HashMap, hash_map::Entry},
ffi::c_void,
ptr::NonNull,
rc::Rc,
};
impl W_Screen {
impl WMScreen {
pub fn new(display: &NonNull<x11::xlib::Display>) -> Option<NonNull<Self>> {
NonNull::new(unsafe { WMCreateScreen(display.as_ptr(), x11::xlib::XDefaultScreen(display.as_ptr())) })
}
fn font_cache_mut(&mut self) -> &mut HashMap<FontName, Rc<Font>> {
if self.fontCache.is_null() {
self.fontCache = (Box::leak(Box::new(HashMap::<FontName, Rc<Font>>::new()))
@@ -50,13 +55,13 @@ impl W_Screen {
#[cfg(test)]
mod test {
use crate::WINGsP::W_Screen;
use crate::WINGsP::WMScreen;
use std::mem::MaybeUninit;
#[test]
fn font_cache_init() {
let mut screen: W_Screen = unsafe { MaybeUninit::zeroed().assume_init() };
let mut screen: WMScreen = unsafe { MaybeUninit::zeroed().assume_init() };
let cache = screen.font_cache_mut();
assert!(cache.is_empty());
}

View File

@@ -933,9 +933,9 @@ AC_CONFIG_FILES(
wutil-rs/Makefile
dnl WINGs toolkit
WINGs/Makefile WINGs/wings-rs/Makefile WINGs/WINGs/Makefile WINGs/po/Makefile
WINGs/Documentation/Makefile WINGs/Resources/Makefile WINGs/Extras/Makefile
WINGs/Examples/Makefile WINGs/Tests/Makefile
WINGs/Makefile WINGs/wings-rs/Makefile WINGs/WINGs/Makefile WINGs/wings-rs-tests/Makefile
WINGs/po/Makefile WINGs/Documentation/Makefile WINGs/Resources/Makefile
WINGs/Extras/Makefile WINGs/Examples/Makefile WINGs/Tests/Makefile
dnl Rust implementation of Window Maker core
wmaker-rs/Makefile

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["staticlib"]
crate-type = ["staticlib","rlib"]
[build-dependencies]
cc = "1.0"

View File

@@ -158,6 +158,11 @@ impl HandlerQueues {
QueueStatus::Pending
}
}
/// Returns `true` if any idle handlers need to be run.
pub fn has_idle_handlers(&self) -> bool {
!self.idle_handlers.is_empty()
}
}
/// Runs each of `handlers` that should run before `now` and returns those that
@@ -209,11 +214,11 @@ struct IdleHandler {
static DEFAULT_HANDLERS: Mutex<HandlerQueues> = Mutex::new(HandlerQueues::new());
fn with_global_handlers<R>(f: impl FnOnce(&mut HandlerQueues) -> R) -> R {
pub fn with_global_handlers<R>(f: impl FnOnce(&mut HandlerQueues) -> R) -> R {
f(&mut DEFAULT_HANDLERS.try_lock().unwrap())
}
fn run_global_idle_handlers() -> QueueStatus {
pub fn run_global_idle_handlers() -> QueueStatus {
// Move handlers out of the global queue because it is locked while
// with_global_handlers is running, and a callback may try to schedule
// another callback or perform some other operation on the global queue.
@@ -226,10 +231,10 @@ fn run_global_idle_handlers() -> QueueStatus {
(h.callback)();
}
with_global_handlers(|handlers| {
if handlers.idle_handlers.is_empty() {
QueueStatus::Empty
} else {
if handlers.has_idle_handlers() {
QueueStatus::Pending
} else {
QueueStatus::Empty
}
})
}