wayland: we have a (blank) window ladies and gentlemen

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id2b91339a2d43cc95041dafe835c6a526a6a6964
This commit is contained in:
raf 2026-06-23 15:02:40 +03:00
commit c68d3445e7
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
4 changed files with 649 additions and 1 deletions

258
src/wayland.rs Normal file
View file

@ -0,0 +1,258 @@
//! Wayland front-end: connection, surface, and software-drawn window.
//!
//! Uses smithay-client-toolkit for protocol boilerplate and calloop for the
//! event loop, so the PTY master fd and timers share one loop.
use std::time::Duration;
use anyhow::Context;
use calloop::EventLoop;
use calloop_wayland_source::WaylandSource;
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
delegate_compositor, delegate_output, delegate_registry, delegate_seat, delegate_shm,
delegate_xdg_shell, delegate_xdg_window,
output::{OutputHandler, OutputState},
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::{Capability, SeatHandler, SeatState},
shell::{
WaylandSurface,
xdg::{
XdgShell,
window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
},
},
shm::{Shm, ShmHandler, slot::SlotPool},
};
use wayland_client::{
Connection, QueueHandle,
globals::registry_queue_init,
protocol::{wl_output, wl_seat, wl_shm, wl_surface},
};
/// Default window size in pixels before the compositor suggests one.
const DEFAULT_W: u32 = 800;
const DEFAULT_H: u32 = 600;
/// Background fill, 0xAARRGGBB. Foot-ish dark grey.
const BG: u32 = 0xFF18_1818;
/// Run a single window until it is closed.
pub fn run() -> anyhow::Result<()> {
let conn = Connection::connect_to_env().context("connect to Wayland compositor")?;
let (globals, event_queue) =
registry_queue_init(&conn).context("initialize Wayland registry")?;
let qh = event_queue.handle();
let mut event_loop: EventLoop<App> =
EventLoop::try_new().context("create calloop event loop")?;
WaylandSource::new(conn, event_queue)
.insert(event_loop.handle())
.map_err(|e| anyhow::anyhow!("insert Wayland source into event loop: {e}"))?;
let compositor = CompositorState::bind(&globals, &qh).context("compositor not available")?;
let xdg_shell = XdgShell::bind(&globals, &qh).context("xdg_wm_base not available")?;
let shm = Shm::bind(&globals, &qh).context("wl_shm not available")?;
let surface = compositor.create_surface(&qh);
let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh);
window.set_title("beer");
window.set_app_id("dev.notashelf.beer");
window.set_min_size(Some((1, 1)));
// First commit with no buffer kicks off the initial configure.
window.commit();
let pool = SlotPool::new(DEFAULT_W as usize * DEFAULT_H as usize * 4, &shm)
.context("create shm slot pool")?;
let mut app = App {
registry_state: RegistryState::new(&globals),
output_state: OutputState::new(&globals, &qh),
seat_state: SeatState::new(&globals, &qh),
shm,
pool,
window,
width: DEFAULT_W,
height: DEFAULT_H,
configured: false,
exit: false,
};
while !app.exit {
event_loop
.dispatch(Duration::from_millis(16), &mut app)
.context("dispatch event loop")?;
}
Ok(())
}
/// Window + Wayland client state shared across all protocol handlers.
#[derive(Debug)]
struct App {
registry_state: RegistryState,
output_state: OutputState,
seat_state: SeatState,
shm: Shm,
pool: SlotPool,
window: Window,
width: u32,
height: u32,
configured: bool,
exit: bool,
}
impl App {
/// Fill the surface with the background colour and present it.
fn draw(&mut self) {
let (w, h) = (self.width, self.height);
let stride = w as i32 * 4;
let (buffer, canvas) =
match self
.pool
.create_buffer(w as i32, h as i32, stride, wl_shm::Format::Argb8888)
{
Ok(buf) => buf,
Err(err) => {
tracing::error!("allocate shm buffer: {err}");
return;
}
};
let bytes = BG.to_le_bytes();
for px in canvas.chunks_exact_mut(4) {
px.copy_from_slice(&bytes);
}
let surface = self.window.wl_surface();
if let Err(err) = buffer.attach_to(surface) {
tracing::error!("attach buffer: {err}");
return;
}
surface.damage_buffer(0, 0, w as i32, h as i32);
self.window.commit();
}
}
impl CompositorHandler for App {
fn scale_factor_changed(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: i32,
) {
}
fn transform_changed(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: wl_output::Transform,
) {
}
fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {}
fn surface_enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: &wl_output::WlOutput,
) {
}
fn surface_leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: &wl_output::WlOutput,
) {
}
}
impl WindowHandler for App {
fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &Window) {
self.exit = true;
}
fn configure(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &Window,
configure: WindowConfigure,
_serial: u32,
) {
if let (Some(w), Some(h)) = configure.new_size {
self.width = w.get();
self.height = h.get();
}
self.configured = true;
self.draw();
}
}
impl ShmHandler for App {
fn shm_state(&mut self) -> &mut Shm {
&mut self.shm
}
}
impl SeatHandler for App {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
fn new_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
_: Capability,
) {
}
fn remove_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
_: Capability,
) {
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
}
impl OutputHandler for App {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
}
impl ProvidesRegistryState for App {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
delegate_compositor!(App);
delegate_output!(App);
delegate_shm!(App);
delegate_seat!(App);
delegate_xdg_shell!(App);
delegate_xdg_window!(App);
delegate_registry!(App);