Skip to main content

aen_window/
lib.rs

1//! Native application windows owned and composed by KnottCompositor.
2//!
3//! One authenticated connection creates one window. Frame acknowledgements mean
4//! software adoption, not physical scanout. Applications cannot assign geometry,
5//! workspaces, focus or stacking. The compositor owns those policies.
6
7mod client;
8pub mod clipboard;
9mod memory;
10mod platform;
11#[cfg(feature = "server")]
12mod server;
13mod state;
14mod wire;
15mod worker;
16
17pub use client::{Client, Event};
18pub use memory::PixelBuffer;
19#[cfg(feature = "server")]
20pub use server::{NativeWindowServer, Registration, ServerEvent};
21pub use worker::WorkerWindow;
22
23use std::io;
24use std::path::{Path, PathBuf};
25use std::time::Duration;
26
27pub const PROTOCOL_NAME: &str = "org.knott.aen-window";
28pub const PROTOCOL_VERSION: u16 = 3;
29pub const MAX_DIMENSION: u32 = 8192;
30pub const MAX_FRAME_BYTES: usize = 32 * 1024 * 1024;
31pub const MAX_MEMORY_BYTES: usize = 128 * 1024 * 1024;
32pub const MAX_CLIENTS: usize = 32;
33pub const MAX_APP_ID_BYTES: usize = 64;
34pub const MAX_TITLE_BYTES: usize = 128;
35/// Maximum UTF-8 byte length of one committed text input event.
36pub const MAX_TEXT_COMMIT_BYTES: usize = 256;
37pub const TRANSFER_TIMEOUT: Duration = Duration::from_secs(3);
38pub const MOD_SHIFT: u32 = 1;
39pub const MOD_CTRL: u32 = 2;
40pub const MOD_ALT: u32 = 4;
41pub const MOD_SUPER: u32 = 8;
42
43/// Explicit optional protocol extensions. The default keeps the original v3
44/// handshake byte-for-byte; unsupported opt-in endpoints fail without replay.
45#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
46pub struct ClientOptions {
47    /// Receive compositor-owned physical relative motion in addition to absolute
48    /// motion. This does not capture the pointer or inhibit host shortcuts.
49    pub relative_pointer: bool,
50}
51impl ClientOptions {
52    pub(crate) const fn bits(self) -> u32 {
53        self.relative_pointer as u32
54    }
55    pub(crate) fn from_bits(bits: u32) -> io::Result<Self> {
56        if bits & !1 != 0 {
57            return Err(invalid("unknown native window options"));
58        }
59        Ok(Self {
60            relative_pointer: bits & 1 != 0,
61        })
62    }
63}
64
65#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct Create {
67    pub app_id: String,
68    pub title: String,
69    pub preferred_width: u32,
70    pub preferred_height: u32,
71}
72impl Create {
73    pub fn validate(&self) -> io::Result<()> {
74        if self.app_id.is_empty()
75            || self.app_id.len() > MAX_APP_ID_BYTES
76            || !self
77                .app_id
78                .bytes()
79                .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
80            || self.title.is_empty()
81            || self.title.len() > MAX_TITLE_BYTES
82            || self.title.chars().any(char::is_control)
83        {
84            return Err(invalid("invalid native window identity"));
85        }
86        frame_bytes(self.preferred_width, self.preferred_height)?;
87        Ok(())
88    }
89}
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub struct Configure {
93    pub window_id: u64,
94    pub configure_serial: u64,
95    pub width: u32,
96    pub height: u32,
97    pub scale: u32,
98}
99impl Configure {
100    pub fn validate(&self) -> io::Result<()> {
101        if self.window_id == 0 || self.configure_serial == 0 || self.scale != 1 {
102            return Err(invalid("invalid native window configuration"));
103        }
104        frame_bytes(self.width, self.height)?;
105        Ok(())
106    }
107}
108
109#[derive(Clone, Debug)]
110pub struct Frame {
111    pub frame_serial: u64,
112    pub configure_serial: u64,
113    pub width: u32,
114    pub height: u32,
115    /// Tight, straight-alpha BGRA pixels. Clones share the allocation and its
116    /// server memory reservation; the backing allocation cannot be extracted.
117    pub pixels: PixelBuffer,
118}
119pub type ServerFrame = Frame;
120impl Frame {
121    pub fn validate(&self) -> io::Result<()> {
122        if self.frame_serial == 0
123            || self.configure_serial == 0
124            || self.pixels.len() != frame_bytes(self.width, self.height)?
125        {
126            return Err(invalid("invalid native window frame"));
127        }
128        Ok(())
129    }
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub enum TouchPhase {
134    /// A contact began on this window.
135    Down,
136    /// An owned contact moved; motion stays routed to its original window.
137    Motion,
138    /// An owned contact ended normally.
139    Up,
140}
141
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct Input {
144    pub frame_serial: u64,
145    pub event: InputEvent,
146}
147#[derive(Clone, Debug, Eq, PartialEq)]
148pub enum InputEvent {
149    Focus {
150        focused: bool,
151    },
152    Motion {
153        x: i32,
154        y: i32,
155    },
156    /// Physical, unaccelerated deltas before output clipping, delivered only
157    /// after explicit negotiation. The monotonic timestamp is compositor-local.
158    /// Relative delivery grants neither capture nor shortcut inhibition.
159    RelativeMotion {
160        dx: i32,
161        dy: i32,
162        time_usec: u64,
163    },
164    Button {
165        button: u32,
166        pressed: bool,
167        x: i32,
168        y: i32,
169    },
170    Axis {
171        horizontal: i32,
172        vertical: i32,
173        x: i32,
174        y: i32,
175    },
176    Leave,
177    Key {
178        keycode: u32,
179        keysym: u32,
180        pressed: bool,
181        repeat: bool,
182        modifiers: u32,
183        text: Option<char>,
184    },
185    /// An atomic, bounded UTF-8 text commit from the compositor.
186    TextCommit {
187        text: String,
188    },
189    /// One bounded compositor-owned touch contact in window-local pixels.
190    Touch {
191        id: i32,
192        phase: TouchPhase,
193        x: i32,
194        y: i32,
195    },
196    /// All active contacts on this window were cancelled by the compositor.
197    TouchCancel,
198}
199impl Input {
200    pub fn validate(&self) -> io::Result<()> {
201        if self.frame_serial == 0 {
202            return Err(invalid("input requires an adopted frame"));
203        }
204        match &self.event {
205            InputEvent::Key {
206                keycode,
207                pressed,
208                repeat,
209                modifiers,
210                text,
211                ..
212            } if *keycode > 0xffff
213                || (*repeat && !*pressed)
214                || *modifiers & !15 != 0
215                || (*text).is_some_and(char::is_control)
216                || (!*pressed && (*text).is_some()) =>
217            {
218                Err(invalid("invalid native window key event"))
219            }
220            InputEvent::TextCommit { text }
221                if text.is_empty()
222                    || text.len() > MAX_TEXT_COMMIT_BYTES
223                    || text.chars().any(char::is_control) =>
224            {
225                Err(invalid("invalid native window text commit"))
226            }
227            InputEvent::Button { button, .. } if *button == 0 || *button > 0xffff => {
228                Err(invalid("invalid native window button"))
229            }
230            InputEvent::Touch { id, phase, x, y }
231                if *id < 0 || (*phase == TouchPhase::Up && (*x != 0 || *y != 0)) =>
232            {
233                Err(invalid("invalid native window touch event"))
234            }
235            _ => Ok(()),
236        }
237    }
238}
239
240pub fn frame_bytes(width: u32, height: u32) -> io::Result<usize> {
241    if width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION {
242        return Err(invalid("native window dimensions exceed limits"));
243    }
244    let bytes = (width as usize)
245        .checked_mul(height as usize)
246        .and_then(|n| n.checked_mul(4))
247        .filter(|n| *n <= MAX_FRAME_BYTES)
248        .ok_or_else(|| invalid("native window frame exceeds limit"))?;
249    Ok(bytes)
250}
251
252pub fn socket_for_shell(shell: &Path) -> PathBuf {
253    let mut path = shell.as_os_str().to_owned();
254    path.push(".windows");
255    path.into()
256}
257
258/// Selects one native endpoint. No display connection or discovery fallback is made.
259pub fn socket_from_env() -> io::Result<PathBuf> {
260    if let Some(path) = std::env::var_os("AEN_WINDOW_SOCKET") {
261        return Ok(path.into());
262    }
263    if let Some(path) = std::env::var_os("KNOTT_COMPOSITOR_SHELL_SOCKET") {
264        return Ok(socket_for_shell(Path::new(&path)));
265    }
266    let runtime = std::env::var_os("XDG_RUNTIME_DIR")
267        .ok_or_else(|| invalid("no native window endpoint is configured"))?;
268    Ok(socket_for_shell(
269        &PathBuf::from(runtime).join("knottx-shell.sock"),
270    ))
271}
272
273pub(crate) fn invalid(message: &'static str) -> io::Error {
274    io::Error::new(io::ErrorKind::InvalidData, message)
275}