// Linux Wayland on wlroots desktops (sway, Hyprland, river, labwc, Wayfire…), pure Rust through // the compositor's own protocols: // screenshots wlr-screencopy (every screen, placed where it is in the layout) // mouse wlr-virtual-pointer (absolute moves over the whole layout) // keyboard virtual-keyboard: AnyBot hands the compositor a small keymap with exactly the // keys it needs, so any character (Chinese too) types directly, like `wtype` // windows wlr-foreign-toplevel (which app is in front; Wayland gives no window places) // idle time ext-idle-notify (a thread that hears "idle" / "back") // GNOME and KDE don't offer these to apps; desk.rs uses the desktop portals there (portal.rs). use std::io::Write as _; use std::os::fd::AsFd; use std::sync::Mutex; use std::time::{Duration, Instant}; use wayland_client::globals::{registry_queue_init, GlobalListContents}; use wayland_client::protocol::{wl_buffer, wl_output, wl_pointer, wl_registry, wl_seat, wl_shm, wl_shm_pool}; use wayland_client::{delegate_noop, event_created_child, Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum}; use wayland_protocols::ext::idle_notify::v1::client::{ext_idle_notification_v1, ext_idle_notifier_v1}; use wayland_protocols::xdg::xdg_output::zv1::client::{zxdg_output_manager_v1, zxdg_output_v1}; use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{zwp_virtual_keyboard_manager_v1, zwp_virtual_keyboard_v1}; use wayland_protocols_wlr::foreign_toplevel::v1::client::{zwlr_foreign_toplevel_handle_v1 as top_handle, zwlr_foreign_toplevel_manager_v1 as top_manager}; use wayland_protocols_wlr::screencopy::v1::client::{zwlr_screencopy_frame_v1 as frame_v1, zwlr_screencopy_manager_v1}; use wayland_protocols_wlr::virtual_pointer::v1::client::{zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1}; #[derive(Default, Clone)] struct Out { name: u32, out: Option, // Logical place and size (xdg-output when there is one, else from the mode and scale). x: i32, y: i32, lw: i32, lh: i32, logical: bool, mode_w: i32, mode_h: i32, scale: i32, } impl Out { fn rect(&self) -> (i32, i32, i32, i32) { if self.logical && self.lw > 0 { (self.x, self.y, self.lw, self.lh) } else { let s = self.scale.max(1); (self.x, self.y, self.mode_w / s, self.mode_h / s) } } } struct Top { id: wayland_client::backend::ObjectId, app: String, active: bool, } #[derive(Default)] struct Frame { buffer: Option<(WEnum, u32, u32, u32)>, buffer_done: bool, ready: bool, failed: bool, y_invert: bool, } #[derive(Default)] struct State { outs: Vec, tops: Vec, frame: Frame, } struct Wl { conn: Connection, queue: EventQueue, qh: QueueHandle, st: State, shm: wl_shm::WlShm, copy: zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, copy_version: u32, pointer: zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1, kb: zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1, /// Keysyms on our keymap: index i is evdev key i + 1 (xkb keycode i + 9). keys: Vec, mods: u32, start: Instant, } static WL: Mutex> = Mutex::new(None); /// The four modifiers always sit on the first keys, with their xkb modifier bits. const MODS: [(u32, u32, &str); 4] = [(0xffe1, 1, "Shift"), (0xffe3, 4, "Control"), (0xffe9, 8, "Mod1"), (0xffeb, 64, "Mod4")]; /// Ask the kernel to give up a read on the compositor socket after `t`. A compositor that /// stops answering used to block `blocking_dispatch` forever while the app's global Wayland /// lock is held — every screen action (and the UI on top of it) froze. The timed-out read /// arrives as an error, so the connection is treated as dead and rebuilt on the next call. fn set_read_timeout(fd: std::os::fd::RawFd, t: Duration) -> std::io::Result<()> { let tv = libc::timeval { tv_sec: i64::try_from(t.as_secs()).unwrap_or(i64::MAX), tv_usec: 0 }; let rc = unsafe { libc::setsockopt(fd, libc::SOL_SOCKET, libc::SO_RCVTIMEO, &tv as *const _ as *const libc::c_void, std::mem::size_of::() as libc::socklen_t) }; if rc == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) } } fn connect() -> Option { let conn = Connection::connect_to_env().ok()?; // The compositor's socket: let the kernel give up on reads after 60 s (see // set_read_timeout). Timed-out reads arrive as errors, so the connection is treated // as dead and rebuilt on the next call. use std::os::fd::AsRawFd as _; set_read_timeout(conn.as_fd().as_raw_fd(), Duration::from_secs(60)).ok()?; let (globals, mut queue) = registry_queue_init::(&conn).ok()?; let qh = queue.handle(); let shm: wl_shm::WlShm = globals.bind(&qh, 1..=1, ()).ok()?; let seat: wl_seat::WlSeat = globals.bind(&qh, 1..=1, ()).ok()?; let copy: zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1 = globals.bind(&qh, 1..=3, ()).ok()?; let copy_version = copy.version(); let pm: zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 = globals.bind(&qh, 1..=2, ()).ok()?; let km: zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1 = globals.bind(&qh, 1..=1, ()).ok()?; let pointer = pm.create_virtual_pointer(Some(&seat), &qh, ()); let kb = km.create_virtual_keyboard(&seat, &qh, ()); let xdg: Option = globals.bind(&qh, 1..=3, ()).ok(); let _tops: Option = globals.bind(&qh, 1..=3, ()).ok(); let mut st = State::default(); for g in globals.contents().clone_list() { if g.interface == "wl_output" { let out: wl_output::WlOutput = globals.registry().bind(g.name, g.version.min(4), &qh, g.name); if let Some(x) = &xdg { x.get_xdg_output(&out, &qh, g.name); } st.outs.push(Out { name: g.name, out: Some(out), scale: 1, ..Default::default() }); } } queue.roundtrip(&mut st).ok()?; queue.roundtrip(&mut st).ok()?; if st.outs.is_empty() { return None; } let mut wl = Wl { conn, queue, qh, st, shm, copy, copy_version, pointer, kb, keys: MODS.iter().map(|m| m.0).collect(), mods: 0, start: Instant::now() }; wl.upload_keymap().ok()?; Some(wl) } fn with(f: impl FnOnce(&mut Wl) -> Option) -> Option { let mut g = WL.lock().unwrap_or_else(|e| e.into_inner()); if g.is_none() { *g = connect(); } let wl = g.as_mut()?; let _ = wl.queue.dispatch_pending(&mut wl.st); let r = f(wl); if wl.conn.flush().is_err() || wl.queue.roundtrip(&mut wl.st).is_err() { // The compositor went away: connect again next time. *g = None; } r } /// This compositor lets AnyBot see and use the screen (screencopy, virtual pointer and keyboard). pub fn available() -> bool { with(|_| Some(())).is_some() } /// A file only we can see, gone from the folder already (for shared memory and keymaps). fn scratch_file(len: u64) -> std::io::Result { let dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from).unwrap_or_else(std::env::temp_dir); let path = dir.join(format!("anybot-wl-{}-{}", std::process::id(), crate::model::now_ms())); let f = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(&path)?; let _ = std::fs::remove_file(&path); f.set_len(len)?; Ok(f) } /// The keymap text: one key per keysym we use; the first four are the modifiers. pub fn keymap_text(keys: &[u32]) -> String { let mut s = String::from("xkb_keymap {\nxkb_keycodes \"anybot\" {\n minimum = 8;\n"); s += &format!(" maximum = {};\n", 9 + keys.len().max(1)); for i in 0..keys.len() { s += &format!(" = {};\n", i + 9); } s += "};\nxkb_types \"anybot\" { include \"complete\" };\nxkb_compat \"anybot\" { include \"complete\" };\nxkb_symbols \"anybot\" {\n"; for (i, k) in keys.iter().enumerate() { s += &format!(" key {{ [ 0x{k:x} ] }};\n"); } for (i, (_, _, name)) in MODS.iter().enumerate() { s += &format!(" modifier_map {name} {{ }};\n"); } s += "};\n};\n"; s } impl Wl { fn time(&self) -> u32 { self.start.elapsed().as_millis() as u32 } fn upload_keymap(&mut self) -> std::io::Result<()> { let mut text = keymap_text(&self.keys).into_bytes(); text.push(0); let mut f = scratch_file(0)?; f.write_all(&text)?; self.kb.keymap(1, f.as_fd(), text.len() as u32); let _ = self.conn.flush(); Ok(()) } /// The evdev key that types `sym`, adding it to our keymap when it's new. fn key_for(&mut self, sym: u32) -> u32 { if let Some(i) = self.keys.iter().position(|k| *k == sym) { return i as u32 + 1; } if self.keys.len() > 200 { self.keys.truncate(MODS.len()); } self.keys.push(sym); let _ = self.upload_keymap(); let _ = self.queue.roundtrip(&mut self.st); self.keys.len() as u32 } fn key(&mut self, sym: u32, press: bool) { let code = self.key_for(sym); let t = self.time(); self.kb.key(t, code, press as u32); if let Some((_, bit, _)) = MODS.iter().find(|m| m.0 == sym) { self.mods = if press { self.mods | bit } else { self.mods & !bit }; self.kb.modifiers(self.mods, 0, 0, 0); } } /// The whole layout: (left, top, width, height) in logical points. fn layout(&self) -> (i32, i32, i32, i32) { let rects: Vec<_> = self.st.outs.iter().map(|o| o.rect()).collect(); let x0 = rects.iter().map(|r| r.0).min().unwrap_or(0); let y0 = rects.iter().map(|r| r.1).min().unwrap_or(0); let x1 = rects.iter().map(|r| r.0 + r.2).max().unwrap_or(0); let y1 = rects.iter().map(|r| r.1 + r.3).max().unwrap_or(0); (x0, y0, (x1 - x0).max(1), (y1 - y0).max(1)) } fn pixel_scale(&self) -> i32 { self.st.outs.iter().map(|o| o.scale.max(1)).max().unwrap_or(1) } fn capture_output(&mut self, out: &wl_output::WlOutput) -> Option { self.st.frame = Frame::default(); let frame = self.copy.capture_output(0, out, &self.qh, ()); // Every early exit passes through here, so the frame (and its buffer/pool) are always // destroyed: failure paths used to leak compositor-side objects on every failed grab. let r = self.grab(&frame); frame.destroy(); r } fn grab(&mut self, frame: &frame_v1::ZwlrScreencopyFrameV1) -> Option { let deadline = Instant::now() + Duration::from_secs(5); while !(self.st.frame.failed || (self.st.frame.buffer.is_some() && (self.st.frame.buffer_done || self.copy_version < 3))) { if Instant::now() > deadline { return None; } self.queue.blocking_dispatch(&mut self.st).ok()?; } let (format, w, h, stride) = self.st.frame.buffer?; let WEnum::Value(fmt) = format else { return None }; let size = (stride as u64) * (h as u64); let file = scratch_file(size).ok()?; let pool = self.shm.create_pool(file.as_fd(), size as i32, &self.qh, ()); let buffer = pool.create_buffer(0, w as i32, h as i32, stride as i32, fmt, &self.qh, ()); frame.copy(&buffer); while !(self.st.frame.ready || self.st.frame.failed) { if Instant::now() > deadline { break; } self.queue.blocking_dispatch(&mut self.st).ok()?; } let ok = self.st.frame.ready; let y_invert = self.st.frame.y_invert; buffer.destroy(); pool.destroy(); if !ok { return None; } let map = unsafe { memmap2::Mmap::map(&file) }.ok()?; to_rgba(&map, fmt, w, h, stride, y_invert) } fn capture(&mut self) -> Option { let (x0, y0, lw, lh) = self.layout(); let s = self.pixel_scale(); let mut canvas = image::RgbaImage::new((lw * s) as u32, (lh * s) as u32); let outs = self.st.outs.clone(); let mut any = false; for o in outs { let Some(out) = &o.out else { continue }; let Some(mut img) = self.capture_output(out) else { continue }; let (ox, oy, ow, oh) = o.rect(); let (tw, th) = ((ow * s) as u32, (oh * s) as u32); if img.dimensions() != (tw, th) && tw > 0 && th > 0 { img = image::imageops::resize(&img, tw, th, image::imageops::FilterType::Triangle); } image::imageops::replace(&mut canvas, &img, ((ox - x0) * s) as i64, ((oy - y0) * s) as i64); any = true; } any.then(|| image::DynamicImage::ImageRgba8(canvas).to_rgb8()) } } /// Shared-memory pixels → RGBA. The 8-bit formats every compositor offers. pub fn to_rgba(data: &[u8], fmt: wl_shm::Format, w: u32, h: u32, stride: u32, y_invert: bool) -> Option { // In memory (little-endian): XRGB/ARGB = B G R X; XBGR/ABGR = R G B X. let bgr = match fmt { wl_shm::Format::Xrgb8888 | wl_shm::Format::Argb8888 => true, wl_shm::Format::Xbgr8888 | wl_shm::Format::Abgr8888 => false, _ => return None, }; // Byte maths in usize: extreme resolutions overflow a u32 product (debug builds panic). let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4); for row in 0..h { let r = if y_invert { h - 1 - row } else { row }; let (a, b) = ((r as u64 * stride as u64) as usize, ((r as u64 * stride as u64) as usize) + (w as usize) * 4); let line = data.get(a..b)?; for px in line.chunks_exact(4) { if bgr { out.extend_from_slice(&[px[2], px[1], px[0], 255]); } else { out.extend_from_slice(&[px[0], px[1], px[2], 255]); } } } image::RgbaImage::from_raw(w, h, out) } // ---------- what desk.rs calls ---------- pub fn capture() -> Result { with(|wl| wl.capture()).ok_or_else(|| "讀不到螢幕畫面(Wayland 截圖失敗)".to_string()) } /// The layout in logical points (where the mouse goes). pub fn screen_points() -> (f64, f64) { with(|wl| { let (_, _, w, h) = wl.layout(); Some((w as f64, h as f64)) }) .unwrap_or((0.0, 0.0)) } /// The screenshot's size in pixels. pub fn screen_pixels() -> (u32, u32) { with(|wl| { let (_, _, w, h) = wl.layout(); let s = wl.pixel_scale(); Some(((w * s) as u32, (h * s) as u32)) }) .unwrap_or((0, 0)) } pub fn motion(px: f64, py: f64) { with(|wl| { let (_, _, w, h) = wl.layout(); let t = wl.time(); let x = px.round().clamp(0.0, (w - 1) as f64) as u32; let y = py.round().clamp(0.0, (h - 1) as f64) as u32; wl.pointer.motion_absolute(t, x, y, w as u32, h as u32); wl.pointer.frame(); Some(()) }); } /// 1 left, 2 middle, 3 right (the X numbering desk.rs uses). pub fn button(b: u8, press: bool) { let code = match b { 3 => 0x111, 2 => 0x112, _ => 0x110, }; with(|wl| { let t = wl.time(); wl.pointer.button(t, code, if press { wl_pointer::ButtonState::Pressed } else { wl_pointer::ButtonState::Released }); wl.pointer.frame(); Some(()) }); } /// Wheel clicks: positive `up` scrolls up, positive `left` scrolls left (like X buttons 4 and 6). pub fn wheel(up: i32, left: i32) { with(|wl| { for (n, axis) in [(up, wl_pointer::Axis::VerticalScroll), (left, wl_pointer::Axis::HorizontalScroll)] { if n == 0 { continue; } let t = wl.time(); wl.pointer.axis_source(wl_pointer::AxisSource::Wheel); wl.pointer.axis_discrete(t, axis, -15.0 * n as f64, -n); wl.pointer.frame(); } Some(()) }); } pub fn key(sym: u32, press: bool) { with(|wl| { wl.key(sym, press); Some(()) }); } /// Any text, Chinese too: each character's keysym goes on our keymap and is pressed. pub fn type_text(text: &str) { for c in text.chars() { let sym = crate::x11::keysym_for_char(c); with(|wl| { wl.key(sym, true); Some(()) }); std::thread::sleep(Duration::from_millis(8)); with(|wl| { wl.key(sym, false); Some(()) }); std::thread::sleep(Duration::from_millis(8)); } } /// The app in front (its Wayland app id, e.g. "firefox", "foot"). pub fn front_app() -> Option<(String, i32)> { with(|wl| wl.st.tops.iter().find(|t| t.active && !t.app.is_empty()).map(|t| (t.app.clone(), 0))) } // ---------- idle time ---------- static IDLE: Mutex>> = Mutex::new(None); struct IdleState; /// Seconds since the last input, from ext-idle-notify (a thread listening with a 1 s threshold). /// None when the compositor doesn't offer it. pub fn idle_secs() -> Option { { let g = IDLE.lock().unwrap_or_else(|e| e.into_inner()); if let Some(since) = *g { return Some(since.map(|t| t.elapsed().as_secs_f64() + 1.0).unwrap_or(0.0)); } } static STARTED: std::sync::Once = std::sync::Once::new(); STARTED.call_once(|| { std::thread::spawn(|| { // Keep listening for the app's whole life: when the connection drops (compositor // restart, read timeout) reconnect and listen again — the old loop quit forever // after one break and idle_secs() went blind. loop { let Ok(conn) = Connection::connect_to_env() else { return }; let Ok((globals, mut queue)) = registry_queue_init::(&conn) else { return }; let qh = queue.handle(); let Ok(seat) = globals.bind::(&qh, 1..=1, ()) else { return }; let Ok(notifier) = globals.bind::(&qh, 1..=1, ()) else { return }; // Keep the notification object alive for as long as the loop runs: wayland-client // destroys an object when its last handle drops, and dropping this return value // here meant Idled events never arrived (idle_secs() was forever 0). let _notification = notifier.get_idle_notification(1000, &seat, &qh, ()); *IDLE.lock().unwrap_or_else(|e| e.into_inner()) = Some(None); let mut st = IdleState; while queue.blocking_dispatch(&mut st).is_ok() {} *IDLE.lock().unwrap_or_else(|e| e.into_inner()) = None; std::thread::sleep(Duration::from_secs(2)); } }); }); None } impl Dispatch for IdleState { fn event(_: &mut Self, _: &ext_idle_notification_v1::ExtIdleNotificationV1, event: ext_idle_notification_v1::Event, _: &(), _: &Connection, _: &QueueHandle) { let v = match event { ext_idle_notification_v1::Event::Idled => Some(Instant::now()), _ => None, }; *IDLE.lock().unwrap_or_else(|e| e.into_inner()) = Some(v); } } impl Dispatch for IdleState { fn event(_: &mut Self, _: &wl_registry::WlRegistry, _: wl_registry::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle) {} } delegate_noop!(IdleState: ignore wl_seat::WlSeat); delegate_noop!(IdleState: ignore ext_idle_notifier_v1::ExtIdleNotifierV1); // ---------- events ---------- impl Dispatch for State { fn event(_: &mut Self, _: &wl_registry::WlRegistry, _: wl_registry::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle) {} } impl Dispatch for State { fn event(st: &mut Self, _: &wl_output::WlOutput, event: wl_output::Event, name: &u32, _: &Connection, _: &QueueHandle) { let Some(o) = st.outs.iter_mut().find(|o| o.name == *name) else { return }; match event { wl_output::Event::Geometry { x, y, .. } if !o.logical => { o.x = x; o.y = y; } wl_output::Event::Mode { flags: WEnum::Value(f), width, height, .. } if f.contains(wl_output::Mode::Current) => { o.mode_w = width; o.mode_h = height; } wl_output::Event::Scale { factor } => o.scale = factor.max(1), _ => {} } } } impl Dispatch for State { fn event(st: &mut Self, _: &zxdg_output_v1::ZxdgOutputV1, event: zxdg_output_v1::Event, name: &u32, _: &Connection, _: &QueueHandle) { let Some(o) = st.outs.iter_mut().find(|o| o.name == *name) else { return }; match event { zxdg_output_v1::Event::LogicalPosition { x, y } => { o.x = x; o.y = y; o.logical = true; } zxdg_output_v1::Event::LogicalSize { width, height } => { o.lw = width; o.lh = height; o.logical = true; } _ => {} } } } impl Dispatch for State { fn event(st: &mut Self, _: &frame_v1::ZwlrScreencopyFrameV1, event: frame_v1::Event, _: &(), _: &Connection, _: &QueueHandle) { let f = &mut st.frame; match event { frame_v1::Event::Buffer { format, width, height, stride } => { // Prefer a format we can read when several are offered. let readable = matches!(format, WEnum::Value(wl_shm::Format::Xrgb8888 | wl_shm::Format::Argb8888 | wl_shm::Format::Xbgr8888 | wl_shm::Format::Abgr8888)); if f.buffer.is_none() || readable { f.buffer = Some((format, width, height, stride)); } } frame_v1::Event::BufferDone => f.buffer_done = true, frame_v1::Event::Flags { flags: WEnum::Value(fl) } => f.y_invert = fl.contains(frame_v1::Flags::YInvert), frame_v1::Event::Ready { .. } => f.ready = true, frame_v1::Event::Failed => f.failed = true, _ => {} } } } impl Dispatch for State { fn event(st: &mut Self, _: &top_manager::ZwlrForeignToplevelManagerV1, event: top_manager::Event, _: &(), _: &Connection, _: &QueueHandle) { if let top_manager::Event::Toplevel { toplevel } = event { st.tops.push(Top { id: toplevel.id(), app: String::new(), active: false }); } } event_created_child!(State, top_manager::ZwlrForeignToplevelManagerV1, [ top_manager::EVT_TOPLEVEL_OPCODE => (top_handle::ZwlrForeignToplevelHandleV1, ()), ]); } impl Dispatch for State { fn event(st: &mut Self, h: &top_handle::ZwlrForeignToplevelHandleV1, event: top_handle::Event, _: &(), _: &Connection, _: &QueueHandle) { let id = h.id(); match event { top_handle::Event::Closed => st.tops.retain(|t| t.id != id), top_handle::Event::AppId { app_id } => { if let Some(t) = st.tops.iter_mut().find(|t| t.id == id) { t.app = app_id; } } top_handle::Event::State { state } => { let active = state.chunks_exact(4).any(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]) == top_handle::State::Activated as u32); if let Some(t) = st.tops.iter_mut().find(|t| t.id == id) { t.active = active; } } _ => {} } } } delegate_noop!(State: ignore wl_shm::WlShm); delegate_noop!(State: ignore wl_shm_pool::WlShmPool); delegate_noop!(State: ignore wl_buffer::WlBuffer); delegate_noop!(State: ignore wl_seat::WlSeat); delegate_noop!(State: ignore zxdg_output_manager_v1::ZxdgOutputManagerV1); delegate_noop!(State: ignore zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1); delegate_noop!(State: ignore zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1); delegate_noop!(State: ignore zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1); delegate_noop!(State: ignore zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1); delegate_noop!(State: ignore zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1); #[cfg(test)] mod tests { use super::*; #[test] fn keymap_and_pixels() { let k = keymap_text(&[0xffe1, 0xffe3, 0xffe9, 0xffeb, 0x1004f60]); assert!(k.contains(" = 13;")); assert!(k.contains("key { [ 0x1004f60 ] };")); assert!(k.contains("modifier_map Control { };")); // One BGRX pixel, upside down flag on a 1×2 image. let px = to_rgba(&[1, 2, 3, 0, 4, 5, 6, 0], wl_shm::Format::Xrgb8888, 1, 2, 4, true).unwrap(); assert_eq!(px.get_pixel(0, 0).0, [6, 5, 4, 255]); let px = to_rgba(&[1, 2, 3, 0], wl_shm::Format::Abgr8888, 1, 1, 4, false).unwrap(); assert_eq!(px.get_pixel(0, 0).0, [1, 2, 3, 255]); } }