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    /// Negotiate explicit, revocable whole-window pointer confinement requests.
51    /// Negotiation alone never grants confinement or inhibits host shortcuts.
52    pub pointer_confinement: bool,
53}
54impl ClientOptions {
55    pub(crate) const fn bits(self) -> u32 {
56        self.relative_pointer as u32 | ((self.pointer_confinement as u32) << 1)
57    }
58    pub(crate) fn from_bits(bits: u32) -> io::Result<Self> {
59        if bits & !3 != 0 {
60            return Err(invalid("unknown native window options"));
61        }
62        Ok(Self {
63            relative_pointer: bits & 1 != 0,
64            pointer_confinement: bits & 2 != 0,
65        })
66    }
67}
68
69/// One bounded request against the current adopted frame. Serials start at one
70/// and increase exactly once per request; a reply releases transport credit.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct PointerConfinementRequest {
73    pub request_serial: u64,
74    pub frame_serial: u64,
75    pub confined: bool,
76}
77impl PointerConfinementRequest {
78    pub fn validate(self) -> io::Result<()> {
79        if self.request_serial == 0 || self.frame_serial == 0 {
80            return Err(invalid("invalid native pointer confinement request"));
81        }
82        Ok(())
83    }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct Create {
88    pub app_id: String,
89    pub title: String,
90    pub preferred_width: u32,
91    pub preferred_height: u32,
92}
93impl Create {
94    pub fn validate(&self) -> io::Result<()> {
95        if self.app_id.is_empty()
96            || self.app_id.len() > MAX_APP_ID_BYTES
97            || !self
98                .app_id
99                .bytes()
100                .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
101            || self.title.is_empty()
102            || self.title.len() > MAX_TITLE_BYTES
103            || self.title.chars().any(char::is_control)
104        {
105            return Err(invalid("invalid native window identity"));
106        }
107        frame_bytes(self.preferred_width, self.preferred_height)?;
108        Ok(())
109    }
110}
111
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub struct Configure {
114    pub window_id: u64,
115    pub configure_serial: u64,
116    pub width: u32,
117    pub height: u32,
118    pub scale: u32,
119}
120impl Configure {
121    pub fn validate(&self) -> io::Result<()> {
122        if self.window_id == 0 || self.configure_serial == 0 || self.scale != 1 {
123            return Err(invalid("invalid native window configuration"));
124        }
125        frame_bytes(self.width, self.height)?;
126        Ok(())
127    }
128}
129
130#[derive(Clone, Debug)]
131pub struct Frame {
132    pub frame_serial: u64,
133    pub configure_serial: u64,
134    pub width: u32,
135    pub height: u32,
136    /// Tight, straight-alpha BGRA pixels. Clones share the allocation and its
137    /// server memory reservation; the backing allocation cannot be extracted.
138    pub pixels: PixelBuffer,
139}
140pub type ServerFrame = Frame;
141impl Frame {
142    pub fn validate(&self) -> io::Result<()> {
143        if self.frame_serial == 0
144            || self.configure_serial == 0
145            || self.pixels.len() != frame_bytes(self.width, self.height)?
146        {
147            return Err(invalid("invalid native window frame"));
148        }
149        Ok(())
150    }
151}
152
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub enum TouchPhase {
155    /// A contact began on this window.
156    Down,
157    /// An owned contact moved; motion stays routed to its original window.
158    Motion,
159    /// An owned contact ended normally.
160    Up,
161}
162
163#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct Input {
165    pub frame_serial: u64,
166    pub event: InputEvent,
167}
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub enum InputEvent {
170    /// Reply to a negotiated request, or permanent revocation of an active grant.
171    /// A false reply denies/releases the request. An active grant never returns
172    /// after revocation without a new explicit request and compositor approval.
173    PointerConfinement {
174        request_serial: u64,
175        active: bool,
176    },
177    Focus {
178        focused: bool,
179    },
180    Motion {
181        x: i32,
182        y: i32,
183    },
184    /// Physical, unaccelerated deltas before output clipping, delivered only
185    /// after explicit negotiation. The monotonic timestamp is compositor-local.
186    /// Relative delivery grants neither capture nor shortcut inhibition.
187    RelativeMotion {
188        dx: i32,
189        dy: i32,
190        time_usec: u64,
191    },
192    Button {
193        button: u32,
194        pressed: bool,
195        x: i32,
196        y: i32,
197    },
198    Axis {
199        horizontal: i32,
200        vertical: i32,
201        x: i32,
202        y: i32,
203    },
204    Leave,
205    Key {
206        keycode: u32,
207        keysym: u32,
208        pressed: bool,
209        repeat: bool,
210        modifiers: u32,
211        text: Option<char>,
212    },
213    /// An atomic, bounded UTF-8 text commit from the compositor.
214    TextCommit {
215        text: String,
216    },
217    /// One bounded compositor-owned touch contact in window-local pixels.
218    Touch {
219        id: i32,
220        phase: TouchPhase,
221        x: i32,
222        y: i32,
223    },
224    /// All active contacts on this window were cancelled by the compositor.
225    TouchCancel,
226}
227impl Input {
228    pub fn validate(&self) -> io::Result<()> {
229        if self.frame_serial == 0 {
230            return Err(invalid("input requires an adopted frame"));
231        }
232        match &self.event {
233            InputEvent::PointerConfinement {
234                request_serial: 0, ..
235            } => Err(invalid("invalid native pointer confinement status")),
236            InputEvent::Key {
237                keycode,
238                pressed,
239                repeat,
240                modifiers,
241                text,
242                ..
243            } if *keycode > 0xffff
244                || (*repeat && !*pressed)
245                || *modifiers & !15 != 0
246                || (*text).is_some_and(char::is_control)
247                || (!*pressed && (*text).is_some()) =>
248            {
249                Err(invalid("invalid native window key event"))
250            }
251            InputEvent::TextCommit { text }
252                if text.is_empty()
253                    || text.len() > MAX_TEXT_COMMIT_BYTES
254                    || text.chars().any(char::is_control) =>
255            {
256                Err(invalid("invalid native window text commit"))
257            }
258            InputEvent::Button { button, .. } if *button == 0 || *button > 0xffff => {
259                Err(invalid("invalid native window button"))
260            }
261            InputEvent::Touch { id, phase, x, y }
262                if *id < 0 || (*phase == TouchPhase::Up && (*x != 0 || *y != 0)) =>
263            {
264                Err(invalid("invalid native window touch event"))
265            }
266            _ => Ok(()),
267        }
268    }
269}
270
271pub fn frame_bytes(width: u32, height: u32) -> io::Result<usize> {
272    if width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION {
273        return Err(invalid("native window dimensions exceed limits"));
274    }
275    let bytes = (width as usize)
276        .checked_mul(height as usize)
277        .and_then(|n| n.checked_mul(4))
278        .filter(|n| *n <= MAX_FRAME_BYTES)
279        .ok_or_else(|| invalid("native window frame exceeds limit"))?;
280    Ok(bytes)
281}
282
283pub fn socket_for_shell(shell: &Path) -> PathBuf {
284    let mut path = shell.as_os_str().to_owned();
285    path.push(".windows");
286    path.into()
287}
288
289/// Selects one native endpoint. No display connection or discovery fallback is made.
290pub fn socket_from_env() -> io::Result<PathBuf> {
291    if let Some(path) = std::env::var_os("AEN_WINDOW_SOCKET") {
292        return Ok(path.into());
293    }
294    if let Some(path) = std::env::var_os("KNOTT_COMPOSITOR_SHELL_SOCKET") {
295        return Ok(socket_for_shell(Path::new(&path)));
296    }
297    let runtime = std::env::var_os("XDG_RUNTIME_DIR")
298        .ok_or_else(|| invalid("no native window endpoint is configured"))?;
299    Ok(socket_for_shell(
300        &PathBuf::from(runtime).join("knottx-shell.sock"),
301    ))
302}
303
304pub(crate) fn invalid(message: &'static str) -> io::Error {
305    io::Error::new(io::ErrorKind::InvalidData, message)
306}