git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitb4160d05d492064096fe262b6f8b7bd295670611
parentbc24779ad1
authorLucas Galante <[email protected]>
date2026-05-23 19:27
feat: Implement window title decorations on borders

 src/decorations.rs | 383 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs         |   1 +
 src/wayland.rs     | 103 +++++++++++++-
 3 files changed, 485 insertions(+), 2 deletions(-)

diff --git a/src/decorations.rs b/src/decorations.rs
new file mode 100644
index 0000000..b5328dc
--- /dev/null
+++ b/src/decorations.rs
@@ -0,0 +1,383 @@
+// Wayland decoration surface drawing and management for clearwm
+
+use std::ffi::CString;
+use std::os::fd::RawFd;
+use std::ptr;
+use wayland_client::protocol::{wl_buffer, wl_shm, wl_shm_pool, wl_surface};
+use wayland_client::QueueHandle;
+
+use crate::protocol::river_window_management::client::river_decoration_v1::RiverDecorationV1;
+use crate::wayland::AppState;
+
+/// Struct tracking the Wayland decoration resources for a window.
+pub struct WindowDecoration {
+    pub surface: wl_surface::WlSurface,
+    pub decoration: RiverDecorationV1,
+    pub buffer: Option<wl_buffer::WlBuffer>,
+    pub pool: Option<wl_shm_pool::WlShmPool>,
+    pub width: i32,
+    pub height: i32,
+    pub mapped_data: *mut u32,
+    pub mapped_size: usize,
+}
+
+impl Drop for WindowDecoration {
+    fn drop(&mut self) {
+        if !self.mapped_data.is_null() {
+            unsafe {
+                libc::munmap(self.mapped_data as *mut libc::c_void, self.mapped_size);
+            }
+        }
+    }
+}
+
+// Standard 8x8 bitmap font (IBM CP437 ASCII printable characters 32..=126)
+const FONT_DATA: &[u8; 760] = &[
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //   (0x20)
+    0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x00, // ! (0x21)
+    0x24, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, // " (0x22)
+    0x24, 0x24, 0x7e, 0x24, 0x7e, 0x24, 0x24, 0x00, // # (0x23)
+    0x08, 0x3e, 0x28, 0x3e, 0x0a, 0x3e, 0x08, 0x00, // $ (0x24)
+    0x00, 0x62, 0x64, 0x08, 0x13, 0x23, 0x00, 0x00, // % (0x25)
+    0x18, 0x24, 0x18, 0x2a, 0x24, 0x24, 0x1b, 0x00, // & (0x26)
+    0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ' (0x27)
+    0x08, 0x10, 0x20, 0x20, 0x20, 0x10, 0x08, 0x00, // ( (0x28)
+    0x10, 0x08, 0x04, 0x04, 0x04, 0x08, 0x10, 0x00, // ) (0x29)
+    0x00, 0x24, 0x18, 0x3c, 0x18, 0x24, 0x00, 0x00, // * (0x2a)
+    0x00, 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, 0x00, // + (0x2b)
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x10, // , (0x2c)
+    0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, // - (0x2d)
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, // . (0x2e)
+    0x00, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, // / (0x2f)
+    0x3c, 0x46, 0x4a, 0x52, 0x62, 0x3c, 0x00, 0x00, // 0 (0x30)
+    0x18, 0x28, 0x08, 0x08, 0x08, 0x3e, 0x00, 0x00, // 1 (0x31)
+    0x3c, 0x42, 0x02, 0x3c, 0x40, 0x7e, 0x00, 0x00, // 2 (0x32)
+    0x3c, 0x42, 0x1c, 0x02, 0x42, 0x3c, 0x00, 0x00, // 3 (0x33)
+    0x08, 0x18, 0x28, 0x48, 0x7e, 0x08, 0x00, 0x00, // 4 (0x34)
+    0x7e, 0x40, 0x7c, 0x02, 0x42, 0x3c, 0x00, 0x00, // 5 (0x35)
+    0x3c, 0x40, 0x7c, 0x42, 0x42, 0x3c, 0x00, 0x00, // 6 (0x36)
+    0x7e, 0x02, 0x04, 0x08, 0x10, 0x10, 0x00, 0x00, // 7 (0x37)
+    0x3c, 0x42, 0x3c, 0x42, 0x42, 0x3c, 0x00, 0x00, // 8 (0x38)
+    0x3c, 0x42, 0x3e, 0x02, 0x02, 0x3c, 0x00, 0x00, // 9 (0x39)
+    0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, // : (0x3a)
+    0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x10, 0x00, // ; (0x3b)
+    0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, // < (0x3c)
+    0x00, 0x00, 0x3e, 0x00, 0x3e, 0x00, 0x00, 0x00, // = (0x3d)
+    0x20, 0x10, 0x08, 0x04, 0x08, 0x10, 0x20, 0x00, // > (0x3e)
+    0x3c, 0x42, 0x02, 0x0c, 0x00, 0x08, 0x08, 0x00, // ? (0x3f)
+    0x3c, 0x42, 0x5a, 0x5a, 0x5a, 0x3e, 0x00, 0x00, // @ (0x40)
+    0x18, 0x24, 0x42, 0x7e, 0x42, 0x42, 0x00, 0x00, // A (0x41)
+    0x7c, 0x42, 0x7c, 0x42, 0x42, 0x7c, 0x00, 0x00, // B (0x42)
+    0x3c, 0x42, 0x40, 0x40, 0x42, 0x3c, 0x00, 0x00, // C (0x43)
+    0x78, 0x44, 0x42, 0x42, 0x44, 0x78, 0x00, 0x00, // D (0x44)
+    0x7e, 0x40, 0x7c, 0x40, 0x40, 0x7e, 0x00, 0x00, // E (0x45)
+    0x7e, 0x40, 0x7c, 0x40, 0x40, 0x40, 0x00, 0x00, // F (0x46)
+    0x3c, 0x42, 0x40, 0x4e, 0x42, 0x3c, 0x00, 0x00, // G (0x47)
+    0x42, 0x42, 0x7e, 0x42, 0x42, 0x42, 0x00, 0x00, // H (0x48)
+    0x3e, 0x08, 0x08, 0x08, 0x08, 0x3e, 0x00, 0x00, // I (0x49)
+    0x02, 0x02, 0x02, 0x02, 0x42, 0x3c, 0x00, 0x00, // J (0x4a)
+    0x44, 0x48, 0x70, 0x48, 0x44, 0x42, 0x00, 0x00, // K (0x4b)
+    0x40, 0x40, 0x40, 0x40, 0x40, 0x7e, 0x00, 0x00, // L (0x4c)
+    0x42, 0x66, 0x5a, 0x42, 0x42, 0x42, 0x00, 0x00, // M (0x4d)
+    0x42, 0x62, 0x52, 0x4a, 0x46, 0x42, 0x00, 0x00, // N (0x4e)
+    0x3c, 0x42, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // O (0x4f)
+    0x7c, 0x42, 0x7c, 0x40, 0x40, 0x40, 0x00, 0x00, // P (0x50)
+    0x3c, 0x42, 0x42, 0x42, 0x4a, 0x3c, 0x02, 0x00, // Q (0x51)
+    0x7c, 0x42, 0x7c, 0x48, 0x44, 0x42, 0x00, 0x00, // R (0x52)
+    0x3c, 0x40, 0x3c, 0x02, 0x02, 0x3c, 0x00, 0x00, // S (0x53)
+    0x7e, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, // T (0x54)
+    0x42, 0x42, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // U (0x55)
+    0x42, 0x42, 0x42, 0x24, 0x24, 0x18, 0x00, 0x00, // V (0x56)
+    0x42, 0x42, 0x42, 0x5a, 0x5a, 0x24, 0x00, 0x00, // W (0x57)
+    0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00, 0x00, // X (0x58)
+    0x42, 0x42, 0x24, 0x18, 0x08, 0x08, 0x00, 0x00, // Y (0x59)
+    0x7e, 0x02, 0x04, 0x08, 0x10, 0x7e, 0x00, 0x00, // Z (0x5a)
+    0x3c, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x00, 0x00, // [ (0x5b)
+    0x00, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, // \ (0x5c)
+    0x3c, 0x02, 0x02, 0x02, 0x02, 0x3c, 0x00, 0x00, // ] (0x5d)
+    0x08, 0x14, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, // ^ (0x5e)
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, // _ (0x5f)
+    0x08, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // ` (0x60)
+    0x00, 0x3c, 0x02, 0x3e, 0x42, 0x3e, 0x00, 0x00, // a (0x61)
+    0x40, 0x40, 0x7c, 0x42, 0x42, 0x7c, 0x00, 0x00, // b (0x62)
+    0x00, 0x3c, 0x40, 0x40, 0x42, 0x3c, 0x00, 0x00, // c (0x63)
+    0x02, 0x02, 0x3e, 0x42, 0x42, 0x3e, 0x00, 0x00, // d (0x64)
+    0x00, 0x3c, 0x42, 0x7e, 0x40, 0x3c, 0x00, 0x00, // e (0x65)
+    0x1c, 0x20, 0x78, 0x20, 0x20, 0x20, 0x00, 0x00, // f (0x66)
+    0x00, 0x3e, 0x42, 0x3e, 0x02, 0x3c, 0x00, 0x00, // g (0x67)
+    0x40, 0x40, 0x7c, 0x42, 0x42, 0x42, 0x00, 0x00, // h (0x68)
+    0x08, 0x00, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, // i (0x69)
+    0x02, 0x00, 0x02, 0x02, 0x02, 0x42, 0x3c, 0x00, // j (0x6a)
+    0x40, 0x44, 0x48, 0x70, 0x48, 0x44, 0x00, 0x00, // k (0x6b)
+    0x18, 0x08, 0x08, 0x08, 0x08, 0x3e, 0x00, 0x00, // l (0x6c)
+    0x00, 0x66, 0x5a, 0x42, 0x42, 0x42, 0x00, 0x00, // m (0x6d)
+    0x00, 0x7c, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, // n (0x6e)
+    0x00, 0x3c, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // o (0x6f)
+    0x00, 0x7c, 0x42, 0x7c, 0x40, 0x40, 0x00, 0x00, // p (0x70)
+    0x00, 0x3e, 0x42, 0x3e, 0x02, 0x02, 0x00, 0x00, // q (0x71)
+    0x00, 0x7c, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, // r (0x72)
+    0x00, 0x3e, 0x40, 0x3c, 0x02, 0x3c, 0x00, 0x00, // s (0x73)
+    0x08, 0x08, 0x3e, 0x08, 0x08, 0x0a, 0x04, 0x00, // t (0x74)
+    0x00, 0x42, 0x42, 0x42, 0x42, 0x3c, 0x00, 0x00, // u (0x75)
+    0x00, 0x42, 0x42, 0x42, 0x24, 0x18, 0x00, 0x00, // v (0x76)
+    0x00, 0x42, 0x42, 0x5a, 0x5a, 0x24, 0x00, 0x00, // w (0x77)
+    0x00, 0x42, 0x24, 0x18, 0x24, 0x42, 0x00, 0x00, // x (0x78)
+    0x00, 0x42, 0x42, 0x3e, 0x02, 0x3c, 0x00, 0x00, // y (0x79)
+    0x00, 0x7e, 0x04, 0x08, 0x10, 0x7e, 0x00, 0x00, // z (0x7a)
+    0x0c, 0x10, 0x10, 0x20, 0x10, 0x10, 0x0c, 0x00, // { (0x7b)
+    0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, // | (0x7c)
+    0x30, 0x08, 0x08, 0x04, 0x08, 0x08, 0x30, 0x00, // } (0x7d)
+    0x00, 0x00, 0x00, 0x14, 0x28, 0x00, 0x00, 0x00, // ~ (0x7e)
+];
+
+/// Helper to draw a character at (x, y) with a given scale and BGRA color.
+fn draw_char(
+    buf: &mut [u32],
+    buf_w: i32,
+    buf_h: i32,
+    c: char,
+    x: i32,
+    y: i32,
+    scale: i32,
+    color: u32,
+) {
+    let code = (c as usize).saturating_sub(32);
+    if code >= 95 {
+        return;
+    }
+    let glyph = &FONT_DATA[code * 8..(code + 1) * 8];
+    for row in 0..8 {
+        let row_byte = glyph[row];
+        for col in 0..8 {
+            let bit = (row_byte >> (7 - col)) & 1;
+            if bit != 0 {
+                for dy in 0..scale {
+                    for dx in 0..scale {
+                        let px = x + (col as i32) * scale + dx;
+                        let py = y + (row as i32) * scale + dy;
+                        if px >= 0 && px < buf_w && py >= 0 && py < buf_h {
+                            buf[(py * buf_w + px) as usize] = color;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// Create a temporary shared memory file descriptor.
+fn create_memfd(size: usize) -> Option<RawFd> {
+    let name = CString::new("clearwm-decoration").ok()?;
+    let fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
+    if fd < 0 {
+        return None;
+    }
+    if unsafe { libc::ftruncate(fd, size as libc::off_t) } < 0 {
+        unsafe { libc::close(fd); }
+        return None;
+    }
+    Some(fd)
+}
+
+/// Main entry point to create or update decoration surfaces during rendering.
+pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>) {
+    let active_tags = state.wm.active_tags;
+
+    // We can only create decoration surfaces if the compositor and shm globals are bound
+    let (compositor, shm) = match (&state.compositor, &state.shm) {
+        (Some(c), Some(s)) => (c, s),
+        _ => return,
+    };
+
+    // Calculate dynamic border colors
+    let border_colors = crate::borders::compute_border_colors(&state.wm);
+    let text_color = 0xFFE0E0E0u32;
+
+    // Collect window IDs to modify so we don't violate the borrow checker
+    let windows_to_decorate: Vec<(u64, i32, i32, String, u32)> = state
+        .wm
+        .windows
+        .iter()
+        .enumerate()
+        .filter(|(_, w)| !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
+        .filter(|(_, w)| (w.tags & active_tags) != 0)
+        .map(|(idx, w)| {
+            let title = w.title.clone().unwrap_or_else(|| {
+                w.app_id.clone().unwrap_or_else(|| "Window".to_string())
+            });
+            let layout_idx = state.wm.windows
+                .iter()
+                .filter(|win| !win.closed && win.app_id.as_deref() != Some("clear-status-interface"))
+                .position(|win| win.id == w.id)
+                .unwrap_or(0);
+            let title_with_idx = format!("[{}] {}", layout_idx, title);
+
+            // Find matching computed border color for this window
+            let bc = border_colors.iter().find(|b| b.window_idx == idx);
+            let (r, g, b, a) = match bc {
+                Some(b) => (b.r, b.g, b.b, b.a),
+                None => (
+                    state.wm.layout.border_r,
+                    state.wm.layout.border_g,
+                    state.wm.layout.border_b,
+                    state.wm.layout.border_a,
+                ),
+            };
+
+            let border_r = (r & 0xFF) as u8;
+            let border_g = (g & 0xFF) as u8;
+            let border_b = (b & 0xFF) as u8;
+            let border_a = (a & 0xFF) as u8;
+            let bg_color = ((border_a as u32) << 24) | ((border_r as u32) << 16) | ((border_g as u32) << 8) | (border_b as u32);
+
+            // Border width is mode-specific
+            let border_w = match w.tiling_mode {
+                crate::types::TilingMode::Cascade => state.wm.layout.cascade_border_width,
+                crate::types::TilingMode::Fullscreen => state.wm.layout.fullscreen_border_width,
+                crate::types::TilingMode::Grid => state.wm.layout.grid_border_width,
+                crate::types::TilingMode::Vsplit => state.wm.layout.vsplit_border_width,
+                crate::types::TilingMode::Hsplit => state.wm.layout.hsplit_border_width,
+                crate::types::TilingMode::Floating => state.wm.layout.floating_border_width,
+            };
+            (w.id, w.width, border_w, title_with_idx, bg_color)
+        })
+        .collect();
+
+    for (wid, win_width, border_width, title, bg_color) in windows_to_decorate {
+        // Find or create decoration proxy
+        let wp_idx = match state.window_proxies.iter().position(|(id, _)| *id == wid) {
+            Some(idx) => idx,
+            None => continue,
+        };
+
+        let wp = &mut state.window_proxies[wp_idx].1;
+
+        // Determine titlebar dimensions:
+        // Height equals border_width (or 16 if border_width is too small to display font)
+        let dec_height = std::cmp::max(border_width, 16);
+        let dec_width = win_width + 2 * border_width;
+
+        let needs_new_buffer = match &wp.decoration {
+            Some(dec) => dec.width != dec_width || dec.height != dec_height,
+            None => true,
+        };
+
+        if wp.decoration.is_none() {
+            eprintln!("[decorations] creating decoration for window {}", wid);
+            let surface: wl_surface::WlSurface = compositor.create_surface(qhandle, ());
+            let decoration: RiverDecorationV1 = wp.river_window.get_decoration_above(&surface, qhandle, ());
+
+            wp.decoration = Some(WindowDecoration {
+                surface,
+                decoration,
+                buffer: None,
+                pool: None,
+                width: 0,
+                height: 0,
+                mapped_data: ptr::null_mut(),
+                mapped_size: 0,
+            });
+        }
+
+        let dec = wp.decoration.as_mut().unwrap();
+
+        // Position decoration on top of the window top border
+        dec.decoration.set_offset(-border_width, -border_width);
+
+        if needs_new_buffer {
+            eprintln!(
+                "[decorations] allocating buffer {}x{} for window {}",
+                dec_width, dec_height, wid
+            );
+
+            // Clean up old mapping if it exists
+            if !dec.mapped_data.is_null() {
+                unsafe {
+                    libc::munmap(dec.mapped_data as *mut libc::c_void, dec.mapped_size);
+                }
+                dec.mapped_data = ptr::null_mut();
+            }
+
+            let stride = dec_width * 4;
+            let size = (stride * dec_height) as usize;
+
+            if let Some(fd) = create_memfd(size) {
+                let mapped_data = unsafe {
+                    libc::mmap(
+                        ptr::null_mut(),
+                        size,
+                        libc::PROT_READ | libc::PROT_WRITE,
+                        libc::MAP_SHARED,
+                        fd,
+                        0,
+                    ) as *mut u32
+                };
+
+                if mapped_data != libc::MAP_FAILED as *mut u32 {
+                    let borrowed_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
+                    let pool = shm.create_pool(borrowed_fd, size as i32, qhandle, ());
+                    let buffer = pool.create_buffer(
+                        0,
+                        dec_width,
+                        dec_height,
+                        stride,
+                        wl_shm::Format::Argb8888,
+                        qhandle,
+                        (),
+                    );
+
+                    dec.pool = Some(pool);
+                    dec.buffer = Some(buffer);
+                    dec.width = dec_width;
+                    dec.height = dec_height;
+                    dec.mapped_data = mapped_data;
+                    dec.mapped_size = size;
+                } else {
+                    eprintln!("[decorations] failed to mmap decoration buffer");
+                    unsafe { libc::close(fd); }
+                    continue;
+                }
+                unsafe { libc::close(fd); }
+            } else {
+                eprintln!("[decorations] failed to create memfd");
+                continue;
+            }
+        }
+
+        // Draw titlebar background and text
+        if !dec.mapped_data.is_null() {
+            let buffer_slice = unsafe {
+                std::slice::from_raw_parts_mut(dec.mapped_data, (dec_width * dec_height) as usize)
+            };
+
+            // Clear background
+            for pixel in buffer_slice.iter_mut() {
+                *pixel = bg_color;
+            }
+
+            // Draw window title text
+            // Determine font scale (1x if height < 32, 2x if height >= 32)
+            let scale = if dec_height >= 32 { 2 } else { 1 };
+            let font_h = 8 * scale;
+            
+            // Vertically center the text inside the titlebar
+            let text_y = (dec_height - font_h) / 2;
+            let mut text_x = 8; // Margin from left
+
+            for c in title.chars() {
+                if text_x + 8 * scale > dec_width {
+                    break; // Out of bounds
+                }
+                draw_char(buffer_slice, dec_width, dec_height, c, text_x, text_y, scale, text_color);
+                text_x += 8 * scale;
+            }
+
+            // Commit surface rendering
+            dec.decoration.sync_next_commit();
+            if let Some(ref wl_buf) = dec.buffer {
+                dec.surface.attach(Some(wl_buf), 0, 0);
+            }
+            dec.surface.commit();
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 84a0af0..87da6c3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@
 
 pub mod borders;
 pub mod config;
+pub mod decorations;
 pub mod ipc;
 pub mod ipc_server;
 pub mod protocol;
diff --git a/src/wayland.rs b/src/wayland.rs
index 4fbf2bf..56806ba 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -19,6 +19,7 @@ use crate::protocol::river_layer_shell::client::{
     river_layer_shell_v1::{self, RiverLayerShellV1},
 };
 use crate::protocol::river_window_management::client::{
+    river_decoration_v1::{self, RiverDecorationV1},
     river_node_v1::{self, RiverNodeV1},
     river_output_v1::{self, RiverOutputV1},
     river_pointer_binding_v1::{self, RiverPointerBindingV1},
@@ -40,7 +41,9 @@ use crate::protocol::wlr_output_management::client::{
 };
 
 use crate::types::{Action, BindingUserData, Output, Seat, TilingMode, Window, WindowManager};
-use wayland_client::protocol::{wl_pointer, wl_seat, wl_output};
+use wayland_client::protocol::{
+    wl_buffer, wl_compositor, wl_output, wl_pointer, wl_seat, wl_shm, wl_shm_pool, wl_surface,
+};
 use wayland_protocols::wp::cursor_shape::v1::client::{
     wp_cursor_shape_device_v1::{self, WpCursorShapeDeviceV1, Shape},
     wp_cursor_shape_manager_v1::{self, WpCursorShapeManagerV1},
@@ -59,6 +62,7 @@ const IFACE_WLR_OUTPUT_MANAGER: &str = "zwlr_output_manager_v1";
 /// call protocol methods (set_position, propose_dimensions, etc.) on it.
 pub struct WindowProxy {
     pub river_window: RiverWindowV1,
+    pub decoration: Option<crate::decorations::WindowDecoration>,
 }
 
 /// Wayland proxy objects stored alongside each Seat.
@@ -98,6 +102,8 @@ pub struct AppState {
     pub layer_shell: Option<RiverLayerShellV1>,
     pub input_manager: Option<RiverInputManagerV1>,
     pub cursor_shape_manager: Option<WpCursorShapeManagerV1>,
+    pub compositor: Option<wl_compositor::WlCompositor>,
+    pub shm: Option<wl_shm::WlShm>,
 
     // Whether we got all required globals
     pub has_window_manager: bool,
@@ -176,6 +182,8 @@ impl AppState {
             layer_shell: None,
             input_manager: None,
             cursor_shape_manager: None,
+            compositor: None,
+            shm: None,
             has_window_manager: false,
             has_xkb_bindings: false,
             window_proxies: Vec::new(),
@@ -309,6 +317,16 @@ impl Dispatch<wl_registry::WlRegistry, RegistryData> for AppState {
                     let csm: WpCursorShapeManagerV1 =
                         registry.bind::<WpCursorShapeManagerV1, _, _>(name, 1, qhandle, ());
                     state.cursor_shape_manager = Some(csm);
+                } else if interface == "wl_compositor" {
+                    eprintln!("registry: binding wl_compositor name={}", name);
+                    let comp: wl_compositor::WlCompositor =
+                        registry.bind::<wl_compositor::WlCompositor, _, _>(name, 4, qhandle, ());
+                    state.compositor = Some(comp);
+                } else if interface == "wl_shm" {
+                    eprintln!("registry: binding wl_shm name={}", name);
+                    let shm: wl_shm::WlShm =
+                        registry.bind::<wl_shm::WlShm, _, _>(name, 1, qhandle, ());
+                    state.shm = Some(shm);
                 } else if interface == "wl_output" {
                     eprintln!("registry: binding wl_output name={}", name);
                     let wl_out: wl_output::WlOutput =
@@ -402,6 +420,10 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                 for (closed_id, closed_app_id) in &closed_ids {
                     state.window_proxies.retain(|(id, wp)| {
                         if id == closed_id {
+                            if let Some(dec) = wp.decoration.as_ref() {
+                                dec.decoration.destroy();
+                                dec.surface.destroy();
+                            }
                             wp.river_window.destroy();
                             false
                         } else {
@@ -783,6 +805,9 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                     // and must happen during ManageStart.
                     crate::wm::render_borders(state);
 
+                    // Update and render window title decorations on borders
+                    crate::decorations::update_decorations(state, qhandle);
+
                     // Raise the focused window to the top of the visual stack.
                     // place_top() modifies rendering state and must be called
                     // during a render sequence.
@@ -890,7 +915,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                 state.wm.windows.push(window);
                 state
                     .window_proxies
-                    .push((id, WindowProxy { river_window }));
+                    .push((id, WindowProxy { river_window, decoration: None }));
                 eprintln!("[window] new window id={} (app_id pending)", id);
             }
 
@@ -2741,3 +2766,77 @@ impl Dispatch<WpCursorShapeDeviceV1, ()> for AppState {
     ) {
     }
 }
+
+// --- core Wayland and River decoration events dispatch ---
+
+impl Dispatch<wl_compositor::WlCompositor, ()> for AppState {
+    fn event(
+        _state: &mut Self,
+        _proxy: &wl_compositor::WlCompositor,
+        _event: wl_compositor::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qhandle: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<wl_shm::WlShm, ()> for AppState {
+    fn event(
+        _state: &mut Self,
+        _proxy: &wl_shm::WlShm,
+        _event: wl_shm::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qhandle: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<wl_shm_pool::WlShmPool, ()> for AppState {
+    fn event(
+        _state: &mut Self,
+        _proxy: &wl_shm_pool::WlShmPool,
+        _event: wl_shm_pool::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qhandle: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<wl_surface::WlSurface, ()> for AppState {
+    fn event(
+        _state: &mut Self,
+        _proxy: &wl_surface::WlSurface,
+        _event: wl_surface::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qhandle: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<wl_buffer::WlBuffer, ()> for AppState {
+    fn event(
+        _state: &mut Self,
+        _proxy: &wl_buffer::WlBuffer,
+        _event: wl_buffer::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qhandle: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<RiverDecorationV1, ()> for AppState {
+    fn event(
+        _state: &mut Self,
+        _proxy: &RiverDecorationV1,
+        _event: river_decoration_v1::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qhandle: &QueueHandle<Self>,
+    ) {
+    }
+}