Files
feresaria/portal/src/parent.rs
T
Samuel Onoja a6f83689f8 Add XWayland support (#16)
* save dev state

* fix xwayland-satellite supervisor lifecycle, readiness and cleanup

* remove wayland scanner protocol generated files

* default xwayland enabled with path

* remove jargons

* address x11 review: park unconfirmed cleanup, never signal a reaped group, fix storm test and notify dir mode

* fix resize tests: never build Size values from negative dimensions

* add min-width/min-height window rules that replace client minimum sizes

* rename effective_size_constraints to effective_minimum_size and minor cleanups
2026-10-06 19:40:22 +01:00

254 lines
8.4 KiB
Rust

pub(crate) type Attachment = Result<
(
Option<std::sync::Arc<Parent>>,
Result<ferese_theme_client::material::ModalMaterial, String>,
),
String,
>;
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{wl_registry, wl_surface};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2;
use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2::ZxdgImportedV2;
use wayland_protocols::xdg::foreign::zv2::client::zxdg_importer_v2::ZxdgImporterV2;
const MAX_PARENT_IDENTIFIER: usize = 4096;
#[derive(Debug, Default)]
pub(crate) struct ParentAttachment {
pub parent: Option<Parent>,
pub diagnostic: Option<String>,
}
fn classify_parent<'a>(parent: &'a str) -> ParentKind<'a> {
if parent.is_empty() {
return ParentKind::None;
}
if parent.len() > MAX_PARENT_IDENTIFIER {
return ParentKind::Unsupported;
}
if let Some(handle) = parent.strip_prefix("wayland:") {
return if handle.is_empty() {
ParentKind::Unsupported
} else {
ParentKind::Wayland(handle)
};
}
if let Some(handle) = parent.strip_prefix("x11:") {
return if is_x11_window_identifier(handle) {
ParentKind::X11
} else {
// A malformed XID is a protocol error, not an X11 parent to
// degrade from: accepting arbitrary text here would let any string
// be reported as an X11 window.
ParentKind::Unsupported
};
}
ParentKind::Unsupported
}
/// Whether `identifier` is a syntactically valid X11 window identifier.
///
/// XIDs are unsigned 32-bit hex, optionally with the `0x` prefix, as X11
/// clients write them (for example `0x2400003` or `1a2b3c`).
fn is_x11_window_identifier(identifier: &str) -> bool {
let digits = identifier
.strip_prefix("0x")
.or_else(|| identifier.strip_prefix("0X"))
.unwrap_or(identifier);
!digits.is_empty() && digits.len() <= 8 && digits.bytes().all(|byte| byte.is_ascii_hexdigit())
}
#[derive(Debug, PartialEq, Eq)]
enum ParentKind<'a> {
None,
Wayland(&'a str),
X11,
Unsupported,
}
#[derive(Debug)]
pub(crate) struct Parent {
connection: Connection,
imported: ZxdgImportedV2,
importer: ZxdgImporterV2,
}
impl Parent {
pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result<ParentAttachment, String> {
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
let handle = match classify_parent(parent) {
ParentKind::None => {
return Ok(ParentAttachment::default());
}
ParentKind::X11 => {
return Ok(ParentAttachment {
parent: None,
diagnostic: Some(
"showing this dialog unparented: X11 parent windows are not supported yet".to_owned(),
),
});
}
ParentKind::Unsupported => {
return Err("Unsupported portal parent window identifier".into());
}
ParentKind::Wayland(handle) => handle,
};
let display = window.display_handle().map_err(|error| error.to_string())?;
let surface = window.window_handle().map_err(|error| error.to_string())?;
let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(surface)) =
(display.as_raw(), surface.as_raw())
else {
return Err("Portal parenting requires Wayland".into());
};
// The window lends live handles; the borrowed backend never owns the display.
let backend =
unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
let connection = Connection::from_backend(backend);
let id = unsafe {
wayland_client::backend::ObjectId::from_ptr(
wl_surface::WlSurface::interface(),
surface.surface.as_ptr().cast(),
)
}
.map_err(|error| error.to_string())?;
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
let (globals, mut queue) = registry_queue_init::<State>(&connection).map_err(|error| error.to_string())?;
let qh = queue.handle();
let importer = globals
.bind::<ZxdgImporterV2, _, _>(&qh, 1..=1, ())
.map_err(|error| error.to_string())?;
let imported = importer.import_toplevel(handle.into(), &qh, ());
let mut state = State::default();
queue.roundtrip(&mut state).map_err(|error| error.to_string())?;
if state.invalid {
imported.destroy();
importer.destroy();
let _ = connection.flush();
return Err("Portal parent window no longer exists".into());
}
imported.set_parent_of(&surface);
connection.flush().map_err(|error| error.to_string())?;
Ok(ParentAttachment {
parent: Some(Self {
connection,
imported,
importer,
}),
diagnostic: None,
})
}
}
impl Drop for Parent {
fn drop(&mut self) {
self.imported.destroy();
self.importer.destroy();
let _ = self.connection.flush();
}
}
#[derive(Default)]
struct State {
invalid: bool,
}
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_: &mut Self,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZxdgImportedV2, ()> for State {
fn event(
state: &mut Self,
_: &ZxdgImportedV2,
event: zxdg_imported_v2::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if matches!(event, zxdg_imported_v2::Event::Destroyed) {
state.invalid = true;
}
}
}
delegate_noop!(State: ignore ZxdgImporterV2);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_x11_parent_degrades_instead_of_failing_the_request() {
assert!(matches!(classify_parent("x11:0x2400003"), ParentKind::X11));
assert!(matches!(classify_parent("x11:1a2b3c"), ParentKind::X11));
}
#[test]
fn an_x11_parent_is_never_reinterpreted_as_a_wayland_handle() {
match classify_parent("x11:0x2400003") {
ParentKind::Wayland(handle) => panic!("an X11 parent must not become a Wayland handle: {handle}"),
ParentKind::X11 => {}
other => panic!("unexpected classification: {other:?}"),
}
}
#[test]
fn wayland_parents_are_still_imported() {
assert!(matches!(
classify_parent("wayland:handle-1"),
ParentKind::Wayland("handle-1")
));
assert_eq!(classify_parent(""), ParentKind::None);
}
#[test]
fn malformed_x11_window_identifiers_are_errors_not_parents() {
// Anything that is not an X11 window identifier must never be reported
// as one: doing so would turn arbitrary caller text into a window.
for parent in [
"x11:",
"x11:not-a-window",
"x11:0xzzzz",
"x11:0x2400003extra",
"x11:9999999999",
"x11:-1",
] {
assert!(
matches!(classify_parent(parent), ParentKind::Unsupported),
"{parent} must stay a hard error"
);
}
// Every form an X11 client actually produces is still accepted.
for parent in ["x11:0x2400003", "x11:1a2b3c", "x11:0X1", "x11:ffffffff"] {
assert!(
matches!(classify_parent(parent), ParentKind::X11),
"{parent} is a valid window identifier"
);
}
}
#[test]
fn malformed_identifiers_remain_hard_errors() {
for parent in ["x11:", "wayland:", "dbus:", "not-a-parent", "X11:0x1"] {
assert!(
matches!(classify_parent(parent), ParentKind::Unsupported),
"{parent} must stay a hard error"
);
}
let huge = format!("wayland:{}", "a".repeat(MAX_PARENT_IDENTIFIER + 1));
assert!(matches!(classify_parent(&huge), ParentKind::Unsupported));
}
}