diff --git a/build/build.rs b/build/build.rs index 4ec611af..dae1d884 100644 --- a/build/build.rs +++ b/build/build.rs @@ -30,6 +30,7 @@ mod tokens; mod vulkan; mod wire; mod wire_dbus; +mod wire_ei; mod wire_xcon; fn open(s: &str) -> io::Result> { @@ -46,6 +47,7 @@ fn open(s: &str) -> io::Result> { fn main() -> anyhow::Result<()> { wire::main()?; + wire_ei::main()?; wire_dbus::main()?; wire_xcon::main()?; enums::main()?; diff --git a/build/wire_ei.rs b/build/wire_ei.rs new file mode 100644 index 00000000..9f77a95f --- /dev/null +++ b/build/wire_ei.rs @@ -0,0 +1,679 @@ +use { + crate::{ + open, + tokens::{tokenize, Symbol, Token, TokenKind, TreeDelim}, + }, + anyhow::{bail, Context, Result}, + std::{fs::DirEntry, io::Write, os::unix::ffi::OsStrExt}, +}; + +#[derive(Debug)] +struct Lined { + #[allow(dead_code)] + line: u32, + val: T, +} + +#[derive(Debug)] +enum Type { + Id(String), + U32, + I32, + U64, + I64, + F32, + Str, + OptStr, + Fd, +} + +#[derive(Debug)] +struct Field { + name: String, + ty: Lined, +} + +#[derive(Debug)] +struct Message { + name: String, + camel_name: String, + safe_name: String, + id: u32, + fields: Vec>, + attribs: MessageAttribs, + has_reference_type: bool, +} + +#[derive(Debug, Default)] +struct MessageAttribs { + since: Option, + context: Option<&'static str>, +} + +struct Parser<'a> { + pos: usize, + tokens: &'a [Token<'a>], +} + +struct ParseResult { + requests: Vec>, + events: Vec>, +} + +impl<'a> Parser<'a> { + fn parse(&mut self) -> Result { + let mut requests = vec![]; + let mut events = vec![]; + while !self.eof() { + let (line, ty) = self.expect_ident()?; + let res = match ty.as_bytes() { + b"request" => &mut requests, + b"event" => &mut events, + _ => bail!("In line {}: Unexpected entry {:?}", line, ty), + }; + res.push(self.parse_message(res.len() as _)?); + } + Ok(ParseResult { requests, events }) + } + + fn eof(&self) -> bool { + self.pos == self.tokens.len() + } + + fn not_eof(&self) -> Result<()> { + if self.eof() { + bail!("Unexpected eof"); + } + Ok(()) + } + + fn yes_eof(&self) -> Result<()> { + if !self.eof() { + bail!( + "Unexpected trailing tokens in line {}", + self.tokens[self.pos].line + ); + } + Ok(()) + } + + fn parse_message_attribs(&mut self, attribs: &mut MessageAttribs) -> Result<()> { + let (_, tokens) = self.expect_tree(TreeDelim::Paren)?; + let mut parser = Parser { pos: 0, tokens }; + while !parser.eof() { + let (line, name) = parser.expect_ident()?; + match name { + "since" => { + parser.expect_symbol(Symbol::Equals)?; + attribs.since = Some(parser.expect_number()?.1) + } + "receiver" => attribs.context = Some("Receiver"), + "sender" => attribs.context = Some("Sender"), + _ => bail!("In line {}: Unexpected attribute {}", line, name), + } + if !parser.eof() { + parser.expect_symbol(Symbol::Comma)?; + } + } + Ok(()) + } + + fn parse_message(&mut self, id: u32) -> Result> { + let (line, name) = self.expect_ident()?; + let res: Result<_> = (|| { + self.not_eof()?; + let mut attribs = MessageAttribs::default(); + if let TokenKind::Tree { + delim: TreeDelim::Paren, + .. + } = self.tokens[self.pos].kind + { + self.parse_message_attribs(&mut attribs)?; + } + let (_, body) = self.expect_tree(TreeDelim::Brace)?; + let mut parser = Parser { + pos: 0, + tokens: body, + }; + let mut fields = vec![]; + while !parser.eof() { + fields.push(parser.parse_field()?); + } + let has_reference_type = fields.iter().any(|f| match &f.val.ty.val { + Type::OptStr | Type::Str => true, + _ => false, + }); + let safe_name = match name { + "move" => "move_", + _ => name, + }; + Ok(Lined { + line, + val: Message { + name: name.to_owned(), + camel_name: to_camel(name), + safe_name: safe_name.to_string(), + id, + fields, + attribs, + has_reference_type, + }, + }) + })(); + res.with_context(|| format!("While parsing message starting at line {}", line)) + } + + fn parse_field(&mut self) -> Result> { + let (line, name) = self.expect_ident()?; + let res: Result<_> = (|| { + self.expect_symbol(Symbol::Colon)?; + let ty = self.parse_type()?; + if !self.eof() { + self.expect_symbol(Symbol::Comma)?; + } + Ok(Lined { + line, + val: Field { + name: name.to_owned(), + ty, + }, + }) + })(); + res.with_context(|| format!("While parsing field starting at line {}", line)) + } + + fn expect_ident(&mut self) -> Result<(u32, &'a str)> { + self.not_eof()?; + let token = &self.tokens[self.pos]; + self.pos += 1; + match &token.kind { + TokenKind::Ident(id) => Ok((token.line, *id)), + k => bail!( + "In line {}: Expected identifier, found {}", + token.line, + k.name() + ), + } + } + + fn expect_number(&mut self) -> Result<(u32, u32)> { + self.not_eof()?; + let token = &self.tokens[self.pos]; + self.pos += 1; + match &token.kind { + TokenKind::Num(n) => Ok((token.line, *n)), + k => bail!( + "In line {}: Expected number, found {}", + token.line, + k.name() + ), + } + } + + fn expect_symbol(&mut self, symbol: Symbol) -> Result<()> { + self.not_eof()?; + let token = &self.tokens[self.pos]; + self.pos += 1; + match &token.kind { + TokenKind::Symbol(s) if *s == symbol => Ok(()), + k => bail!( + "In line {}: Expected {}, found {}", + token.line, + symbol.name(), + k.name() + ), + } + } + + fn expect_tree_(&mut self) -> Result<(u32, TreeDelim, &'a [Token<'a>])> { + self.not_eof()?; + let token = &self.tokens[self.pos]; + self.pos += 1; + match &token.kind { + TokenKind::Tree { delim, body } => Ok((token.line, *delim, body)), + k => bail!("In line {}: Expected tree, found {}", token.line, k.name()), + } + } + + fn expect_tree(&mut self, exp_delim: TreeDelim) -> Result<(u32, &'a [Token<'a>])> { + let (line, delim, tokens) = self.expect_tree_()?; + if delim == exp_delim { + Ok((line, tokens)) + } else { + bail!( + "In line {}: Expected {:?}-delimited tree, found {:?}-delimited tree", + line, + exp_delim, + delim.opening() + ) + } + } + + fn parse_type(&mut self) -> Result> { + self.not_eof()?; + let (line, ty) = self.expect_ident()?; + let ty = match ty.as_bytes() { + b"u32" => Type::U32, + b"i32" => Type::I32, + b"u64" => Type::U64, + b"i64" => Type::I64, + b"f32" => Type::F32, + b"str" => Type::Str, + b"optstr" => Type::OptStr, + b"fd" => Type::Fd, + b"id" => { + let (_, body) = self.expect_tree(TreeDelim::Paren)?; + let ident: Result<_> = (|| { + let mut parser = Parser { + pos: 0, + tokens: body, + }; + let id = parser.expect_ident()?; + parser.yes_eof()?; + Ok(id) + })(); + let (_, ident) = ident.with_context(|| { + format!("While parsing identifier starting in line {}", line) + })?; + Type::Id(to_camel(ident)) + } + _ => bail!("Unknown type {}", ty), + }; + Ok(Lined { line, val: ty }) + } +} + +fn parse_messages(s: &[u8]) -> Result { + let tokens = tokenize(s)?; + let mut parser = Parser { + pos: 0, + tokens: &tokens, + }; + parser.parse() +} + +fn to_camel(s: &str) -> String { + let mut last_was_underscore = true; + let mut res = String::new(); + for mut b in s.as_bytes().iter().copied() { + if b == b'_' { + last_was_underscore = true; + } else { + if last_was_underscore { + b = b.to_ascii_uppercase() + } + res.push(b as char); + last_was_underscore = false; + } + } + res +} + +fn write_type(f: &mut W, ty: &Type) -> Result<()> { + let ty = match ty { + Type::Id(id) => { + write!(f, "{}Id", id)?; + return Ok(()); + } + Type::U32 => "u32", + Type::I32 => "i32", + Type::U64 => "u64", + Type::I64 => "i64", + Type::F32 => "f32", + Type::Str => "&'a str", + Type::OptStr => "Option<&'a str>", + Type::Fd => "Rc", + }; + write!(f, "{}", ty)?; + Ok(()) +} + +fn write_field(f: &mut W, field: &Field) -> Result<()> { + write!(f, " pub {}: ", field.name)?; + write_type(f, &field.ty.val)?; + writeln!(f, ",")?; + Ok(()) +} + +fn write_message_type( + f: &mut W, + obj: &str, + message: &Message, + needs_lifetime: bool, +) -> Result<()> { + let lifetime = if needs_lifetime { "<'a>" } else { "" }; + writeln!(f, " pub struct {}{} {{", message.camel_name, lifetime)?; + writeln!(f, " pub self_id: {}Id,", obj)?; + for field in &message.fields { + write_field(f, &field.val)?; + } + writeln!(f, " }}")?; + writeln!( + f, + " impl{} std::fmt::Debug for {}{} {{", + lifetime, message.camel_name, lifetime + )?; + writeln!( + f, + " fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{" + )?; + write!(f, r#" write!(fmt, "{}("#, message.name)?; + for (i, field) in message.fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + let formatter = match &field.val.ty.val { + Type::OptStr | Type::Str | Type::Fd => "{:?}", + Type::Id(_) => "{:x}", + _ => "{}", + }; + write!(f, "{}: {}", field.val.name, formatter)?; + } + write!(f, r#")""#)?; + for field in &message.fields { + write!(f, ", self.{}", field.val.name)?; + } + writeln!(f, r")")?; + writeln!(f, " }}")?; + writeln!(f, " }}")?; + Ok(()) +} + +fn write_message(f: &mut W, obj: &str, message: &Message) -> Result<()> { + let has_reference_type = message.has_reference_type; + let uppercase = message.name.to_ascii_uppercase(); + writeln!(f)?; + writeln!(f, " pub const {}: u32 = {};", uppercase, message.id)?; + write_message_type(f, obj, message, has_reference_type)?; + let lifetime = if has_reference_type { "<'a>" } else { "" }; + let lifetime_b = if has_reference_type { "<'b>" } else { "" }; + let parser = if message.fields.len() > 0 { + "parser" + } else { + "_parser" + }; + writeln!( + f, + " impl<'a> EiRequestParser<'a> for {}{} {{", + message.camel_name, lifetime + )?; + writeln!( + f, + " type Generic<'b> = {}{};", + message.camel_name, lifetime_b, + )?; + writeln!( + f, + " fn parse({}: &mut EiMsgParser<'_, 'a>) -> Result {{", + parser + )?; + writeln!(f, " Ok(Self {{")?; + writeln!(f, " self_id: {}Id::NONE,", obj)?; + for field in &message.fields { + let p = match &field.val.ty.val { + Type::Id(_) => "object", + Type::U32 => "uint", + Type::I32 => "int", + Type::U64 => "ulong", + Type::I64 => "long", + Type::F32 => "float", + Type::OptStr => "optstr", + Type::Str => "str", + Type::Fd => "fd", + }; + writeln!(f, " {}: parser.{}()?,", field.val.name, p)?; + } + writeln!(f, " }})")?; + writeln!(f, " }}")?; + writeln!(f, " }}")?; + writeln!( + f, + " impl{} EiEventFormatter for {}{} {{", + lifetime, message.camel_name, lifetime + )?; + writeln!( + f, + " fn format(self, fmt: &mut EiMsgFormatter<'_>) {{" + )?; + writeln!(f, " fmt.header(self.self_id, {});", uppercase)?; + fn write_fmt_expr(f: &mut W, prefix: &str, ty: &Type, access: &str) -> Result<()> { + let p = match ty { + Type::Id(_) => "object", + Type::U32 => "uint", + Type::I32 => "int", + Type::U64 => "ulong", + Type::I64 => "long", + Type::F32 => "float", + Type::OptStr => "optstr", + Type::Str => "string", + Type::Fd => "fd", + }; + writeln!(f, " {prefix}fmt.{p}({access});")?; + Ok(()) + } + for field in &message.fields { + write_fmt_expr( + f, + "", + &field.val.ty.val, + &format!("self.{}", field.val.name), + )?; + } + writeln!(f, " }}")?; + writeln!(f, " fn id(&self) -> EiObjectId {{")?; + writeln!(f, " self.self_id.into()")?; + writeln!(f, " }}")?; + writeln!(f, " fn interface(&self) -> EiInterface {{")?; + writeln!(f, " {}", obj)?; + writeln!(f, " }}")?; + writeln!(f, " }}")?; + Ok(()) +} + +fn write_request_handler( + f: &mut W, + camel_obj_name: &str, + messages: &ParseResult, +) -> Result<()> { + writeln!(f)?; + writeln!( + f, + " pub trait {camel_obj_name}RequestHandler: crate::ei::ei_object::EiObject + Sized {{" + )?; + writeln!(f, " type Error: std::error::Error;")?; + for message in &messages.requests { + let msg = &message.val; + let lt = match msg.has_reference_type { + true => "<'_>", + false => "", + }; + writeln!(f)?; + writeln!( + f, + " fn {}(&self, req: {}{lt}, _slf: &Rc) -> Result<(), Self::Error>;", + msg.safe_name, msg.camel_name + )?; + } + writeln!(f)?; + writeln!(f, " #[inline(always)]")?; + writeln!(f, " fn handle_request_impl(")?; + writeln!(f, " self: Rc,")?; + writeln!(f, " client: &crate::ei::ei_client::EiClient,")?; + writeln!(f, " req: u32,")?; + writeln!( + f, + " parser: crate::utils::buffd::EiMsgParser<'_, '_>," + )?; + writeln!( + f, + " ) -> Result<(), crate::ei::ei_client::EiClientError> {{" + )?; + if messages.requests.is_empty() { + writeln!(f, " #![allow(unused_variables)]")?; + writeln!( + f, + " Err(crate::ei::ei_client::EiClientError::InvalidMethod)" + )?; + } else { + writeln!(f, " let method;")?; + writeln!( + f, + " let error: Box = match req {{" + )?; + for message in &messages.requests { + let msg = &message.val; + write!(f, " {} ", msg.id)?; + let mut have_cond = false; + if let Some(since) = msg.attribs.since { + write!(f, "if self.version() >= {since} ")?; + have_cond = true; + } + if let Some(context) = msg.attribs.context { + if have_cond { + write!(f, "&&")?; + } else { + write!(f, "if")?; + } + write!(f, " self.context() == EiContext::{context} ")?; + } + writeln!(f, "=> {{")?; + writeln!(f, " method = \"{}\";", msg.name)?; + writeln!( + f, + " match client.parse(&*self, parser) {{" + )?; + writeln!( + f, + " Ok(req) => match self.{}(req, &self) {{", + msg.safe_name + )?; + writeln!(f, " Ok(()) => return Ok(()),")?; + writeln!(f, " Err(e) => Box::new(e),")?; + writeln!(f, " }},")?; + writeln!( + f, + " Err(e) => Box::new(crate::ei::ei_client::EiParserError(e))," + )?; + writeln!(f, " }}")?; + writeln!(f, " }},")?; + } + writeln!( + f, + " _ => return Err(crate::ei::ei_client::EiClientError::InvalidMethod)," + )?; + writeln!(f, " }};")?; + writeln!( + f, + " Err(crate::ei::ei_client::EiClientError::MethodError {{" + )?; + writeln!(f, " interface: {camel_obj_name},")?; + writeln!(f, " method,")?; + writeln!(f, " error,")?; + writeln!(f, " }})")?; + } + writeln!(f, " }}")?; + writeln!(f, " }}")?; + Ok(()) +} + +fn write_file(f: &mut W, file: &DirEntry, obj_names: &mut Vec) -> Result<()> { + let file_name = file.file_name(); + let file_name = std::str::from_utf8(file_name.as_bytes())?; + println!("cargo:rerun-if-changed=wire-ei/{}", file_name); + let obj_name = file_name.split(".").next().unwrap(); + obj_names.push(obj_name.to_string()); + let camel_obj_name = to_camel(obj_name); + writeln!(f)?; + writeln!(f, "ei_id!({}Id);", camel_obj_name)?; + writeln!(f)?; + writeln!( + f, + "pub const {}: EiInterface = EiInterface(\"{}\");", + camel_obj_name, obj_name + )?; + let contents = std::fs::read(file.path())?; + let messages = parse_messages(&contents)?; + writeln!(f)?; + writeln!(f, "pub mod {} {{", obj_name)?; + writeln!(f, " use super::*;")?; + for message in messages.requests.iter().chain(messages.events.iter()) { + write_message(f, &camel_obj_name, &message.val)?; + } + write_request_handler(f, &camel_obj_name, &messages)?; + writeln!(f, "}}")?; + Ok(()) +} + +fn write_interface_versions(f: &mut W, obj_names: &[String]) -> Result<()> { + writeln!(f)?; + writeln!(f, "pub struct EiInterfaceVersions {{")?; + for obj_name in obj_names { + writeln!(f, " pub {obj_name}: EiInterfaceVersion,")?; + } + writeln!(f, "}}")?; + writeln!(f)?; + writeln!(f, "impl EiInterfaceVersions {{")?; + writeln!( + f, + " pub fn for_each(&self, mut f: impl FnMut(EiInterface, &EiInterfaceVersion)) {{" + )?; + for obj_name in obj_names { + let camel = to_camel(obj_name); + writeln!(f, " f(crate::wire_ei::{camel}, &self.{obj_name});")?; + } + writeln!(f, " }}")?; + writeln!(f)?; + writeln!( + f, + " pub fn match_(&self, name: &str, f: impl FnOnce(&EiInterfaceVersion)) -> bool {{" + )?; + writeln!(f, " match name {{")?; + for obj_name in obj_names { + writeln!(f, " \"{obj_name}\" => f(&self.{obj_name}),")?; + } + writeln!(f, " _ => return false,")?; + writeln!(f, " }}")?; + writeln!(f, " true")?; + writeln!(f, " }}")?; + for obj_name in obj_names { + writeln!(f)?; + writeln!(f, " #[allow(dead_code)]")?; + writeln!(f, " pub fn {obj_name}(&self) -> EiVersion {{")?; + writeln!(f, " self.{obj_name}.version.get()")?; + writeln!(f, " }}")?; + } + writeln!(f, "}}")?; + Ok(()) +} + +pub fn main() -> Result<()> { + let mut f = open("wire_ei.rs")?; + writeln!(f, "use std::rc::Rc;")?; + writeln!(f, "use uapi::OwnedFd;")?; + writeln!(f, "use crate::ei::{{EiContext, EiInterfaceVersion}};")?; + writeln!( + f, + "use crate::ei::ei_client::{{EiEventFormatter, EiRequestParser}};" + )?; + writeln!( + f, + "use crate::ei::ei_object::{{EiObjectId, EiInterface, EiVersion}};" + )?; + writeln!( + f, + "use crate::utils::buffd::{{EiMsgFormatter, EiMsgParser, EiMsgParserError}};" + )?; + println!("cargo:rerun-if-changed=wire-ei"); + let mut files = vec![]; + for file in std::fs::read_dir("wire-ei")? { + files.push(file?); + } + files.sort_by_key(|f| f.file_name()); + let mut obj_names = vec![]; + for file in files { + write_file(&mut f, &file, &mut obj_names) + .with_context(|| format!("While processing {}", file.path().display()))?; + } + write_interface_versions(&mut f, &obj_names).context("Could not write interface versions")?; + Ok(()) +} diff --git a/etc/jay-portals.conf b/etc/jay-portals.conf index a3e4e097..078add3e 100644 --- a/etc/jay-portals.conf +++ b/etc/jay-portals.conf @@ -1,3 +1,4 @@ [preferred] default=gtk org.freedesktop.impl.portal.ScreenCast=jay +org.freedesktop.impl.portal.RemoteDesktop=jay diff --git a/etc/jay.portal b/etc/jay.portal index 99a7d867..c6d67365 100644 --- a/etc/jay.portal +++ b/etc/jay.portal @@ -1,3 +1,3 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.jay -Interfaces=org.freedesktop.impl.portal.ScreenCast; +Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop; diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index cc2bf7f3..fa70777d 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -992,6 +992,10 @@ impl Client { keymap } + pub fn set_ei_socket_enabled(&self, enabled: bool) { + self.send(&ClientMessage::SetEiSocketEnabled { enabled }) + } + pub fn latch(&self, seat: Seat, f: F) { if !self.feat_mod_mask.get() { log::error!("compositor does not support latching"); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index e53d2350..1a91eb66 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -506,6 +506,9 @@ pub enum ClientMessage<'a> { device: InputDevice, matrix: [[f32; 3]; 2], }, + SetEiSocketEnabled { + enabled: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 349a868a..702ca31f 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -564,3 +564,12 @@ pub enum SwitchEvent { /// event of this kind is generated. ConvertedToTablet, } + +/// Enables or disables the unauthenticated libei socket. +/// +/// Even if the socket is disabled, application can still request access via the portal. +/// +/// The default is `false`. +pub fn set_libei_socket_enabled(enabled: bool) { + get!().set_ei_socket_enabled(enabled); +} diff --git a/release-notes.md b/release-notes.md index e7e1abd0..ec48660a 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,8 @@ - Add support for adaptive sync. - Add support for tearing. - Add support for touch input. +- Add support for libei. +- Add support for RemoteDesktop portal. # 1.4.0 (2024-07-07) diff --git a/src/backend.rs b/src/backend.rs index 1ede68ed..9816bff3 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -418,7 +418,9 @@ pub enum InputEvent { time_usec: u64, id: i32, }, - TouchFrame, + TouchFrame { + time_usec: u64, + }, } pub enum DrmEvent { diff --git a/src/backends/metal/input.rs b/src/backends/metal/input.rs index 8f638a79..49a137b1 100644 --- a/src/backends/metal/input.rs +++ b/src/backends/metal/input.rs @@ -582,7 +582,9 @@ impl MetalBackend { } fn handle_touch_frame(self: &Rc, event: LibInputEvent) { - let (_, dev) = unpack!(self, event, touch_event); - dev.event(InputEvent::TouchFrame) + let (event, dev) = unpack!(self, event, touch_event); + dev.event(InputEvent::TouchFrame { + time_usec: event.time_usec(), + }) } } diff --git a/src/client.rs b/src/client.rs index 4b316aa1..19ba92cc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,12 +18,11 @@ use { errorfmt::ErrorFmt, numcell::NumCell, pending_serial::PendingSerial, - trim::AsciiTrim, + pid_info::{get_pid_info, get_socket_creds, PidInfo}, }, wire::WlRegistryId, }, ahash::AHashMap, - bstr::ByteSlice, std::{ cell::{Cell, RefCell}, collections::VecDeque, @@ -123,22 +122,8 @@ impl Clients { effective_caps: ClientCaps, bounding_caps: ClientCaps, ) -> Result<(), ClientError> { - let (uid, pid) = { - let mut cred = c::ucred { - pid: 0, - uid: 0, - gid: 0, - }; - match uapi::getsockopt(socket.raw(), c::SOL_SOCKET, c::SO_PEERCRED, &mut cred) { - Ok(_) => (cred.uid, cred.pid), - Err(e) => { - log::error!( - "Cannot determine peer credentials of new connection: {:?}", - crate::utils::oserror::OsError::from(e) - ); - return Ok(()); - } - } + let Some((uid, pid)) = get_socket_creds(&socket) else { + return Ok(()); }; self.spawn2( id, @@ -274,12 +259,6 @@ pub trait RequestParser<'a>: Debug + Sized { fn parse(parser: &mut MsgParser<'_, 'a>) -> Result; } -pub struct PidInfo { - pub _uid: c::uid_t, - pub pid: c::pid_t, - pub comm: String, -} - pub struct Client { pub id: ClientId, pub state: Rc, @@ -516,18 +495,3 @@ pub trait WaylandObjectLookup: Copy + Into { fn lookup(client: &Client, id: Self) -> Option>; } - -fn get_pid_info(uid: c::uid_t, pid: c::pid_t) -> PidInfo { - let comm = match std::fs::read(format!("/proc/{}/comm", pid)) { - Ok(name) => name.trim().as_bstr().to_string(), - Err(e) => { - log::warn!("Could not read `comm` of pid {}: {}", pid, ErrorFmt(e)); - "Unknown".to_string() - } - }; - PidInfo { - _uid: uid, - pid, - comm, - } -} diff --git a/src/compositor.rs b/src/compositor.rs index ab335892..ee3d617a 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -15,6 +15,7 @@ use { config::ConfigProxy, damage::{visualize_damage, DamageVisualizer}, dbus::Dbus, + ei::ei_client::EiClients, forker, globals::Globals, ifs::{ @@ -109,6 +110,7 @@ pub enum CompositorError { } pub const WAYLAND_DISPLAY: &str = "WAYLAND_DISPLAY"; +pub const LIBEI_SOCKET: &str = "LIBEI_SOCKET"; pub const DISPLAY: &str = "DISPLAY"; const STATIC_VARS: &[(&str, &str)] = &[ @@ -251,6 +253,11 @@ fn start_compositor2( default_vrr_mode: Cell::new(VrrMode::NEVER), default_vrr_cursor_hz: Cell::new(None), default_tearing_mode: Cell::new(TearingMode::VARIANT_3), + ei_acceptor: Default::default(), + ei_acceptor_future: Default::default(), + enable_ei_acceptor: Default::default(), + ei_clients: EiClients::new(), + slow_ei_clients: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -305,6 +312,7 @@ async fn start_compositor3(state: Rc, test_future: Option) { if state.create_default_seat.get() && state.globals.seats.is_empty() { state.create_seat(DEFAULT_SEAT_NAME); } + state.update_ei_acceptor(); let _geh = start_global_event_handlers(&state, &backend); state.start_xwayland(); @@ -351,6 +359,7 @@ fn start_global_event_handlers( eng.spawn2(Phase::Present, perform_toplevel_screencasts(state.clone())), eng.spawn2(Phase::PostLayout, perform_screencast_realloc(state.clone())), eng.spawn2(Phase::PostLayout, visualize_damage(state.clone())), + eng.spawn(tasks::handle_slow_ei_clients(state.clone())), ] } diff --git a/src/config/handler.rs b/src/config/handler.rs index 5b9e5396..a21fcea9 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -699,6 +699,11 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_ei_socket_enabled(&self, enabled: bool) { + self.state.enable_ei_acceptor.set(enabled); + self.state.update_ei_acceptor(); + } + fn handle_get_workspace(&self, name: &str) { let name = Rc::new(name.to_owned()); let ws = match self.workspaces_by_name.get(&name) { @@ -1295,6 +1300,7 @@ impl ConfigProxyHandler { Some(f) => f, _ => return Err(CphError::NoForker), }; + let env = env.into_iter().map(|(k, v)| (k, Some(v))).collect(); forker.spawn(prog.to_string(), args, env, fds); Ok(()) } @@ -1910,6 +1916,9 @@ impl ConfigProxyHandler { ClientMessage::SetCalibrationMatrix { device, matrix } => self .handle_set_calibration_matrix(device, matrix) .wrn("set_calibration_matrix")?, + ClientMessage::SetEiSocketEnabled { enabled } => { + self.handle_set_ei_socket_enabled(enabled) + } } Ok(()) } diff --git a/src/ei.rs b/src/ei.rs new file mode 100644 index 00000000..16d2a638 --- /dev/null +++ b/src/ei.rs @@ -0,0 +1,33 @@ +use {crate::ei::ei_object::EiVersion, std::cell::Cell}; + +pub mod ei_acceptor; +pub mod ei_client; +pub mod ei_ifs; +pub mod ei_object; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EiContext { + Sender, + Receiver, +} + +pub struct EiInterfaceVersion { + pub server_max_version: EiVersion, + pub client_max_version: Cell, + pub version: Cell, +} + +impl EiInterfaceVersion { + pub fn new(server_max_version: u32) -> Self { + Self { + server_max_version: EiVersion(server_max_version), + client_max_version: Cell::new(EiVersion(0)), + version: Cell::new(EiVersion(0)), + } + } + + pub fn set_client_version(&self, version: EiVersion) { + self.client_max_version.set(version); + self.version.set(self.server_max_version.min(version)); + } +} diff --git a/src/ei/ei_acceptor.rs b/src/ei/ei_acceptor.rs new file mode 100644 index 00000000..b3b2d211 --- /dev/null +++ b/src/ei/ei_acceptor.rs @@ -0,0 +1,155 @@ +use { + crate::{ + async_engine::SpawnedFuture, + state::State, + utils::{errorfmt::ErrorFmt, oserror::OsError, xrd::xrd}, + }, + std::rc::Rc, + thiserror::Error, + uapi::{c, format_ustr, Errno, OwnedFd, Ustring}, +}; + +#[derive(Debug, Error)] +pub enum EiAcceptorError { + #[error("XDG_RUNTIME_DIR is not set")] + XrdNotSet, + #[error("XDG_RUNTIME_DIR ({0:?}) is too long to form a unix socket address")] + XrdTooLong(String), + #[error("Could not create a libei socket")] + SocketFailed(#[source] OsError), + #[error("Could not stat the existing socket")] + SocketStat(#[source] OsError), + #[error("Could not start listening for incoming connections")] + ListenFailed(#[source] OsError), + #[error("Could not open the lock file")] + OpenLockFile(#[source] OsError), + #[error("Could not lock the lock file")] + LockLockFile(#[source] OsError), + #[error("Could not bind the socket to an address")] + BindFailed(#[source] OsError), + #[error("All libei addresses in the range 0..1000 are already in use")] + AddressesInUse, +} + +pub struct EiAcceptor { + socket: EiAllocatedSocket, +} + +struct EiAllocatedSocket { + // eis-x + name: String, + // /run/user/1000/eis-x + path: Ustring, + insecure: Rc, + // /run/user/1000/eis-x.lock + lock_path: Ustring, + _lock_fd: OwnedFd, +} + +impl Drop for EiAllocatedSocket { + fn drop(&mut self) { + let _ = uapi::unlink(&self.path); + let _ = uapi::unlink(&self.lock_path); + } +} + +fn bind_socket( + insecure: &Rc, + xrd: &str, + id: u32, +) -> Result { + let mut addr: c::sockaddr_un = uapi::pod_zeroed(); + addr.sun_family = c::AF_UNIX as _; + let name = format!("eis-{}", id); + let path = format_ustr!("{}/{}", xrd, name); + let lock_path = format_ustr!("{}.lock", path.display()); + if path.len() + 1 > addr.sun_path.len() { + return Err(EiAcceptorError::XrdTooLong(xrd.to_string())); + } + let lock_fd = match uapi::open(&*lock_path, c::O_CREAT | c::O_CLOEXEC | c::O_RDWR, 0o644) { + Ok(l) => l, + Err(e) => return Err(EiAcceptorError::OpenLockFile(e.into())), + }; + if let Err(e) = uapi::flock(lock_fd.raw(), c::LOCK_EX | c::LOCK_NB) { + return Err(EiAcceptorError::LockLockFile(e.into())); + } + match uapi::lstat(&path) { + Ok(_) => { + log::info!("Unlinking {}", path.display()); + let _ = uapi::unlink(&path); + } + Err(Errno(c::ENOENT)) => {} + Err(e) => return Err(EiAcceptorError::SocketStat(e.into())), + } + let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]); + sun_path[..path.len()].copy_from_slice(path.as_bytes()); + sun_path[path.len()] = 0; + if let Err(e) = uapi::bind(insecure.raw(), &addr) { + return Err(EiAcceptorError::BindFailed(e.into())); + } + Ok(EiAllocatedSocket { + name, + path, + insecure: insecure.clone(), + lock_path, + _lock_fd: lock_fd, + }) +} + +fn allocate_socket() -> Result { + let xrd = match xrd() { + Some(d) => d, + _ => return Err(EiAcceptorError::XrdNotSet), + }; + let socket = match uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) { + Ok(f) => Rc::new(f), + Err(e) => return Err(EiAcceptorError::SocketFailed(e.into())), + }; + for i in 1..1000 { + match bind_socket(&socket, &xrd, i) { + Ok(s) => return Ok(s), + Err(e) => { + log::warn!("Cannot use the eis-{} socket: {}", i, ErrorFmt(e)); + } + } + } + Err(EiAcceptorError::AddressesInUse) +} + +impl EiAcceptor { + pub fn spawn( + state: &Rc, + ) -> Result<(Rc, SpawnedFuture<()>), EiAcceptorError> { + let socket = allocate_socket()?; + log::info!("bound to socket {}", socket.path.display()); + if let Err(e) = uapi::listen(socket.insecure.raw(), 4096) { + return Err(EiAcceptorError::ListenFailed(e.into())); + } + let acc = Rc::new(EiAcceptor { socket }); + let future = state + .eng + .spawn(accept(acc.socket.insecure.clone(), state.clone())); + Ok((acc, future)) + } + + pub fn socket_name(&self) -> &str { + &self.socket.name + } +} + +async fn accept(fd: Rc, state: Rc) { + loop { + let fd = match state.ring.accept(&fd, c::SOCK_CLOEXEC).await { + Ok(fd) => fd, + Err(e) => { + log::error!("Could not accept a client: {}", ErrorFmt(e)); + break; + } + }; + if let Err(e) = state.ei_clients.spawn(&state, fd) { + log::error!("Could not spawn a client: {}", ErrorFmt(e)); + break; + } + } + state.ring.stop(); +} diff --git a/src/ei/ei_client.rs b/src/ei/ei_client.rs new file mode 100644 index 00000000..9d69e56c --- /dev/null +++ b/src/ei/ei_client.rs @@ -0,0 +1,304 @@ +pub use crate::ei::ei_client::ei_error::{EiClientError, EiParserError}; +use { + crate::{ + async_engine::SpawnedFuture, + client::ClientId, + ei::{ + ei_client::ei_objects::EiObjects, + ei_ifs::{ei_connection::EiConnection, ei_handshake::EiHandshake}, + ei_object::{EiInterface, EiObject, EiObjectId}, + EiContext, EiInterfaceVersion, + }, + ifs::wl_seat::WlSeatGlobal, + leaks::Tracker, + state::State, + utils::{ + asyncevent::AsyncEvent, + buffd::{EiMsgFormatter, EiMsgParser, EiMsgParserError, OutBufferSwapchain}, + clonecell::CloneCell, + debug_fn::debug_fn, + errorfmt::ErrorFmt, + numcell::NumCell, + pid_info::{get_pid_info, get_socket_creds, PidInfo}, + }, + wire_ei::EiInterfaceVersions, + }, + ahash::AHashMap, + std::{ + cell::{Cell, RefCell}, + error::Error, + fmt::Debug, + mem, + ops::DerefMut, + rc::Rc, + }, + uapi::OwnedFd, +}; + +mod ei_error; +mod ei_objects; +mod ei_tasks; + +pub struct EiClients { + pub clients: RefCell>, + shutdown_clients: RefCell>, +} + +impl EiClients { + pub fn new() -> Self { + Self { + clients: Default::default(), + shutdown_clients: Default::default(), + } + } + + pub fn announce_seat(&self, seat: &Rc) { + for ei_client in self.clients.borrow().values() { + if let Some(connection) = ei_client.data.connection.get() { + connection.announce_seat(&seat); + } + } + } + + pub fn clear(&self) { + mem::take(self.clients.borrow_mut().deref_mut()); + mem::take(self.shutdown_clients.borrow_mut().deref_mut()); + } + + pub fn spawn(&self, global: &Rc, socket: Rc) -> Result<(), EiClientError> { + let Some((uid, pid)) = get_socket_creds(&socket) else { + return Ok(()); + }; + let pid_info = get_pid_info(uid, pid); + self.spawn2(global, socket, Some(pid_info), None)?; + Ok(()) + } + + pub fn spawn2( + &self, + global: &Rc, + socket: Rc, + pid_info: Option, + app_id: Option, + ) -> Result, EiClientError> { + let versions = EiInterfaceVersions { + ei_button: EiInterfaceVersion::new(1), + ei_callback: EiInterfaceVersion::new(1), + ei_connection: EiInterfaceVersion::new(1), + ei_device: EiInterfaceVersion::new(2), + ei_handshake: EiInterfaceVersion::new(1), + ei_keyboard: EiInterfaceVersion::new(1), + ei_pingpong: EiInterfaceVersion::new(1), + ei_pointer: EiInterfaceVersion::new(1), + ei_pointer_absolute: EiInterfaceVersion::new(1), + ei_scroll: EiInterfaceVersion::new(1), + ei_seat: EiInterfaceVersion::new(1), + ei_touchscreen: EiInterfaceVersion::new(1), + }; + let data = Rc::new(EiClient { + id: global.clients.id(), + state: global.clone(), + context: Cell::new(EiContext::Receiver), + connection: Default::default(), + checking_queue_size: Cell::new(false), + socket, + objects: EiObjects::new(), + swapchain: Default::default(), + flush_request: Default::default(), + shutdown: Default::default(), + tracker: Default::default(), + pid_info, + disconnect_announced: Cell::new(false), + versions, + name: Default::default(), + app_id, + last_serial: Default::default(), + }); + track!(data, data); + let handshake = Rc::new(EiHandshake::new(&data)); + track!(data, handshake); + handshake.send_handshake_version(); + data.objects.add_handshake(&handshake); + let client = EiClientHolder { + _handler: global.eng.spawn(ei_tasks::ei_client(data.clone())), + data: data.clone(), + }; + log::info!( + "Client {} connected{:?}", + data.id, + debug_fn(|fmt| { + if let Some(p) = &data.pid_info { + write!(fmt, ", pid: {}, uid: {}, comm: {:?}", p.pid, p.uid, p.comm)?; + } + if let Some(app_id) = &data.app_id { + write!(fmt, ", app-id: {app_id:?}")?; + } + Ok(()) + }), + ); + self.clients.borrow_mut().insert(client.data.id, client); + Ok(data) + } + + pub fn kill(&self, client: ClientId) { + log::info!("Removing client {}", client); + if self.clients.borrow_mut().remove(&client).is_none() { + self.shutdown_clients.borrow_mut().remove(&client); + } + } + + pub fn shutdown(&self, client_id: ClientId) { + if let Some(client) = self.clients.borrow_mut().remove(&client_id) { + log::info!("Shutting down client {}", client.data.id); + client.data.shutdown.trigger(); + client.data.flush_request.trigger(); + self.shutdown_clients.borrow_mut().insert(client_id, client); + } + } +} + +impl Drop for EiClients { + fn drop(&mut self) { + let _clients1 = mem::take(&mut *self.clients.borrow_mut()); + let _clients2 = mem::take(&mut *self.shutdown_clients.borrow_mut()); + } +} + +pub struct EiClientHolder { + pub data: Rc, + _handler: SpawnedFuture<()>, +} + +impl Drop for EiClientHolder { + fn drop(&mut self) { + self.data.objects.destroy(); + self.data.flush_request.clear(); + self.data.shutdown.clear(); + self.data.connection.take(); + } +} + +pub trait EiEventFormatter: Debug { + fn format(self, fmt: &mut EiMsgFormatter<'_>); + fn id(&self) -> EiObjectId; + fn interface(&self) -> EiInterface; +} + +pub trait EiRequestParser<'a>: Debug + Sized { + type Generic<'b>: EiRequestParser<'b>; + fn parse(parser: &mut EiMsgParser<'_, 'a>) -> Result; +} + +pub struct EiClient { + pub id: ClientId, + pub state: Rc, + pub context: Cell, + pub connection: CloneCell>>, + checking_queue_size: Cell, + socket: Rc, + pub objects: EiObjects, + swapchain: Rc>, + flush_request: AsyncEvent, + shutdown: AsyncEvent, + pub tracker: Tracker, + pub pid_info: Option, + pub disconnect_announced: Cell, + pub versions: EiInterfaceVersions, + pub name: RefCell>, + pub app_id: Option, + pub last_serial: NumCell, +} + +impl EiClient { + pub fn new_id>(&self) -> T { + self.objects.id() + } + + pub fn serial(&self) -> u32 { + (self.last_serial.fetch_add(1) + 1) as u32 + } + + pub fn last_serial(&self) -> u32 { + self.last_serial.get() as u32 + } + + pub fn error(&self, message: impl Error) { + let msg = ErrorFmt(message).to_string(); + log::error!("Client {}: A fatal error occurred: {}", self.id, msg); + match self.connection.get() { + Some(d) => { + d.send_disconnected(Some(&msg)); + self.state.clients.shutdown(self.id); + } + _ => { + self.state.clients.kill(self.id); + } + } + } + + pub fn parse<'a, R: EiRequestParser<'a>>( + &self, + obj: &impl EiObject, + mut parser: EiMsgParser<'_, 'a>, + ) -> Result { + let res = R::parse(&mut parser)?; + parser.eof()?; + log::trace!( + "Client {} -> {}@{:x}.{:?}", + self.id, + obj.interface().name(), + obj.id(), + res + ); + Ok(res) + } + + pub fn event(self: &Rc, event: T) { + log::trace!( + "Client {} <= {}@{:x}.{:?}", + self.id, + event.interface().name(), + event.id(), + event, + ); + let mut fds = vec![]; + let mut swapchain = self.swapchain.borrow_mut(); + let mut fmt = EiMsgFormatter::new(&mut swapchain.cur, &mut fds); + event.format(&mut fmt); + fmt.write_len(); + if swapchain.cur.is_full() { + swapchain.commit(); + if swapchain.exceeds_limit() { + if !self.checking_queue_size.replace(true) { + self.state.slow_ei_clients.push(self.clone()); + } + } + } + self.flush_request.trigger(); + } + + pub async fn check_queue_size(&self) { + if self.swapchain.borrow_mut().exceeds_limit() { + self.state.eng.yield_now().await; + if self.swapchain.borrow_mut().exceeds_limit() { + log::error!("Client {} is too slow at fetching events", self.id); + self.state.ei_clients.kill(self.id); + return; + } + } + self.checking_queue_size.set(false); + } + + pub fn add_client_obj(&self, obj: &Rc) -> Result<(), EiClientError> { + self.objects.add_client_object(obj.clone())?; + Ok(()) + } + + pub fn add_server_obj(&self, obj: &Rc) { + self.objects.add_server_object(obj.clone()); + } + + pub fn remove_obj(self: &Rc, obj: &T) -> Result<(), EiClientError> { + self.objects.remove_obj(obj.id()) + } +} diff --git a/src/ei/ei_client/ei_error.rs b/src/ei/ei_client/ei_error.rs new file mode 100644 index 00000000..df23f458 --- /dev/null +++ b/src/ei/ei_client/ei_error.rs @@ -0,0 +1,45 @@ +use { + crate::{ + ei::ei_object::{EiInterface, EiObjectId}, + utils::buffd::{BufFdError, EiMsgParserError}, + }, + std::error::Error, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum EiClientError { + #[error("An error occurred reading from/writing to the client")] + Io(#[from] BufFdError), + #[error("An error occurred while processing a request")] + RequestError(#[source] Box), + #[error("Client tried to invoke a non-existent method")] + InvalidMethod, + #[error("The message size is < 16")] + MessageSizeTooSmall, + #[error("The message size is > 2^16")] + MessageSizeTooLarge, + #[error("The size of the message is not a multiple of 4")] + UnalignedMessage, + #[error("The object id is unknown")] + UnknownId, + #[error("Client tried to access non-existent object {0}")] + InvalidObject(EiObjectId), + #[error("The id is already in use")] + IdAlreadyInUse, + #[error("The client object id is out of bounds")] + ClientIdOutOfBounds, + #[error("Could not process a `{}.{}` request", .interface.name(), .method)] + MethodError { + interface: EiInterface, + method: &'static str, + #[source] + error: Box, + }, + #[error("Could not add object {0} to the client")] + AddObjectError(EiObjectId, #[source] Box), +} + +#[derive(Debug, Error)] +#[error("Parsing failed")] +pub struct EiParserError(#[source] pub EiMsgParserError); diff --git a/src/ei/ei_client/ei_objects.rs b/src/ei/ei_client/ei_objects.rs new file mode 100644 index 00000000..56499328 --- /dev/null +++ b/src/ei/ei_client/ei_objects.rs @@ -0,0 +1,83 @@ +use { + crate::{ + ei::{ + ei_client::ei_error::EiClientError, + ei_ifs::ei_handshake::EiHandshake, + ei_object::{EiObject, EiObjectId}, + }, + utils::{copyhashmap::CopyHashMap, numcell::NumCell}, + }, + std::rc::Rc, +}; + +pub struct EiObjects { + registry: CopyHashMap>, + next_sever_id: NumCell, +} + +pub const MIN_SERVER_ID: u64 = 0xff00_0000_0000_0000; + +impl EiObjects { + pub fn new() -> Self { + Self { + registry: Default::default(), + next_sever_id: NumCell::new(MIN_SERVER_ID), + } + } + + pub fn destroy(&self) { + for obj in self.registry.lock().values_mut() { + obj.break_loops(); + } + self.registry.clear(); + } + + pub fn id(&self) -> T + where + EiObjectId: Into, + { + EiObjectId::from_raw(self.next_sever_id.fetch_add(1)).into() + } + + pub fn get_obj(&self, id: EiObjectId) -> Option> { + self.registry.get(&id) + } + + pub fn add_server_object(&self, obj: Rc) { + let id = obj.id(); + assert!(id.raw() >= MIN_SERVER_ID); + assert!(!self.registry.contains(&id)); + self.registry.set(id, obj.clone()); + } + + pub fn add_handshake(&self, obj: &Rc) { + assert_eq!(obj.id.raw(), 0); + assert!(self.registry.is_empty()); + self.registry.set(obj.id.into(), obj.clone()); + } + + pub fn add_client_object(&self, obj: Rc) -> Result<(), EiClientError> { + let id = obj.id(); + let res = (|| { + if id.raw() == 0 || id.raw() >= MIN_SERVER_ID { + return Err(EiClientError::ClientIdOutOfBounds); + } + if self.registry.contains(&id) { + return Err(EiClientError::IdAlreadyInUse); + } + self.registry.set(id, obj.clone()); + Ok(()) + })(); + if let Err(e) = res { + return Err(EiClientError::AddObjectError(id, Box::new(e))); + } + Ok(()) + } + + pub fn remove_obj(&self, id: EiObjectId) -> Result<(), EiClientError> { + match self.registry.remove(&id) { + Some(_) => Ok(()), + _ => Err(EiClientError::UnknownId), + } + } +} diff --git a/src/ei/ei_client/ei_tasks.rs b/src/ei/ei_client/ei_tasks.rs new file mode 100644 index 00000000..bffa79f2 --- /dev/null +++ b/src/ei/ei_client/ei_tasks.rs @@ -0,0 +1,141 @@ +use { + crate::{ + async_engine::Phase, + ei::{ + ei_client::{ei_error::EiClientError, EiClient}, + ei_object::EiObjectId, + }, + utils::{ + buffd::{BufFdIn, BufFdOut, EiMsgParser}, + errorfmt::ErrorFmt, + vec_ext::VecExt, + }, + }, + futures_util::{select, FutureExt}, + std::{collections::VecDeque, mem, rc::Rc, time::Duration}, +}; + +pub async fn ei_client(data: Rc) { + let mut recv = data.state.eng.spawn(receive(data.clone())).fuse(); + let mut shutdown = data.shutdown.triggered().fuse(); + let _send = data.state.eng.spawn2(Phase::PostLayout, send(data.clone())); + select! { + _ = recv => { }, + _ = shutdown => { }, + } + drop(recv); + data.flush_request.trigger(); + match data.state.wheel.timeout(5000).await { + Ok(_) => { + log::error!("Could not shut down client {} within 5 seconds", data.id); + } + Err(e) => { + log::error!("Could not create a timeout: {}", ErrorFmt(e)); + } + } + data.state.ei_clients.kill(data.id); +} + +async fn receive(data: Rc) { + let recv = async { + let mut buf = BufFdIn::new(&data.socket, &data.state.ring); + let mut data_buf = Vec::::new(); + loop { + let mut hdr = [0u32; 4]; + buf.read_full(&mut hdr[..]).await?; + #[cfg(target_endian = "little")] + let obj_id = (hdr[0] as u64) | ((hdr[1] as u64) << 32); + #[cfg(target_endian = "big")] + let obj_id = (hdr[1] as u64) | ((hdr[0] as u64) << 32); + let obj_id = EiObjectId::from_raw(obj_id); + let len = hdr[2] as usize; + let request = hdr[3]; + if len < 16 { + return Err(EiClientError::MessageSizeTooSmall); + } + if len > (u16::MAX as usize) { + return Err(EiClientError::MessageSizeTooLarge); + } + if len % 4 != 0 { + return Err(EiClientError::UnalignedMessage); + } + let len = (len - 16) / 4; + data_buf.clear(); + data_buf.reserve(len); + let unused = data_buf.split_at_spare_mut_ext().1; + buf.read_full(&mut unused[..len]).await?; + unsafe { + data_buf.set_len(len); + } + let obj = match data.objects.get_obj(obj_id) { + Some(obj) => obj, + _ => match data.connection.get() { + None => { + return Err(EiClientError::InvalidObject(obj_id)); + } + Some(c) => { + c.send_invalid_object(obj_id); + continue; + } + }, + }; + let parser = EiMsgParser::new(&mut buf, &data_buf[..]); + if let Err(e) = obj.handle_request(&data, request, parser) { + return Err(EiClientError::RequestError(Box::new(e))); + } + } + }; + let res: Result<(), EiClientError> = recv.await; + if let Err(e) = res { + if data.disconnect_announced.get() { + log::info!("Client {} terminated the connection", data.id); + data.state.ei_clients.kill(data.id); + } else { + let e = ErrorFmt(e); + log::error!( + "An error occurred while trying to handle a message from client {}: {}", + data.id, + e + ); + if let Some(c) = data.connection.get() { + c.send_disconnected(Some(&e.to_string())); + data.state.ei_clients.shutdown(data.id); + } else { + data.state.ei_clients.kill(data.id); + } + } + } +} + +async fn send(data: Rc) { + let send = async { + let mut out = BufFdOut::new(&data.socket, &data.state.ring); + let mut buffers = VecDeque::new(); + loop { + data.flush_request.triggered().await; + { + let mut swapchain = data.swapchain.borrow_mut(); + swapchain.commit(); + mem::swap(&mut swapchain.pending, &mut buffers); + } + let timeout = data.state.now() + Duration::from_millis(5000); + while let Some(mut cur) = buffers.pop_front() { + out.flush(&mut cur, timeout).await?; + data.swapchain.borrow_mut().free.push(cur); + } + } + }; + let res: Result<(), EiClientError> = send.await; + if let Err(e) = res { + if data.disconnect_announced.get() { + log::info!("Client {} terminated the connection", data.id); + } else { + log::error!( + "An error occurred while sending data to client {}: {}", + data.id, + ErrorFmt(e) + ); + } + } + data.state.ei_clients.kill(data.id); +} diff --git a/src/ei/ei_ifs.rs b/src/ei/ei_ifs.rs new file mode 100644 index 00000000..029103ef --- /dev/null +++ b/src/ei/ei_ifs.rs @@ -0,0 +1,44 @@ +macro_rules! ei_device_interface { + ($camel:ident, $snake:ident, $field:ident) => { + impl EiDeviceInterface for $camel { + fn new(device: &Rc, version: EiVersion) -> Rc { + let v = Rc::new(Self { + id: device.client.new_id(), + client: device.client.clone(), + tracker: Default::default(), + version, + device: device.clone(), + }); + track!(v.client, v); + v + } + + fn destroy(&self) -> Result<(), EiClientError> { + self.send_destroyed(self.client.serial()); + self.client.remove_obj(self)?; + self.device.seat.$field.take(); + Ok(()) + } + + fn send_destroyed(&self, serial: u32) { + self.client.event(crate::wire_ei::$snake::Destroyed { + self_id: self.id, + serial, + }); + } + } + }; +} + +pub mod ei_button; +pub mod ei_callback; +pub mod ei_connection; +pub mod ei_device; +pub mod ei_handshake; +pub mod ei_keyboard; +pub mod ei_pingpong; +pub mod ei_pointer; +pub mod ei_pointer_absolute; +pub mod ei_scroll; +pub mod ei_seat; +pub mod ei_touchscreen; diff --git a/src/ei/ei_ifs/ei_button.rs b/src/ei/ei_ifs/ei_button.rs new file mode 100644 index 00000000..fc3aa200 --- /dev/null +++ b/src/ei/ei_ifs/ei_button.rs @@ -0,0 +1,72 @@ +use { + crate::{ + backend::KeyState, + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::ei_device::{EiDevice, EiDeviceInterface}, + ei_object::{EiObject, EiVersion}, + }, + leaks::Tracker, + wire_ei::{ + ei_button::{ClientButton, EiButtonRequestHandler, Release, ServerButton}, + EiButtonId, + }, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct EiButton { + pub id: EiButtonId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub device: Rc, +} + +ei_device_interface!(EiButton, ei_button, button); + +impl EiButton { + pub fn send_button(&self, button: u32, state: KeyState) { + self.client.event(ServerButton { + self_id: self.id, + button, + state: state as _, + }); + } +} + +impl EiButtonRequestHandler for EiButton { + type Error = EiButtonError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroy()?; + Ok(()) + } + + fn client_button(&self, req: ClientButton, _slf: &Rc) -> Result<(), Self::Error> { + let pressed = match req.state { + 0 => KeyState::Released, + 1 => KeyState::Pressed, + _ => return Err(EiButtonError::InvalidButtonState(req.state)), + }; + self.device.button_changes.push((req.button, pressed)); + Ok(()) + } +} + +ei_object_base! { + self = EiButton; + version = self.version; +} + +impl EiObject for EiButton {} + +#[derive(Debug, Error)] +pub enum EiButtonError { + #[error(transparent)] + EiClientError(Box), + #[error("Invalid button state {0}")] + InvalidButtonState(u32), +} +efrom!(EiButtonError, EiClientError); diff --git a/src/ei/ei_ifs/ei_callback.rs b/src/ei/ei_ifs/ei_callback.rs new file mode 100644 index 00000000..f65ed15e --- /dev/null +++ b/src/ei/ei_ifs/ei_callback.rs @@ -0,0 +1,49 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_object::{EiObject, EiVersion}, + }, + leaks::Tracker, + wire_ei::{ + ei_callback::{Done, EiCallbackRequestHandler}, + EiCallbackId, + }, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct EiCallback { + pub id: EiCallbackId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, +} + +impl EiCallback { + pub fn send_done(&self, callback_data: u64) { + self.client.event(Done { + self_id: self.id, + callback_data, + }); + } +} + +impl EiCallbackRequestHandler for EiCallback { + type Error = EiCallbackError; +} + +ei_object_base! { + self = EiCallback; + version = self.version; +} + +impl EiObject for EiCallback {} + +#[derive(Debug, Error)] +pub enum EiCallbackError { + #[error(transparent)] + EiClientError(Box), +} +efrom!(EiCallbackError, EiClientError); diff --git a/src/ei/ei_ifs/ei_connection.rs b/src/ei/ei_ifs/ei_connection.rs new file mode 100644 index 00000000..510995a3 --- /dev/null +++ b/src/ei/ei_ifs/ei_connection.rs @@ -0,0 +1,160 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::{ + ei_callback::EiCallback, + ei_seat::{ + EiSeat, EI_CAP_BUTTON, EI_CAP_KEYBOARD, EI_CAP_POINTER, + EI_CAP_POINTER_ABSOLUTE, EI_CAP_SCROLL, EI_CAP_TOUCHSCREEN, + }, + }, + ei_object::{EiObject, EiObjectId, EiVersion}, + EiContext, + }, + ifs::wl_seat::WlSeatGlobal, + leaks::Tracker, + wire_ei::{ + ei_connection::{ + Disconnect, Disconnected, EiConnectionRequestHandler, InvalidObject, Seat, + }, + EiButton, EiConnectionId, EiKeyboard, EiPointer, EiPointerAbsolute, EiScroll, + EiTouchscreen, + }, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct EiConnection { + pub id: EiConnectionId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, +} + +impl EiConnection { + pub fn send_invalid_object(&self, id: EiObjectId) { + self.client.event(InvalidObject { + self_id: self.id, + last_serial: self.client.last_serial(), + invalid_id: id, + }); + } + + pub fn send_disconnected(&self, error: Option<&str>) { + self.client.event(Disconnected { + self_id: self.id, + last_serial: self.client.last_serial(), + reason: error.is_some() as _, + explanation: error, + }); + } + + pub fn send_seat(&self, seat: &EiSeat) { + self.client.event(Seat { + self_id: self.id, + seat: seat.id, + version: seat.version.0, + }); + } + + pub fn announce_seat(&self, seat: &Rc) { + let version = self.client.versions.ei_seat.version.get(); + if version == EiVersion(0) { + return; + } + let xkb_state_id = match self.context() { + EiContext::Sender => seat.seat_xkb_state().borrow().id, + EiContext::Receiver => seat.latest_xkb_state().borrow().id, + }; + let seat = Rc::new(EiSeat { + id: self.client.new_id(), + client: self.client.clone(), + tracker: Default::default(), + version, + seat: seat.clone(), + capabilities: Cell::new(0), + kb_state_id: Cell::new(xkb_state_id), + device: Default::default(), + pointer: Default::default(), + pointer_absolute: Default::default(), + keyboard: Default::default(), + button: Default::default(), + scroll: Default::default(), + touchscreen: Default::default(), + }); + track!(self.client, seat); + self.client.add_server_obj(&seat); + self.send_seat(&seat); + let v = &self.client.versions; + let caps = [ + (EI_CAP_POINTER, EiPointer, &v.ei_pointer), + ( + EI_CAP_POINTER_ABSOLUTE, + EiPointerAbsolute, + &v.ei_pointer_absolute, + ), + (EI_CAP_SCROLL, EiScroll, &v.ei_scroll), + (EI_CAP_BUTTON, EiButton, &v.ei_button), + (EI_CAP_KEYBOARD, EiKeyboard, &v.ei_keyboard), + (EI_CAP_TOUCHSCREEN, EiTouchscreen, &v.ei_touchscreen), + ]; + for (mask, interface, version) in caps { + if version.version.get() > EiVersion(0) { + seat.send_capability(interface, mask); + } + } + seat.send_name(&seat.seat.seat_name()); + seat.send_done(); + seat.seat.add_ei_seat(&seat); + } +} + +impl EiConnectionRequestHandler for EiConnection { + type Error = EiConnectionError; + + fn sync( + &self, + req: crate::wire_ei::ei_connection::Sync, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let version = EiVersion(req.version); + if version > self.client.versions.ei_callback.version.get() { + return Err(EiConnectionError::CallbackVersion(req.version)); + } + let cb = Rc::new(EiCallback { + id: req.callback, + client: self.client.clone(), + tracker: Default::default(), + version, + }); + track!(self.client, cb); + self.client.add_client_obj(&cb)?; + cb.send_done(0); + self.client.remove_obj(&*cb)?; + Ok(()) + } + + fn disconnect(&self, _req: Disconnect, _slf: &Rc) -> Result<(), Self::Error> { + self.client.disconnect_announced.set(true); + self.client.state.ei_clients.shutdown(self.client.id); + Ok(()) + } +} + +ei_object_base! { + self = EiConnection; + version = self.version; +} + +impl EiObject for EiConnection {} + +#[derive(Debug, Error)] +pub enum EiConnectionError { + #[error(transparent)] + EiClientError(Box), + #[error("The callback version is too large: {0}")] + CallbackVersion(u32), +} +efrom!(EiConnectionError, EiClientError); diff --git a/src/ei/ei_ifs/ei_device.rs b/src/ei/ei_ifs/ei_device.rs new file mode 100644 index 00000000..77553e01 --- /dev/null +++ b/src/ei/ei_ifs/ei_device.rs @@ -0,0 +1,249 @@ +use { + crate::{ + backend::{KeyState, ScrollAxis}, + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::{ei_seat::EiSeat, ei_touchscreen::TouchChange}, + ei_object::{EiObject, EiVersion}, + }, + fixed::Fixed, + ifs::wl_seat::PX_PER_SCROLL, + leaks::Tracker, + rect::Rect, + scale::Scale, + utils::syncqueue::SyncQueue, + wire_ei::{ + ei_device::{ + ClientFrame, ClientStartEmulating, ClientStopEmulating, Destroyed, DeviceType, + Done, EiDeviceRequestHandler, Interface, Paused, Region, Release, Resumed, + ServerFrame, ServerStartEmulating, + }, + EiDeviceId, + }, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub const EI_DEVICE_TYPE_VIRTUAL: u32 = 1; +#[allow(dead_code)] +pub const EI_DEVICE_TYPE_PHYSICAL: u32 = 2; + +pub struct EiDevice { + pub id: EiDeviceId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub seat: Rc, + + pub button_changes: SyncQueue<(u32, KeyState)>, + pub touch_changes: SyncQueue<(u32, TouchChange)>, + pub scroll_px: [Cell>; 2], + pub scroll_v120: [Cell>; 2], + pub scroll_stop: [Cell>; 2], + pub absolute_motion: Cell>, + pub relative_motion: Cell>, + pub key_changes: SyncQueue<(u32, KeyState)>, +} + +pub trait EiDeviceInterface: EiObject { + fn new(device: &Rc, version: EiVersion) -> Rc; + + fn destroy(&self) -> Result<(), EiClientError>; + + fn send_destroyed(&self, serial: u32); +} + +impl EiDevice { + pub fn send_interface(&self, interface: &T) + where + T: EiDeviceInterface, + { + self.client.event(Interface { + self_id: self.id, + object: interface.id(), + interface_name: interface.interface().name(), + version: interface.version().0, + }); + } + + pub fn send_device_type(&self, ty: u32) { + self.client.event(DeviceType { + self_id: self.id, + device_type: ty, + }); + } + + pub fn send_resumed(&self, serial: u32) { + self.client.event(Resumed { + self_id: self.id, + serial, + }); + } + + pub fn send_start_emulating(&self, serial: u32, sequence: u32) { + self.client.event(ServerStartEmulating { + self_id: self.id, + serial, + sequence, + }); + } + + pub fn send_region(&self, rect: Rect, scale: Scale) { + self.client.event(Region { + self_id: self.id, + offset_x: rect.x1() as u32, + offset_y: rect.y1() as u32, + width: rect.width() as u32, + hight: rect.height() as u32, + scale: scale.to_f64() as f32, + }); + } + + #[allow(dead_code)] + pub fn send_paused(&self, serial: u32) { + self.client.event(Paused { + self_id: self.id, + serial, + }); + } + + pub fn send_done(&self) { + self.client.event(Done { self_id: self.id }); + } + + pub fn send_frame(&self, serial: u32, timestamp: u64) { + self.client.event(ServerFrame { + self_id: self.id, + serial, + timestamp, + }); + } + + pub fn send_destroyed(&self, serial: u32) { + self.client.event(Destroyed { + self_id: self.id, + serial, + }); + } + + pub fn destroy(&self) -> Result<(), EiClientError> { + macro_rules! destroy_interface { + ($name:ident) => { + if let Some(interface) = self.seat.$name.take() { + interface.destroy()?; + } + }; + } + destroy_interface!(pointer); + destroy_interface!(pointer_absolute); + destroy_interface!(scroll); + destroy_interface!(button); + destroy_interface!(keyboard); + destroy_interface!(touchscreen); + self.send_destroyed(self.client.serial()); + self.client.remove_obj(self)?; + Ok(()) + } +} + +impl EiDeviceRequestHandler for EiDevice { + type Error = EiDeviceError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroy()?; + Ok(()) + } + + fn client_start_emulating( + &self, + _req: ClientStartEmulating, + _slf: &Rc, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn client_stop_emulating( + &self, + _req: ClientStopEmulating, + _slf: &Rc, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn client_frame(&self, req: ClientFrame, _slf: &Rc) -> Result<(), Self::Error> { + let seat = &self.seat.seat; + let time = req.timestamp; + while let Some((button, pressed)) = self.button_changes.pop() { + seat.button_event(time, button, pressed); + } + while let Some((button, pressed)) = self.key_changes.pop() { + seat.key_event_with_seat_state(time, button, pressed); + } + if let Some((x, y)) = self.relative_motion.take() { + let x = Fixed::from_f32(x); + let y = Fixed::from_f32(y); + seat.motion_event(time, x, y, x, y); + } + if let Some((x, y)) = self.absolute_motion.take() { + let x = Fixed::from_f32(x); + let y = Fixed::from_f32(y); + seat.motion_event_abs(time, x, y); + } + { + let mut need_frame = false; + for axis in [ScrollAxis::Horizontal, ScrollAxis::Vertical] { + let idx = axis as usize; + if let Some(v120) = self.scroll_v120[idx].take() { + need_frame = true; + seat.axis_120(v120, axis, false); + } + if let Some(px) = self.scroll_px[idx].take() { + need_frame = true; + seat.axis_px(Fixed::from_f32(px), axis, false); + } + if self.scroll_stop[idx].take() == Some(true) { + need_frame = true; + seat.axis_stop(axis); + } + } + if need_frame { + seat.axis_frame(PX_PER_SCROLL, time); + } + } + if self.touch_changes.is_not_empty() { + while let Some((touch_id, change)) = self.touch_changes.pop() { + let id = touch_id as i32; + match change { + TouchChange::Down(x, y) => { + let x = Fixed::from_f32(x); + let y = Fixed::from_f32(y); + seat.touch_down_at(time, id, x, y); + } + TouchChange::Motion(x, y) => { + let x = Fixed::from_f32(x); + let y = Fixed::from_f32(y); + seat.touch_motion_at(time, id, x, y); + } + TouchChange::Up => seat.touch_up(time, id), + } + } + seat.touch_frame(time); + } + Ok(()) + } +} + +ei_object_base! { + self = EiDevice; + version = self.version; +} + +impl EiObject for EiDevice {} + +#[derive(Debug, Error)] +pub enum EiDeviceError { + #[error(transparent)] + EiClientError(Box), +} +efrom!(EiDeviceError, EiClientError); diff --git a/src/ei/ei_ifs/ei_handshake.rs b/src/ei/ei_ifs/ei_handshake.rs new file mode 100644 index 00000000..759db643 --- /dev/null +++ b/src/ei/ei_ifs/ei_handshake.rs @@ -0,0 +1,189 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::ei_connection::EiConnection, + ei_object::{EiInterface, EiObject, EiVersion, EI_HANDSHAKE_ID}, + EiContext, + }, + leaks::Tracker, + wire_ei::{ + ei_handshake::{ + ClientHandshakeVersion, ClientInterfaceVersion, Connection, ContextType, + EiHandshakeRequestHandler, Finish, Name, ServerHandshakeVersion, + ServerInterfaceVersion, + }, + EiHandshake, EiHandshakeId, + }, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct EiHandshake { + pub id: EiHandshakeId, + client: Rc, + version: Cell, + pub tracker: Tracker, + have_context_type: Cell, +} + +impl EiHandshake { + pub fn new(client: &Rc) -> Self { + Self { + id: EI_HANDSHAKE_ID, + client: client.clone(), + version: Cell::new(EiVersion(1)), + tracker: Default::default(), + have_context_type: Cell::new(false), + } + } + + pub fn send_handshake_version(&self) { + self.client.event(ServerHandshakeVersion { + self_id: self.id, + version: 1, + }); + } + + fn send_interface_version(&self, interface: EiInterface, version: EiVersion) { + self.client.event(ServerInterfaceVersion { + self_id: self.id, + name: interface.0, + version: version.0, + }); + } + + fn send_connection(&self, serial: u32, connection: &EiConnection) { + self.client.event(Connection { + self_id: self.id, + serial, + connection: connection.id, + version: connection.version.0, + }); + } +} + +impl EiHandshakeRequestHandler for EiHandshake { + type Error = EiHandshakeError; + + fn client_handshake_version( + &self, + req: ClientHandshakeVersion, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let version = EiVersion(req.version); + if version > self.client.versions.ei_handshake.server_max_version { + return Err(EiHandshakeError::UnknownHandshakeVersion); + } + self.client + .versions + .ei_handshake + .set_client_version(version); + Ok(()) + } + + fn finish(&self, _req: Finish, _slf: &Rc) -> Result<(), Self::Error> { + if !self.have_context_type.get() { + return Err(EiHandshakeError::NoContextType); + } + if self.client.name.borrow().is_none() { + return Err(EiHandshakeError::NoName); + } + if self.client.versions.ei_connection.version.get() == EiVersion(0) { + return Err(EiHandshakeError::NoConnectionVersion); + } + if self.client.versions.ei_callback.version.get() == EiVersion(0) { + return Err(EiHandshakeError::NoCallbackVersion); + } + self.client.versions.for_each(|interface, version| { + let version = version.version.get(); + if version > EiVersion(0) && interface != EiHandshake { + self.send_interface_version(interface, version); + } + }); + let connection = Rc::new(EiConnection { + id: self.client.new_id(), + client: self.client.clone(), + tracker: Default::default(), + version: self.client.versions.ei_connection.version.get(), + }); + self.client.add_server_obj(&connection); + track!(self.client, connection); + self.client.connection.set(Some(connection.clone())); + self.send_connection(self.client.serial(), &connection); + self.client.remove_obj(self)?; + for seat in self.client.state.globals.seats.lock().values() { + connection.announce_seat(seat); + } + Ok(()) + } + + fn context_type(&self, req: ContextType, _slf: &Rc) -> Result<(), Self::Error> { + if self.have_context_type.replace(true) { + return Err(EiHandshakeError::ContextTypeSet); + } + let ty = match req.context_type { + 1 => EiContext::Receiver, + 2 => EiContext::Sender, + _ => return Err(EiHandshakeError::UnknownContextType(req.context_type)), + }; + self.client.context.set(ty); + self.have_context_type.set(true); + Ok(()) + } + + fn name(&self, req: Name<'_>, _slf: &Rc) -> Result<(), Self::Error> { + let name = &mut *self.client.name.borrow_mut(); + if name.is_some() { + return Err(EiHandshakeError::NameSet); + } + *name = Some(req.name.to_string()); + Ok(()) + } + + fn client_interface_version( + &self, + req: ClientInterfaceVersion<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.client.versions.match_(req.name, |v| { + v.set_client_version(EiVersion(req.version)); + }); + Ok(()) + } +} + +ei_object_base! { + self = EiHandshake; + version = self.version.get(); +} + +impl EiObject for EiHandshake { + fn context(&self) -> EiContext { + panic!("context requested for EiHandshake") + } +} + +#[derive(Debug, Error)] +pub enum EiHandshakeError { + #[error(transparent)] + EiClientError(Box), + #[error("ei_handshake version is too large")] + UnknownHandshakeVersion, + #[error("Name is already set")] + NameSet, + #[error("Unknown context type {0}")] + UnknownContextType(u32), + #[error("Context type is already set")] + ContextTypeSet, + #[error("Client did not set connection version")] + NoConnectionVersion, + #[error("Client did not set callback version")] + NoCallbackVersion, + #[error("Client did not set context type")] + NoContextType, + #[error("Client did not set name")] + NoName, +} +efrom!(EiHandshakeError, EiClientError); diff --git a/src/ei/ei_ifs/ei_keyboard.rs b/src/ei/ei_ifs/ei_keyboard.rs new file mode 100644 index 00000000..93831261 --- /dev/null +++ b/src/ei/ei_ifs/ei_keyboard.rs @@ -0,0 +1,97 @@ +use { + crate::{ + backend::KeyState, + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::ei_device::{EiDevice, EiDeviceInterface}, + ei_object::{EiObject, EiVersion}, + }, + leaks::Tracker, + wire_ei::{ + ei_keyboard::{ + ClientKey, EiKeyboardRequestHandler, Keymap, Modifiers, Release, ServerKey, + }, + EiKeyboardId, + }, + xkbcommon::KeyboardState, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct EiKeyboard { + pub id: EiKeyboardId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub device: Rc, +} + +ei_device_interface!(EiKeyboard, ei_keyboard, keyboard); + +const KEYMAP_TYPE_XKB: u32 = 1; + +impl EiKeyboard { + pub fn send_keymap(&self, state: &KeyboardState) { + self.client.event(Keymap { + self_id: self.id, + keymap_type: KEYMAP_TYPE_XKB, + size: state.map_len as _, + keymap: state.map.clone(), + }); + } + + pub fn send_modifiers(&self, state: &KeyboardState) { + self.client.event(Modifiers { + self_id: self.id, + serial: self.client.serial(), + depressed: state.mods.mods_depressed, + locked: state.mods.mods_locked, + latched: state.mods.mods_latched, + group: state.mods.group, + }); + } + + pub fn send_key(&self, key: u32, state: u32) { + self.client.event(ServerKey { + self_id: self.id, + key, + state, + }); + } +} + +impl EiKeyboardRequestHandler for EiKeyboard { + type Error = EiKeyboardError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroy()?; + Ok(()) + } + + fn client_key(&self, req: ClientKey, _slf: &Rc) -> Result<(), Self::Error> { + let pressed = match req.state { + 0 => KeyState::Released, + 1 => KeyState::Pressed, + _ => return Err(EiKeyboardError::InvalidKeyState(req.state)), + }; + self.device.key_changes.push((req.key, pressed)); + Ok(()) + } +} + +ei_object_base! { + self = EiKeyboard; + version = self.version; +} + +impl EiObject for EiKeyboard {} + +#[derive(Debug, Error)] +pub enum EiKeyboardError { + #[error(transparent)] + EiClientError(Box), + #[error("Invalid key state {0}")] + InvalidKeyState(u32), +} +efrom!(EiKeyboardError, EiClientError); diff --git a/src/ei/ei_ifs/ei_pingpong.rs b/src/ei/ei_ifs/ei_pingpong.rs new file mode 100644 index 00000000..53220676 --- /dev/null +++ b/src/ei/ei_ifs/ei_pingpong.rs @@ -0,0 +1,45 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_object::{EiObject, EiVersion}, + }, + leaks::Tracker, + wire_ei::{ + ei_pingpong::{Done, EiPingpongRequestHandler}, + EiPingpongId, + }, + }, + std::rc::Rc, + thiserror::Error, +}; + +#[allow(dead_code)] +pub struct EiPingpong { + pub id: EiPingpongId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, +} + +impl EiPingpongRequestHandler for EiPingpong { + type Error = EiPingpongError; + + fn done(&self, _req: Done, _slf: &Rc) -> Result<(), Self::Error> { + Ok(()) + } +} + +ei_object_base! { + self = EiPingpong; + version = self.version; +} + +impl EiObject for EiPingpong {} + +#[derive(Debug, Error)] +pub enum EiPingpongError { + #[error(transparent)] + EiClientError(Box), +} +efrom!(EiPingpongError, EiClientError); diff --git a/src/ei/ei_ifs/ei_pointer.rs b/src/ei/ei_ifs/ei_pointer.rs new file mode 100644 index 00000000..85038da7 --- /dev/null +++ b/src/ei/ei_ifs/ei_pointer.rs @@ -0,0 +1,71 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::ei_device::{EiDevice, EiDeviceInterface}, + ei_object::{EiObject, EiVersion}, + }, + fixed::Fixed, + leaks::Tracker, + wire_ei::{ + ei_pointer::{ + ClientMotionRelative, EiPointerRequestHandler, Release, ServerMotionRelative, + }, + EiPointerId, + }, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct EiPointer { + pub id: EiPointerId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub device: Rc, +} + +ei_device_interface!(EiPointer, ei_pointer, pointer); + +impl EiPointer { + pub fn send_motion(&self, dx: Fixed, dy: Fixed) { + self.client.event(ServerMotionRelative { + self_id: self.id, + x: dx.to_f32(), + y: dy.to_f32(), + }); + } +} + +impl EiPointerRequestHandler for EiPointer { + type Error = EiPointerError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroy()?; + Ok(()) + } + + fn client_motion_relative( + &self, + req: ClientMotionRelative, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.device.relative_motion.set(Some((req.x, req.y))); + Ok(()) + } +} + +ei_object_base! { + self = EiPointer; + version = self.version; +} + +impl EiObject for EiPointer {} + +#[derive(Debug, Error)] +pub enum EiPointerError { + #[error(transparent)] + EiClientError(Box), +} +efrom!(EiPointerError, EiClientError); diff --git a/src/ei/ei_ifs/ei_pointer_absolute.rs b/src/ei/ei_ifs/ei_pointer_absolute.rs new file mode 100644 index 00000000..cd1cfe79 --- /dev/null +++ b/src/ei/ei_ifs/ei_pointer_absolute.rs @@ -0,0 +1,72 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::ei_device::{EiDevice, EiDeviceInterface}, + ei_object::{EiObject, EiVersion}, + }, + fixed::Fixed, + leaks::Tracker, + wire_ei::{ + ei_pointer_absolute::{ + ClientMotionAbsolute, EiPointerAbsoluteRequestHandler, Release, + ServerMotionAbsolute, + }, + EiPointerAbsoluteId, + }, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct EiPointerAbsolute { + pub id: EiPointerAbsoluteId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub device: Rc, +} + +ei_device_interface!(EiPointerAbsolute, ei_pointer_absolute, pointer_absolute); + +impl EiPointerAbsolute { + pub fn send_motion_absolute(&self, x: Fixed, y: Fixed) { + self.client.event(ServerMotionAbsolute { + self_id: self.id, + x: x.to_f32(), + y: y.to_f32(), + }); + } +} + +impl EiPointerAbsoluteRequestHandler for EiPointerAbsolute { + type Error = EiCallbackError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroy()?; + Ok(()) + } + + fn client_motion_absolute( + &self, + req: ClientMotionAbsolute, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.device.absolute_motion.set(Some((req.x, req.y))); + Ok(()) + } +} + +ei_object_base! { + self = EiPointerAbsolute; + version = self.version; +} + +impl EiObject for EiPointerAbsolute {} + +#[derive(Debug, Error)] +pub enum EiCallbackError { + #[error(transparent)] + EiClientError(Box), +} +efrom!(EiCallbackError, EiClientError); diff --git a/src/ei/ei_ifs/ei_scroll.rs b/src/ei/ei_ifs/ei_scroll.rs new file mode 100644 index 00000000..93ae2e61 --- /dev/null +++ b/src/ei/ei_ifs/ei_scroll.rs @@ -0,0 +1,106 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::ei_device::{EiDevice, EiDeviceInterface}, + ei_object::{EiObject, EiVersion}, + }, + fixed::Fixed, + leaks::Tracker, + wire_ei::{ + ei_scroll::{ + ClientScroll, ClientScrollDiscrete, ClientScrollStop, EiScrollRequestHandler, + Release, ServerScroll, ServerScrollDiscrete, ServerScrollStop, + }, + EiScrollId, + }, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct EiScroll { + pub id: EiScrollId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub device: Rc, +} + +ei_device_interface!(EiScroll, ei_scroll, scroll); + +impl EiScroll { + pub fn send_scroll(&self, x: Fixed, y: Fixed) { + self.client.event(ServerScroll { + self_id: self.id, + x: x.to_f32(), + y: y.to_f32(), + }); + } + + pub fn send_scroll_discrete(&self, x: i32, y: i32) { + self.client.event(ServerScrollDiscrete { + self_id: self.id, + x, + y, + }); + } + + pub fn send_scroll_stop(&self, x: bool, y: bool) { + self.client.event(ServerScrollStop { + self_id: self.id, + x: x as _, + y: y as _, + is_cancel: 0, + }); + } +} + +impl EiScrollRequestHandler for EiScroll { + type Error = EiScrollError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroy()?; + Ok(()) + } + + fn client_scroll(&self, req: ClientScroll, _slf: &Rc) -> Result<(), Self::Error> { + self.device.scroll_px[0].set(Some(req.x)); + self.device.scroll_px[1].set(Some(req.y)); + Ok(()) + } + + fn client_scroll_discrete( + &self, + req: ClientScrollDiscrete, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.device.scroll_v120[0].set(Some(req.x)); + self.device.scroll_v120[1].set(Some(req.y)); + Ok(()) + } + + fn client_scroll_stop( + &self, + req: ClientScrollStop, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.device.scroll_stop[0].set(Some(req.x != 0)); + self.device.scroll_stop[1].set(Some(req.y != 0)); + Ok(()) + } +} + +ei_object_base! { + self = EiScroll; + version = self.version; +} + +impl EiObject for EiScroll {} + +#[derive(Debug, Error)] +pub enum EiScrollError { + #[error(transparent)] + EiClientError(Box), +} +efrom!(EiScrollError, EiClientError); diff --git a/src/ei/ei_ifs/ei_seat.rs b/src/ei/ei_ifs/ei_seat.rs new file mode 100644 index 00000000..40430299 --- /dev/null +++ b/src/ei/ei_ifs/ei_seat.rs @@ -0,0 +1,425 @@ +use { + crate::{ + backend::KeyState, + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::{ + ei_button::EiButton, + ei_device::{EiDevice, EiDeviceInterface, EI_DEVICE_TYPE_VIRTUAL}, + ei_keyboard::EiKeyboard, + ei_pointer::EiPointer, + ei_pointer_absolute::EiPointerAbsolute, + ei_scroll::EiScroll, + ei_touchscreen::EiTouchscreen, + }, + ei_object::{EiInterface, EiObject, EiVersion}, + EiContext, + }, + fixed::Fixed, + ifs::wl_seat::{wl_pointer::PendingScroll, WlSeatGlobal}, + leaks::Tracker, + tree::Node, + utils::{array, bitflags::BitflagsExt, clonecell::CloneCell}, + wire_ei::{ + ei_seat::{ + Bind, Capability, Destroyed, Device, Done, EiSeatRequestHandler, Name, Release, + }, + EiSeatId, + }, + xkbcommon::{DynKeyboardState, KeyboardState, KeyboardStateId}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub const EI_CAP_POINTER: u64 = 1 << 0; +pub const EI_CAP_POINTER_ABSOLUTE: u64 = 1 << 1; +pub const EI_CAP_SCROLL: u64 = 1 << 2; +pub const EI_CAP_BUTTON: u64 = 1 << 3; +pub const EI_CAP_KEYBOARD: u64 = 1 << 4; +pub const EI_CAP_TOUCHSCREEN: u64 = 1 << 5; + +pub const EI_CAP_ALL: u64 = (1 << 6) - 1; + +pub struct EiSeat { + pub id: EiSeatId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub seat: Rc, + pub capabilities: Cell, + pub kb_state_id: Cell, + + pub device: CloneCell>>, + pub pointer: CloneCell>>, + pub pointer_absolute: CloneCell>>, + pub keyboard: CloneCell>>, + pub button: CloneCell>>, + pub scroll: CloneCell>>, + pub touchscreen: CloneCell>>, +} + +impl EiSeat { + fn is_sender(&self) -> bool { + self.context() == EiContext::Sender + } + + pub fn regions_changed(self: &Rc) { + if self.touchscreen.is_none() && self.pointer_absolute.is_none() { + return; + } + let kb_state = self.get_kb_state(); + let kb_state = kb_state.borrow(); + if let Err(e) = self.recreate_all(false, &kb_state) { + self.client.error(e); + } + } + + pub fn handle_xkb_state_change(self: &Rc, old_id: KeyboardStateId, new: &KeyboardState) { + if self.keyboard.is_none() { + return; + } + if self.kb_state_id.get() != old_id { + return; + } + self.kb_state_id.set(new.id); + if let Err(e) = self.recreate_all(false, new) { + self.client.error(e); + } + } + + pub fn handle_modifiers_changed(self: &Rc, state: &KeyboardState) { + let old_id = self.kb_state_id.get(); + if old_id != state.id { + if self.is_sender() { + return; + } + self.handle_xkb_state_change(old_id, state); + return; + } + if let Some(kb) = self.keyboard.get() { + kb.send_modifiers(state); + } + } + + pub fn handle_key( + self: &Rc, + time_usec: u64, + key: u32, + state: u32, + kb_state: &KeyboardState, + ) { + if self.is_sender() { + return; + } + let old_id = self.kb_state_id.get(); + if old_id != kb_state.id { + self.handle_xkb_state_change(old_id, kb_state); + } + if let Some(kb) = self.keyboard.get() { + kb.send_key(key, state); + kb.device.send_frame(self.client.serial(), time_usec); + } + } + + pub fn handle_motion_abs(&self, time_usec: u64, x: Fixed, y: Fixed) { + if self.is_sender() { + return; + } + if let Some(v) = self.pointer_absolute.get() { + v.send_motion_absolute(x, y); + v.device.send_frame(self.client.serial(), time_usec); + } + } + + pub fn handle_motion(&self, time_usec: u64, dx: Fixed, dy: Fixed) { + if self.is_sender() { + return; + } + if let Some(v) = self.pointer.get() { + v.send_motion(dx, dy); + v.device.send_frame(self.client.serial(), time_usec); + } + } + + pub fn handle_button(&self, time_usec: u64, button: u32, state: KeyState) { + if self.is_sender() { + return; + } + if let Some(b) = self.button.get() { + b.send_button(button, state); + b.device.send_frame(self.client.serial(), time_usec); + } + } + + pub fn handle_pending_scroll(&self, time_usec: u64, ps: &PendingScroll) { + if self.is_sender() { + return; + } + if let Some(b) = self.scroll.get() { + b.send_scroll( + ps.px[0].get().unwrap_or_default(), + ps.px[1].get().unwrap_or_default(), + ); + b.send_scroll_discrete( + ps.v120[0].get().unwrap_or_default(), + ps.v120[1].get().unwrap_or_default(), + ); + b.send_scroll_stop(ps.stop[0].get(), ps.stop[1].get()); + b.device.send_frame(self.client.serial(), time_usec); + } + } + + pub fn handle_touch_down(&self, id: u32, x: Fixed, y: Fixed) { + if self.is_sender() { + return; + } + if let Some(b) = self.touchscreen.get() { + b.send_down(id, x, y); + } + } + + pub fn handle_touch_motion(&self, id: u32, x: Fixed, y: Fixed) { + if self.is_sender() { + return; + } + if let Some(b) = self.touchscreen.get() { + b.send_motion(id, x, y); + } + } + + pub fn handle_touch_up(&self, id: u32) { + if self.is_sender() { + return; + } + if let Some(b) = self.touchscreen.get() { + b.send_up(id); + } + } + + pub fn handle_touch_frame(&self, time_usec: u64) { + if self.is_sender() { + return; + } + if let Some(b) = self.touchscreen.get() { + b.device.send_frame(self.client.serial(), time_usec); + } + } + + pub fn send_capability(&self, interface: EiInterface, mask: u64) { + self.client.event(Capability { + self_id: self.id, + mask, + interface: interface.0, + }); + } + + pub fn send_done(&self) { + self.client.event(Done { self_id: self.id }); + } + + pub fn send_name(&self, name: &str) { + self.client.event(Name { + self_id: self.id, + name, + }); + } + + pub fn send_device(&self, device: &EiDevice) { + self.client.event(Device { + self_id: self.id, + device: device.id, + version: device.version.0, + }); + } + + pub fn send_destroyed(&self) { + self.client.event(Destroyed { + self_id: self.id, + serial: self.client.serial(), + }); + } + + fn create_interface(self: &Rc, field: &CloneCell>>, version: EiVersion) + where + T: EiDeviceInterface, + { + if version == EiVersion(0) { + return; + } + let Some(device) = self.device.get() else { + return; + }; + let interface = T::new(&device, version); + self.client.add_server_obj(&interface); + device.send_interface(&*interface); + field.set(Some(interface.clone())); + } + + fn create_pointer(self: &Rc) { + self.create_interface(&self.pointer, self.client.versions.ei_pointer()); + } + + fn create_button(self: &Rc) { + self.create_interface(&self.button, self.client.versions.ei_button()); + } + + fn create_keyboard(self: &Rc) { + self.create_interface(&self.keyboard, self.client.versions.ei_keyboard()); + } + + fn create_scroll(self: &Rc) { + self.create_interface(&self.scroll, self.client.versions.ei_scroll()); + } + + fn create_pointer_absolute(self: &Rc) { + self.create_interface( + &self.pointer_absolute, + self.client.versions.ei_pointer_absolute(), + ); + } + + fn create_touchscreen(self: &Rc) { + self.create_interface(&self.touchscreen, self.client.versions.ei_touchscreen()); + } + + fn get_kb_state(&self) -> Rc { + match self.context() { + EiContext::Sender => self.seat.seat_xkb_state(), + EiContext::Receiver => self.seat.latest_xkb_state(), + } + } + + fn recreate_all( + self: &Rc, + create_all: bool, + kb_state: &KeyboardState, + ) -> Result<(), EiClientError> { + self.kb_state_id.set(kb_state.id); + let have_outputs = self.client.state.root.outputs.is_not_empty(); + let create_pointer = create_all || self.pointer.is_some(); + let create_pointer_absolute = + have_outputs && (create_all || self.pointer_absolute.is_some()); + let create_scroll = create_all || self.scroll.is_some(); + let create_button = create_all || self.button.is_some(); + let create_keyboard = create_all || self.keyboard.is_some(); + let create_touchscreen = have_outputs && (create_all || self.touchscreen.is_some()); + if let Some(device) = self.device.take() { + device.destroy()?; + } + let version = self.client.versions.ei_device(); + if version == EiVersion(0) { + return Ok(()); + } + let device = Rc::new(EiDevice { + id: self.client.new_id(), + client: self.client.clone(), + tracker: Default::default(), + version, + seat: self.clone(), + button_changes: Default::default(), + touch_changes: Default::default(), + scroll_px: array::from_fn(|_| Default::default()), + scroll_v120: array::from_fn(|_| Default::default()), + scroll_stop: array::from_fn(|_| Default::default()), + absolute_motion: Default::default(), + relative_motion: Default::default(), + key_changes: Default::default(), + }); + track!(self.client, device); + self.device.set(Some(device.clone())); + self.client.add_server_obj(&device); + self.send_device(&device); + device.send_device_type(EI_DEVICE_TYPE_VIRTUAL); + let caps = self.capabilities.get(); + macro_rules! apply { + ($cap:expr, $create:ident) => { + if $create && caps.contains($cap) { + self.$create(); + } + }; + } + apply!(EI_CAP_POINTER, create_pointer); + apply!(EI_CAP_POINTER_ABSOLUTE, create_pointer_absolute); + apply!(EI_CAP_SCROLL, create_scroll); + apply!(EI_CAP_BUTTON, create_button); + apply!(EI_CAP_KEYBOARD, create_keyboard); + apply!(EI_CAP_TOUCHSCREEN, create_touchscreen); + for output in self.client.state.root.outputs.lock().values() { + device.send_region( + output.node_absolute_position(), + output.global.persistent.scale.get(), + ); + } + if let Some(kb) = self.keyboard.get() { + kb.send_keymap(kb_state); + } + device.send_done(); + device.send_resumed(self.client.serial()); + if self.context() == EiContext::Receiver { + device.send_start_emulating(self.client.serial(), 1); + } + if let Some(kb) = self.keyboard.get() { + kb.send_modifiers(kb_state); + } + Ok(()) + } + + pub fn is_touch_input(&self) -> bool { + self.capabilities.get().contains(EI_CAP_TOUCHSCREEN) && self.context() == EiContext::Sender + } +} + +impl EiSeatRequestHandler for EiSeat { + type Error = EiSeatError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.seat.remove_ei_seat(self); + self.send_destroyed(); + if let Some(device) = self.device.take() { + device.destroy()?; + } + self.client.remove_obj(self)?; + Ok(()) + } + + fn bind(&self, req: Bind, slf: &Rc) -> Result<(), Self::Error> { + let caps = req.capabilities; + let unknown = caps & !EI_CAP_ALL; + if unknown != 0 { + return Err(EiSeatError::UnknownCapabilities(unknown)); + } + self.capabilities.set(caps); + let kb_state = self.get_kb_state(); + slf.recreate_all(true, &kb_state.borrow())?; + self.seat.update_capabilities(); + Ok(()) + } +} + +ei_object_base! { + self = EiSeat; + version = self.version; +} + +impl EiObject for EiSeat { + fn break_loops(&self) { + self.seat.remove_ei_seat(self); + self.device.take(); + self.pointer.take(); + self.pointer_absolute.take(); + self.keyboard.take(); + self.button.take(); + self.scroll.take(); + self.touchscreen.take(); + } +} + +#[derive(Debug, Error)] +pub enum EiSeatError { + #[error(transparent)] + EiClientError(Box), + #[error("Capabilities {0} are unknown")] + UnknownCapabilities(u64), +} +efrom!(EiSeatError, EiClientError); diff --git a/src/ei/ei_ifs/ei_touchscreen.rs b/src/ei/ei_ifs/ei_touchscreen.rs new file mode 100644 index 00000000..52c0d60d --- /dev/null +++ b/src/ei/ei_ifs/ei_touchscreen.rs @@ -0,0 +1,108 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + ei_ifs::ei_device::{EiDevice, EiDeviceInterface}, + ei_object::{EiObject, EiVersion}, + }, + fixed::Fixed, + leaks::Tracker, + wire_ei::{ + ei_touchscreen::{ + ClientDown, ClientMotion, ClientUp, EiTouchscreenRequestHandler, Release, + ServerDown, ServerMotion, ServerUp, + }, + EiTouchscreenId, + }, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct EiTouchscreen { + pub id: EiTouchscreenId, + pub client: Rc, + pub tracker: Tracker, + pub version: EiVersion, + pub device: Rc, +} + +#[derive(Copy, Clone, Debug)] +pub enum TouchChange { + Down(f32, f32), + Motion(f32, f32), + Up, +} + +ei_device_interface!(EiTouchscreen, ei_touchscreen, touchscreen); + +impl EiTouchscreen { + pub fn send_down(&self, touchid: u32, x: Fixed, y: Fixed) { + self.client.event(ServerDown { + self_id: self.id, + touchid, + x: x.to_f32(), + y: y.to_f32(), + }); + } + + pub fn send_motion(&self, touchid: u32, x: Fixed, y: Fixed) { + self.client.event(ServerMotion { + self_id: self.id, + touchid, + x: x.to_f32(), + y: y.to_f32(), + }); + } + + pub fn send_up(&self, touchid: u32) { + self.client.event(ServerUp { + self_id: self.id, + touchid, + }); + } +} + +impl EiTouchscreenRequestHandler for EiTouchscreen { + type Error = EiTouchscreenError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroy()?; + Ok(()) + } + + fn client_down(&self, req: ClientDown, _slf: &Rc) -> Result<(), Self::Error> { + self.device + .touch_changes + .push((req.touchid, TouchChange::Down(req.x, req.y))); + Ok(()) + } + + fn client_motion(&self, req: ClientMotion, _slf: &Rc) -> Result<(), Self::Error> { + self.device + .touch_changes + .push((req.touchid, TouchChange::Motion(req.x, req.y))); + Ok(()) + } + + fn client_up(&self, req: ClientUp, _slf: &Rc) -> Result<(), Self::Error> { + self.device + .touch_changes + .push((req.touchid, TouchChange::Up)); + Ok(()) + } +} + +ei_object_base! { + self = EiTouchscreen; + version = self.version; +} + +impl EiObject for EiTouchscreen {} + +#[derive(Debug, Error)] +pub enum EiTouchscreenError { + #[error(transparent)] + EiClientError(Box), +} +efrom!(EiTouchscreenError, EiClientError); diff --git a/src/ei/ei_object.rs b/src/ei/ei_object.rs new file mode 100644 index 00000000..9aac90bd --- /dev/null +++ b/src/ei/ei_object.rs @@ -0,0 +1,94 @@ +use { + crate::{ + ei::{ + ei_client::{EiClient, EiClientError}, + EiContext, + }, + utils::buffd::EiMsgParser, + wire_ei::EiHandshakeId, + }, + std::{ + cmp::Ordering, + fmt::{Display, Formatter, LowerHex}, + rc::Rc, + }, +}; + +pub const EI_HANDSHAKE_ID: EiHandshakeId = EiHandshakeId::from_raw(0); + +#[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct EiObjectId(u64); + +impl EiObjectId { + #[allow(dead_code)] + pub const NONE: Self = EiObjectId(0); + + pub fn from_raw(raw: u64) -> Self { + Self(raw) + } + + pub fn raw(self) -> u64 { + self.0 + } +} + +impl Display for EiObjectId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl LowerHex for EiObjectId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + LowerHex::fmt(&self.0, f) + } +} + +pub trait EiObjectBase { + fn id(&self) -> EiObjectId; + fn version(&self) -> EiVersion; + fn client(&self) -> &EiClient; + fn handle_request( + self: Rc, + client: &EiClient, + request: u32, + parser: EiMsgParser<'_, '_>, + ) -> Result<(), EiClientError>; + fn interface(&self) -> EiInterface; +} + +pub trait EiObject: EiObjectBase + 'static { + fn break_loops(&self) {} + + fn context(&self) -> EiContext { + self.client().context.get() + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct EiInterface(pub &'static str); + +impl EiInterface { + pub fn name(self) -> &'static str { + self.0 + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct EiVersion(pub u32); + +impl EiVersion { + // pub const ALL: EiVersion = EiVersion(0); +} + +impl PartialEq for EiVersion { + fn eq(&self, other: &u32) -> bool { + self.0 == *other + } +} + +impl PartialOrd for EiVersion { + fn partial_cmp(&self, other: &u32) -> Option { + self.0.partial_cmp(other) + } +} diff --git a/src/fixed.rs b/src/fixed.rs index 3bd02e9a..c758f637 100644 --- a/src/fixed.rs +++ b/src/fixed.rs @@ -19,10 +19,18 @@ impl Fixed { Self((f * 256.0) as i32) } + pub fn from_f32(f: f32) -> Self { + Self::from_f64(f as f64) + } + pub fn to_f64(self) -> f64 { self.0 as f64 / 256.0 } + pub fn to_f32(self) -> f32 { + self.0 as f32 / 256.0 + } + pub fn from_1616(i: i32) -> Self { Self(i >> 8) } diff --git a/src/forker.rs b/src/forker.rs index 0067198d..3d6a3508 100644 --- a/src/forker.rs +++ b/src/forker.rs @@ -3,7 +3,7 @@ mod io; use { crate::{ async_engine::{AsyncEngine, SpawnedFuture}, - compositor::{DISPLAY, WAYLAND_DISPLAY}, + compositor::{DISPLAY, LIBEI_SOCKET, WAYLAND_DISPLAY}, forker::io::{IoIn, IoOut}, io_uring::IoUring, state::State, @@ -151,14 +151,18 @@ impl ForkerProxy { pub async fn xwayland( &self, + state: &State, stderr: Rc, dfd: Rc, listenfd: Rc, wmfd: Rc, waylandfd: Rc, ) -> Result<(Rc, c::pid_t), ForkerError> { - let (prog, args) = xwayland::build_args(); - let env = vec![("WAYLAND_SOCKET".to_string(), "6".to_string())]; + let (prog, args) = xwayland::build_args(state, self).await; + let env = vec![ + ("WAYLAND_SOCKET".to_string(), Some("6".to_string())), + (LIBEI_SOCKET.to_string(), None), + ]; let fds = vec![ (2, stderr), (3, dfd), @@ -175,7 +179,7 @@ impl ForkerProxy { &self, prog: String, args: Vec, - env: Vec<(String, String)>, + env: Vec<(String, Option)>, fds: Vec<(i32, Rc)>, ) { self.spawn_(prog, args, env, fds, None) @@ -185,7 +189,7 @@ impl ForkerProxy { &self, prog: String, args: Vec, - env: Vec<(String, String)>, + env: Vec<(String, Option)>, fds: Vec<(i32, Rc)>, pidfd_id: Option, ) { @@ -291,7 +295,7 @@ enum ServerMessage { Spawn { prog: String, args: Vec, - env: Vec<(String, String)>, + env: Vec<(String, Option)>, fds: Vec, pidfd_id: Option, }, @@ -408,7 +412,7 @@ impl Forker { self: &Rc, prog: String, args: Vec, - env: Vec<(String, String)>, + env: Vec<(String, Option)>, fds: Vec, io: &mut IoIn, pidfd_id: Option, @@ -424,7 +428,7 @@ impl Forker { self: &Rc, prog: String, args: Vec, - env: Vec<(String, String)>, + env: Vec<(String, Option)>, fds: Vec<(i32, OwnedFd)>, pidfd_id: Option, ) { @@ -502,7 +506,10 @@ impl Forker { c::signal(c::SIGCHLD, c::SIG_DFL); } for (key, val) in env { - env::set_var(&key, &val); + match val { + None => env::remove_var(&key), + Some(val) => env::set_var(&key, &val), + } } let prog = prog.into_ustr(); let mut argsnt = UstrPtr::new(); diff --git a/src/ifs.rs b/src/ifs.rs index 851dd9fa..3d81f770 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -7,6 +7,8 @@ pub mod ext_session_lock_v1; pub mod ipc; pub mod jay_compositor; pub mod jay_damage_tracking; +pub mod jay_ei_session; +pub mod jay_ei_session_builder; pub mod jay_idle; pub mod jay_input; pub mod jay_log_file; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index e7faef22..dd778d0c 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -4,6 +4,7 @@ use { client::{Client, ClientCaps, ClientError, CAP_JAY_COMPOSITOR}, globals::{Global, GlobalName}, ifs::{ + jay_ei_session_builder::JayEiSessionBuilder, jay_idle::JayIdle, jay_input::JayInput, jay_log_file::JayLogFile, @@ -30,6 +31,8 @@ use { thiserror::Error, }; +pub const CREATE_EI_SESSION_SINCE: Version = Version(5); + pub struct JayCompositorGlobal { name: GlobalName, } @@ -66,7 +69,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 4 + 5 } fn required_caps(&self) -> ClientCaps { @@ -377,6 +380,19 @@ impl JayCompositorRequestHandler for JayCompositor { seat.global.select_workspace(selector); Ok(()) } + + fn create_ei_session(&self, req: CreateEiSession, _slf: &Rc) -> Result<(), Self::Error> { + let obj = Rc::new(JayEiSessionBuilder { + id: req.id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + app_id: Default::default(), + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_ei_session.rs b/src/ifs/jay_ei_session.rs new file mode 100644 index 00000000..8aa516ca --- /dev/null +++ b/src/ifs/jay_ei_session.rs @@ -0,0 +1,81 @@ +use { + crate::{ + client::{Client, ClientError, ClientId}, + leaks::Tracker, + object::{Object, Version}, + wire::{ + jay_ei_session::{Created, Destroyed, Failed, JayEiSessionRequestHandler, Release}, + JayEiSessionId, + }, + }, + std::rc::Rc, + thiserror::Error, + uapi::OwnedFd, +}; + +pub struct JayEiSession { + pub id: JayEiSessionId, + pub client: Rc, + pub ei_client_id: Option, + pub tracker: Tracker, + pub version: Version, +} + +impl JayEiSession { + pub fn send_created(&self, fd: &Rc) { + self.client.event(Created { + self_id: self.id, + fd: fd.clone(), + }); + } + + pub fn send_failed(&self, reason: &str) { + self.client.event(Failed { + self_id: self.id, + reason, + }); + } + + fn send_destroyed(&self) { + self.client.event(Destroyed { self_id: self.id }); + } + + fn kill(&self, send_destroyed: bool) { + if let Some(id) = self.ei_client_id { + self.client.state.ei_clients.shutdown(id); + } + if send_destroyed { + self.send_destroyed(); + } + } +} + +impl JayEiSessionRequestHandler for JayEiSession { + type Error = JayEiSessionError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.kill(false); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = JayEiSession; + version = self.version; +} + +impl Object for JayEiSession { + fn break_loops(&self) { + self.kill(false); + } +} + +simple_add_obj!(JayEiSession); + +#[derive(Debug, Error)] +pub enum JayEiSessionError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JayEiSessionError, ClientError); diff --git a/src/ifs/jay_ei_session_builder.rs b/src/ifs/jay_ei_session_builder.rs new file mode 100644 index 00000000..e42b36fa --- /dev/null +++ b/src/ifs/jay_ei_session_builder.rs @@ -0,0 +1,97 @@ +use { + crate::{ + client::{Client, ClientError}, + ei::ei_client::EiClientError, + ifs::jay_ei_session::JayEiSession, + leaks::Tracker, + object::{Object, Version}, + utils::{errorfmt::ErrorFmt, oserror::OsError}, + wire::{ + jay_ei_session_builder::{Commit, JayEiSessionBuilderRequestHandler, SetAppId}, + JayEiSessionBuilderId, + }, + }, + std::{cell::RefCell, rc::Rc}, + thiserror::Error, + uapi::c, +}; + +pub struct JayEiSessionBuilder { + pub id: JayEiSessionBuilderId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + pub app_id: RefCell>, +} + +impl JayEiSessionBuilderRequestHandler for JayEiSessionBuilder { + type Error = JayEiSessionBuilderError; + + fn commit(&self, req: Commit, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + let app_id = self.app_id.borrow().clone(); + if app_id.is_none() { + return Err(JayEiSessionBuilderError::NoAppId); + } + let res = (move || { + let con = uapi::socketpair(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0); + let (server, client) = match con { + Ok(w) => w, + Err(e) => return Err(JayEiSessionBuilderError::SocketPair(e.into())), + }; + let ei_client_id = self + .client + .state + .ei_clients + .spawn2(&self.client.state, Rc::new(server), None, app_id) + .map_err(JayEiSessionBuilderError::SpawnClient)? + .id; + Ok((ei_client_id, Rc::new(client))) + })(); + let obj = Rc::new(JayEiSession { + id: req.id, + client: self.client.clone(), + ei_client_id: res.as_ref().ok().map(|v| v.0), + tracker: Default::default(), + version: self.version, + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + match res { + Ok((_, fd)) => obj.send_created(&fd), + Err(e) => { + let e = format!("Could not spawn client: {}", ErrorFmt(e)); + log::error!("{}", e); + obj.send_failed(&e); + } + } + Ok(()) + } + + fn set_app_id(&self, req: SetAppId<'_>, _slf: &Rc) -> Result<(), Self::Error> { + *self.app_id.borrow_mut() = Some(req.app_id.to_string()); + Ok(()) + } +} + +object_base! { + self = JayEiSessionBuilder; + version = self.version; +} + +impl Object for JayEiSessionBuilder {} + +simple_add_obj!(JayEiSessionBuilder); + +#[derive(Debug, Error)] +pub enum JayEiSessionBuilderError { + #[error(transparent)] + ClientError(Box), + #[error("Could not create a socketpair")] + SocketPair(#[source] OsError), + #[error("Could not spawn a new client")] + SpawnClient(#[source] EiClientError), + #[error("Commit called without app-id")] + NoAppId, +} +efrom!(JayEiSessionBuilderError, ClientError); diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 339a7a63..48d2190d 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -25,6 +25,7 @@ use { async_engine::SpawnedFuture, client::{Client, ClientError, ClientId}, cursor_user::{CursorUser, CursorUserGroup, CursorUserOwner}, + ei::ei_ifs::ei_seat::EiSeat, fixed::Fixed, globals::{Global, GlobalName}, ifs::{ @@ -84,6 +85,7 @@ use { WlSeatId, WlTouchId, ZwlrDataControlDeviceV1Id, ZwpPrimarySelectionDeviceV1Id, ZwpRelativePointerV1Id, ZwpTextInputV3Id, }, + wire_ei::EiSeatId, xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState}, }, ahash::AHashMap, @@ -195,6 +197,7 @@ pub struct WlSeatGlobal { pinch_bindings: PerClientBindings, hold_bindings: PerClientBindings, tablet: TabletSeatData, + ei_seats: CopyHashMap<(ClientId, EiSeatId), Rc>, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -263,6 +266,7 @@ impl WlSeatGlobal { pinch_bindings: Default::default(), hold_bindings: Default::default(), tablet: Default::default(), + ei_seats: Default::default(), }); slf.pointer_cursor.set_owner(slf.clone()); let seat = slf.clone(); @@ -280,10 +284,14 @@ impl WlSeatGlobal { slf } - fn update_capabilities(&self) { + pub fn update_capabilities(&self) { let mut caps = POINTER | KEYBOARD; if self.num_touch_devices.get() > 0 { caps |= TOUCH; + } else { + if self.ei_seats.lock().values().any(|s| s.is_touch_input()) { + caps |= TOUCH; + } } if self.capabilities.replace(caps) != caps { for client in self.bindings.borrow().values() { @@ -481,6 +489,9 @@ impl WlSeatGlobal { } fn handle_xkb_state_change(&self, old: &XkbState, new: &XkbState) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_xkb_state_change(old.kb_state.id, &new.kb_state); + }); let Some(surface) = self.keyboard_node.get().node_into_surface() else { return; }; @@ -888,6 +899,7 @@ impl WlSeatGlobal { self.hold_bindings.clear(); self.cursor_user_group.detach(); self.tablet_clear(); + self.ei_seats.clear(); } pub fn id(&self) -> SeatId { @@ -982,6 +994,30 @@ impl WlSeatGlobal { self.pointer_owner .set_window_management_enabled(self, enabled); } + + pub fn add_ei_seat(&self, ei: &Rc) { + self.ei_seats.set((ei.client.id, ei.id), ei.clone()); + self.update_capabilities(); + } + + pub fn remove_ei_seat(&self, ei: &EiSeat) { + self.ei_seats.remove(&(ei.client.id, ei.id)); + self.update_capabilities(); + } + + pub fn seat_xkb_state(&self) -> Rc { + self.seat_xkb_state.get() + } + + pub fn latest_xkb_state(&self) -> Rc { + self.latest_kb_state.get() + } + + pub fn output_extents_changed(&self) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.regions_changed(); + }); + } } impl CursorUserOwner for WlSeatGlobal { diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index ee6c55c7..e47cb4f1 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -1,8 +1,11 @@ use { crate::{ - backend::{ConnectorId, InputDeviceId, InputEvent, KeyState, AXIS_120}, + backend::{ + AxisSource, ConnectorId, InputDeviceId, InputEvent, KeyState, ScrollAxis, AXIS_120, + }, client::ClientId, config::InvokedShortcut, + ei::ei_ifs::ei_seat::EiSeat, fixed::Fixed, ifs::{ ipc::{ @@ -245,10 +248,7 @@ impl WlSeatGlobal { | InputEvent::TabletPadModeSwitch { time_usec, .. } | InputEvent::TabletPadRing { time_usec, .. } | InputEvent::TabletPadStrip { time_usec, .. } - | InputEvent::TouchDown { time_usec, .. } - | InputEvent::TouchUp { time_usec, .. } - | InputEvent::TouchMotion { time_usec, .. } - | InputEvent::TouchCancel { time_usec, .. } => { + | InputEvent::TouchFrame { time_usec, .. } => { self.last_input_usec.set(time_usec); if self.idle_notifications.is_not_empty() { for notification in self.idle_notifications.lock().drain_values() { @@ -262,7 +262,10 @@ impl WlSeatGlobal { | InputEvent::Axis120 { .. } | InputEvent::TabletToolAdded { .. } | InputEvent::TabletToolRemoved { .. } - | InputEvent::TouchFrame => {} + | InputEvent::TouchDown { .. } + | InputEvent::TouchUp { .. } + | InputEvent::TouchMotion { .. } + | InputEvent::TouchCancel { .. } => {} } match event { InputEvent::ConnectorPosition { .. } @@ -297,7 +300,7 @@ impl WlSeatGlobal { InputEvent::TouchUp { .. } => {} InputEvent::TouchMotion { .. } => {} InputEvent::TouchCancel { .. } => {} - InputEvent::TouchFrame => {} + InputEvent::TouchFrame { .. } => {} } match event { InputEvent::Key { @@ -324,19 +327,21 @@ impl WlSeatGlobal { state, } => self.button_event(time_usec, button, state), - InputEvent::AxisSource { source } => self.pointer_owner.axis_source(source), + InputEvent::AxisSource { source } => self.axis_source(source), InputEvent::Axis120 { dist, axis, inverted, - } => self.pointer_owner.axis_120(dist, axis, inverted), + } => self.axis_120(dist, axis, inverted), InputEvent::AxisPx { dist, axis, inverted, - } => self.pointer_owner.axis_px(dist, axis, inverted), - InputEvent::AxisStop { axis } => self.pointer_owner.axis_stop(axis), - InputEvent::AxisFrame { time_usec } => self.pointer_owner.frame(dev, self, time_usec), + } => self.axis_px(dist, axis, inverted), + InputEvent::AxisStop { axis } => self.axis_stop(axis), + InputEvent::AxisFrame { time_usec } => { + self.axis_frame(dev.px_per_scroll_wheel.get(), time_usec) + } InputEvent::SwipeBegin { time_usec, finger_count, @@ -445,7 +450,7 @@ impl WlSeatGlobal { y_normed, } => self.touch_motion(time_usec, id, dev.get_rect(&self.state), x_normed, y_normed), InputEvent::TouchCancel { time_usec, id } => self.touch_cancel(time_usec, id), - InputEvent::TouchFrame => self.touch_frame(), + InputEvent::TouchFrame { time_usec } => self.touch_frame(time_usec), } } @@ -481,7 +486,14 @@ impl WlSeatGlobal { let pos = output.global.pos.get(); x += Fixed::from_int(pos.x1()); y += Fixed::from_int(pos.y1()); - (x, y) = self.set_pointer_cursor_position(x, y); + self.motion_event_abs(time_usec, x, y); + } + + pub fn motion_event_abs(self: &Rc, time_usec: u64, x: Fixed, y: Fixed) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_motion_abs(time_usec, x, y); + }); + let (x, y) = self.set_pointer_cursor_position(x, y); if let Some(c) = self.constraint.get() { if c.ty == ConstraintType::Lock || !c.contains(x.round_down(), y.round_down()) { c.deactivate(); @@ -493,7 +505,7 @@ impl WlSeatGlobal { self.cursor_moved(time_usec); } - fn motion_event( + pub fn motion_event( self: &Rc, time_usec: u64, dx: Fixed, @@ -501,6 +513,9 @@ impl WlSeatGlobal { dx_unaccelerated: Fixed, dy_unaccelerated: Fixed, ) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_motion(time_usec, dx, dy); + }); self.pointer_owner.relative_motion( self, time_usec, @@ -545,13 +560,37 @@ impl WlSeatGlobal { self.cursor_moved(time_usec); } - fn button_event(self: &Rc, time_usec: u64, button: u32, state: KeyState) { + pub fn button_event(self: &Rc, time_usec: u64, button: u32, state: KeyState) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_button(time_usec, button, state); + }); self.state.for_each_seat_tester(|t| { t.send_button(self.id, time_usec, button, state); }); self.pointer_owner.button(self, time_usec, button, state); } + pub fn axis_source(&self, axis_source: AxisSource) { + self.pointer_owner.axis_source(axis_source); + } + + pub fn axis_120(&self, delta: i32, axis: ScrollAxis, inverted: bool) { + self.pointer_owner.axis_120(delta, axis, inverted); + } + + pub fn axis_px(&self, delta: Fixed, axis: ScrollAxis, inverted: bool) { + self.pointer_owner.axis_px(delta, axis, inverted); + } + + pub fn axis_stop(&self, axis: ScrollAxis) { + self.pointer_owner.axis_stop(axis); + } + + pub fn axis_frame(self: &Rc, px_per_scroll_wheel: f64, time_usec: u64) { + self.pointer_owner + .frame(px_per_scroll_wheel, self, time_usec); + } + fn swipe_begin(self: &Rc, time_usec: u64, finger_count: u32) { self.state.for_each_seat_tester(|t| { t.send_swipe_begin(self.id, time_usec, finger_count); @@ -660,16 +699,26 @@ impl WlSeatGlobal { x_normed: Fixed, y_normed: Fixed, ) { - self.cursor_group().deactivate(); let x = Fixed::from_f64(rect.x1() as f64 + rect.width() as f64 * x_normed.to_f64()); let y = Fixed::from_f64(rect.y1() as f64 + rect.height() as f64 * y_normed.to_f64()); + self.touch_down_at(time_usec, id, x, y); + } + + pub fn touch_down_at(self: &Rc, time_usec: u64, id: i32, x: Fixed, y: Fixed) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_touch_down(id as _, x, y); + }); + self.cursor_group().deactivate(); self.state.for_each_seat_tester(|t| { t.send_touch_down(self.id, time_usec, id, x, y); }); self.touch_owner.down(self, time_usec, id, x, y); } - fn touch_up(self: &Rc, time_usec: u64, id: i32) { + pub fn touch_up(self: &Rc, time_usec: u64, id: i32) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_touch_up(id as _); + }); self.state.for_each_seat_tester(|t| { t.send_touch_up(self.id, time_usec, id); }); @@ -684,9 +733,16 @@ impl WlSeatGlobal { x_normed: Fixed, y_normed: Fixed, ) { - self.cursor_group().deactivate(); let x = Fixed::from_f64(rect.x1() as f64 + rect.width() as f64 * x_normed.to_f64()); let y = Fixed::from_f64(rect.y1() as f64 + rect.height() as f64 * y_normed.to_f64()); + self.touch_motion_at(time_usec, id, x, y); + } + + pub fn touch_motion_at(self: &Rc, time_usec: u64, id: i32, x: Fixed, y: Fixed) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_touch_motion(id as _, x, y); + }); + self.cursor_group().deactivate(); self.state.for_each_seat_tester(|t| { t.send_touch_motion(self.id, time_usec, id, x, y); }); @@ -694,16 +750,31 @@ impl WlSeatGlobal { } fn touch_cancel(self: &Rc, time_usec: u64, id: i32) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_touch_up(id as _); + }); self.state.for_each_seat_tester(|t| { t.send_touch_cancel(self.id, time_usec, id); }); self.touch_owner.cancel(self); } - fn touch_frame(self: &Rc) { + pub fn touch_frame(self: &Rc, time_usec: u64) { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_touch_frame(time_usec); + }); self.touch_owner.frame(self); } + pub fn key_event_with_seat_state( + self: &Rc, + time_usec: u64, + key: u32, + key_state: KeyState, + ) { + self.key_event(time_usec, key, key_state, || self.seat_xkb_state.get()); + } + pub(super) fn key_event( self: &Rc, time_usec: u64, @@ -787,8 +858,14 @@ impl WlSeatGlobal { Some(g) => g.on_key(time_usec, key, state, &xkb_state.kb_state), _ => node.node_on_key(self, time_usec, key, state, &xkb_state.kb_state), } + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_key(time_usec, key, state, &xkb_state.kb_state); + }); } if new_mods { + self.for_each_ei_seat(|ei_seat| { + ei_seat.handle_modifiers_changed(&xkb_state.kb_state); + }); self.state.for_each_seat_tester(|t| { t.send_modifiers(self.id, &xkb_state.kb_state.mods); }); @@ -808,6 +885,14 @@ impl WlSeatGlobal { drop(xkb_state); self.latest_kb_state.set(xkb_state_rc); } + + pub(super) fn for_each_ei_seat(&self, mut f: impl FnMut(&Rc)) { + if self.ei_seats.is_not_empty() { + for ei_seat in self.ei_seats.lock().values() { + f(ei_seat); + } + } + } } impl WlSeatGlobal { diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index b5e760c7..527ae609 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -13,7 +13,6 @@ use { wl_surface::{dnd_icon::DndIcon, WlSurface}, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, - state::DeviceHandlerData, tree::{ContainingNode, FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode}, utils::{clonecell::CloneCell, smallmap::SmallMap}, }, @@ -73,15 +72,18 @@ impl PointerOwnerHolder { self.pending_scroll.stop[axis as usize].set(true); } - pub fn frame(&self, dev: &DeviceHandlerData, seat: &Rc, time_usec: u64) { + pub fn frame(&self, px_per_scroll_wheel: f64, seat: &Rc, time_usec: u64) { self.pending_scroll.time_usec.set(time_usec); let pending = self.pending_scroll.take(); for axis in 0..2 { if let Some(dist) = pending.v120[axis].get() { - let px = (dist as f64 / AXIS_120 as f64) * dev.px_per_scroll_wheel.get(); + let px = (dist as f64 / AXIS_120 as f64) * px_per_scroll_wheel; pending.px[axis].set(Some(Fixed::from_f64(px))); } } + seat.for_each_ei_seat(|ei_seat| { + ei_seat.handle_pending_scroll(time_usec, &pending); + }); seat.state.for_each_seat_tester(|t| { t.send_axis(seat.id, time_usec, &pending); }); diff --git a/src/macros.rs b/src/macros.rs index 015dcbbe..41cd330c 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -655,3 +655,86 @@ macro_rules! pw_object_base { } } } + +macro_rules! ei_id { + ($name:ident) => { + #[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] + pub struct $name(u64); + + #[allow(dead_code)] + impl $name { + pub const NONE: Self = $name(0); + + pub const fn from_raw(raw: u64) -> Self { + Self(raw) + } + + pub fn raw(self) -> u64 { + self.0 + } + + pub fn is_some(self) -> bool { + self.0 != 0 + } + + pub fn is_none(self) -> bool { + self.0 == 0 + } + } + + impl From for $name { + fn from(f: crate::ei::ei_object::EiObjectId) -> Self { + Self(f.raw()) + } + } + + impl From<$name> for crate::ei::ei_object::EiObjectId { + fn from(f: $name) -> Self { + crate::ei::ei_object::EiObjectId::from_raw(f.0) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } + } + + impl std::fmt::LowerHex for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::LowerHex::fmt(&self.0, f) + } + } + }; +} + +macro_rules! ei_object_base { + ($self:ident = $oname:ident; version = $version:expr;) => { + impl crate::ei::ei_object::EiObjectBase for $oname { + fn id(&$self) -> crate::ei::ei_object::EiObjectId { + $self.id.into() + } + + fn version(&$self) -> crate::ei::ei_object::EiVersion { + $version + } + + fn client(&$self) -> &crate::ei::ei_client::EiClient { + &$self.client + } + + fn handle_request( + $self: std::rc::Rc, + client: &crate::ei::ei_client::EiClient, + request: u32, + parser: crate::utils::buffd::EiMsgParser<'_, '_>, + ) -> Result<(), crate::ei::ei_client::EiClientError> { + $self.handle_request_impl(client, request, parser) + } + + fn interface(&$self) -> crate::ei::ei_object::EiInterface { + crate::wire_ei::$oname + } + } + }; +} diff --git a/src/main.rs b/src/main.rs index 9e8d4cbb..e4e72a12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ mod damage; mod dbus; mod drm_feedback; mod edid; +mod ei; mod fixed; mod forker; mod format; @@ -97,6 +98,7 @@ mod video; mod wheel; mod wire; mod wire_dbus; +mod wire_ei; mod wire_xcon; mod wl_usr; mod xcon; diff --git a/src/portal.rs b/src/portal.rs index fdea28c4..368a435b 100644 --- a/src/portal.rs +++ b/src/portal.rs @@ -1,4 +1,5 @@ mod ptl_display; +mod ptl_remote_desktop; mod ptl_render_ctx; mod ptl_screencast; mod ptr_gui; @@ -17,6 +18,7 @@ use { pipewire::pw_con::{PwConHolder, PwConOwner}, portal::{ ptl_display::{watch_displays, PortalDisplay, PortalDisplayId}, + ptl_remote_desktop::{add_remote_desktop_dbus_members, RemoteDesktopSession}, ptl_render_ctx::PortalRenderCtx, ptl_screencast::{add_screencast_dbus_members, ScreencastSession}, }, @@ -195,6 +197,7 @@ async fn run_async( displays: Default::default(), dbus, screencasts: Default::default(), + remote_desktop_sessions: Default::default(), next_id: NumCell::new(1), render_ctxs: Default::default(), dma_buf_ids: Default::default(), @@ -210,6 +213,7 @@ async fn run_async( if let Some(pw_con) = &pw_con { add_screencast_dbus_members(&state, &pw_con.con, &obj); } + add_remote_desktop_dbus_members(&state, &obj); obj }; watch_displays(state.clone()).await; @@ -288,6 +292,7 @@ struct PortalState { displays: CopyHashMap>, dbus: Rc, screencasts: CopyHashMap>, + remote_desktop_sessions: CopyHashMap>, next_id: NumCell, render_ctxs: CopyHashMap>, dma_buf_ids: Rc, diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs index 51255c0d..675bb597 100644 --- a/src/portal/ptl_display.rs +++ b/src/portal/ptl_display.rs @@ -4,8 +4,8 @@ use { ifs::wl_seat::POINTER, object::Version, portal::{ - ptl_render_ctx::PortalRenderCtx, ptl_screencast::ScreencastSession, - ptr_gui::WindowData, PortalState, + ptl_remote_desktop::RemoteDesktopSession, ptl_render_ctx::PortalRenderCtx, + ptl_screencast::ScreencastSession, ptr_gui::WindowData, PortalState, }, utils::{ bitflags::BitflagsExt, clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -74,6 +74,7 @@ pub struct PortalDisplay { pub windows: CopyHashMap>, pub screencasts: CopyHashMap>, + pub remote_desktop_sessions: CopyHashMap>, } pub struct PortalOutput { @@ -303,7 +304,7 @@ fn finish_display_connect(dpy: Rc) { con: dpy.con.clone(), owner: Default::default(), caps: Default::default(), - version: Version(version.min(4)), + version: Version(version.min(5)), }); dpy.con.add_object(jc.clone()); dpy.registry.request_bind(name, version, jc.deref()); @@ -395,6 +396,7 @@ fn finish_display_connect(dpy: Rc) { vp, windows: Default::default(), screencasts: Default::default(), + remote_desktop_sessions: Default::default(), }); dpy.state.displays.set(dpy.id, dpy.clone()); diff --git a/src/portal/ptl_remote_desktop.rs b/src/portal/ptl_remote_desktop.rs new file mode 100644 index 00000000..8f14349d --- /dev/null +++ b/src/portal/ptl_remote_desktop.rs @@ -0,0 +1,384 @@ +mod remote_desktop_gui; + +use { + crate::{ + dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply, FALSE}, + ifs::jay_compositor::CREATE_EI_SESSION_SINCE, + portal::{ + ptl_display::{PortalDisplay, PortalDisplayId}, + ptl_remote_desktop::remote_desktop_gui::SelectionGui, + PortalState, PORTAL_SUCCESS, + }, + utils::{ + clonecell::{CloneCell, UnsafeCellCloneSafe}, + copyhashmap::CopyHashMap, + hash_map_ext::HashMapExt, + }, + wire_dbus::{ + org, + org::freedesktop::impl_::portal::{ + remote_desktop::{ + ConnectToEIS, ConnectToEISReply, CreateSession, CreateSessionReply, + SelectDevices, SelectDevicesReply, Start, StartReply, + }, + session::{CloseReply as SessionCloseReply, Closed}, + }, + }, + wl_usr::usr_ifs::usr_jay_ei_session::{UsrJayEiSession, UsrJayEiSessionOwner}, + }, + std::{borrow::Cow, cell::Cell, ops::Deref, rc::Rc}, + uapi::OwnedFd, +}; + +shared_ids!(ScreencastSessionId); +pub struct RemoteDesktopSession { + _id: ScreencastSessionId, + state: Rc, + pub app: String, + session_obj: DbusObject, + pub phase: CloneCell, +} + +#[derive(Clone)] +pub enum RemoteDesktopPhase { + Init, + DevicesSelected, + Selecting(Rc), + Starting(Rc), + Started(Rc), + Terminated, +} + +unsafe impl UnsafeCellCloneSafe for RemoteDesktopPhase {} + +pub struct SelectingDisplay { + pub session: Rc, + pub request_obj: Rc, + pub reply: Rc>>, + pub guis: CopyHashMap>, +} + +pub struct StartingRemoteDesktop { + pub session: Rc, + pub _request_obj: Rc, + pub reply: Rc>>, + pub dpy: Rc, + pub ei_session: Rc, +} + +pub struct StartedRemoteDesktop { + session: Rc, + dpy: Rc, + ei_session: Rc, + ei_fd: Cell>>, +} + +bitflags! { + DeviceTypes: u32; + + KEYBOARD = 1, + POINTER = 2, + TOUCHSCREEN = 4, +} + +impl UsrJayEiSessionOwner for StartingRemoteDesktop { + fn created(&self, fd: &Rc) { + { + let inner_type = DynamicType::DictEntry( + Box::new(DynamicType::String), + Box::new(DynamicType::Variant), + ); + let kt = DynamicType::Struct(vec![ + DynamicType::U32, + DynamicType::Array(Box::new(inner_type.clone())), + ]); + let variants = [ + DictEntry { + key: "devices".into(), + value: Variant::U32(DeviceTypes::all().0), + }, + DictEntry { + key: "clipboard_enabled".into(), + value: Variant::Bool(FALSE), + }, + DictEntry { + key: "streams".into(), + value: Variant::Array(kt, vec![]), + }, + ]; + self.reply.ok(&StartReply { + response: PORTAL_SUCCESS, + results: Cow::Borrowed(&variants[..]), + }); + } + let started = Rc::new(StartedRemoteDesktop { + session: self.session.clone(), + dpy: self.dpy.clone(), + ei_session: self.ei_session.clone(), + ei_fd: Cell::new(Some(fd.clone())), + }); + self.session + .phase + .set(RemoteDesktopPhase::Started(started.clone())); + started.ei_session.owner.set(Some(started.clone())); + } + + fn failed(&self, reason: &str) { + log::error!("Could not create session: {}", reason); + self.reply.err(reason); + self.session.kill(); + } +} + +impl SelectingDisplay { + pub fn starting(&self, dpy: &Rc) { + let builder = dpy.jc.create_ei_session(); + builder.set_app_id(&self.session.app); + let ei_session = builder.commit(); + let starting = Rc::new(StartingRemoteDesktop { + session: self.session.clone(), + _request_obj: self.request_obj.clone(), + reply: self.reply.clone(), + dpy: dpy.clone(), + ei_session, + }); + self.session + .phase + .set(RemoteDesktopPhase::Starting(starting.clone())); + starting.ei_session.owner.set(Some(starting.clone())); + dpy.remote_desktop_sessions.set( + self.session.session_obj.path().to_owned(), + self.session.clone(), + ); + } +} + +impl RemoteDesktopSession { + pub(super) fn kill(&self) { + self.session_obj.emit_signal(&Closed); + self.state + .remote_desktop_sessions + .remove(self.session_obj.path()); + match self.phase.set(RemoteDesktopPhase::Terminated) { + RemoteDesktopPhase::Init => {} + RemoteDesktopPhase::DevicesSelected => {} + RemoteDesktopPhase::Terminated => {} + RemoteDesktopPhase::Selecting(s) => { + s.reply.err("Session has been terminated"); + for gui in s.guis.lock().drain_values() { + gui.kill(false); + } + } + RemoteDesktopPhase::Starting(s) => { + s.reply.err("Session has been terminated"); + s.ei_session.con.remove_obj(s.ei_session.deref()); + s.dpy + .remote_desktop_sessions + .remove(self.session_obj.path()); + } + RemoteDesktopPhase::Started(s) => { + s.ei_session.con.remove_obj(s.ei_session.deref()); + s.dpy + .remote_desktop_sessions + .remove(self.session_obj.path()); + } + } + } + + fn dbus_select_devices( + self: &Rc, + _req: SelectDevices, + reply: PendingReply>, + ) { + match self.phase.get() { + RemoteDesktopPhase::Init => {} + _ => { + self.kill(); + reply.err("Devices have already been selected"); + return; + } + } + self.phase.set(RemoteDesktopPhase::DevicesSelected); + reply.ok(&SelectDevicesReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); + } + + fn dbus_start(self: &Rc, req: Start<'_>, reply: PendingReply>) { + match self.phase.get() { + RemoteDesktopPhase::DevicesSelected => {} + _ => { + self.kill(); + reply.err("Session is not in the correct phase for starting"); + return; + } + } + let request_obj = match self.state.dbus.add_object(req.handle.to_string()) { + Ok(r) => r, + Err(_) => { + self.kill(); + reply.err("Request handle is not unique"); + return; + } + }; + { + use org::freedesktop::impl_::portal::request::*; + request_obj.add_method::({ + let slf = self.clone(); + move |_, pr| { + slf.kill(); + pr.ok(&CloseReply); + } + }); + } + let guis = CopyHashMap::new(); + for dpy in self.state.displays.lock().values() { + if dpy.outputs.len() > 0 && dpy.jc.version >= CREATE_EI_SESSION_SINCE { + guis.set(dpy.id, SelectionGui::new(self, dpy)); + } + } + if guis.is_empty() { + self.kill(); + reply.err("There are no running displays"); + return; + } + self.phase + .set(RemoteDesktopPhase::Selecting(Rc::new(SelectingDisplay { + session: self.clone(), + request_obj: Rc::new(request_obj), + reply: Rc::new(reply), + guis, + }))); + } + + fn dbus_connect_to_eis( + self: &Rc, + _req: ConnectToEIS, + reply: PendingReply, + ) { + let RemoteDesktopPhase::Started(started) = self.phase.get() else { + self.kill(); + reply.err("Sources have already been selected"); + return; + }; + let Some(fd) = started.ei_fd.take() else { + self.kill(); + reply.err("EI file descriptor has already been consumed"); + return; + }; + reply.ok(&ConnectToEISReply { fd }); + } +} + +impl UsrJayEiSessionOwner for StartedRemoteDesktop { + fn destroyed(&self) { + self.session.kill(); + } +} + +pub(super) fn add_remote_desktop_dbus_members(state_: &Rc, object: &DbusObject) { + use org::freedesktop::impl_::portal::remote_desktop::*; + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_create_session(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_select_devices(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_start(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_connect_to_eis(&state, req, pr); + }); + object.set_property::(Variant::U32(DeviceTypes::all().0)); + object.set_property::(Variant::U32(2)); +} + +fn dbus_create_session( + state: &Rc, + req: CreateSession, + reply: PendingReply>, +) { + log::info!("Create remote desktop session {:#?}", req); + if state + .remote_desktop_sessions + .contains(req.session_handle.0.deref()) + { + reply.err("Session already exists"); + return; + } + let obj = match state.dbus.add_object(req.session_handle.0.to_string()) { + Ok(obj) => obj, + Err(_) => { + reply.err("Session path is not unique"); + return; + } + }; + let session = Rc::new(RemoteDesktopSession { + _id: state.id(), + state: state.clone(), + app: req.app_id.to_string(), + session_obj: obj, + phase: CloneCell::new(RemoteDesktopPhase::Init), + }); + { + use org::freedesktop::impl_::portal::session::*; + let ses = session.clone(); + session.session_obj.add_method::(move |_, pr| { + ses.kill(); + pr.ok(&SessionCloseReply); + }); + session.session_obj.set_property::(Variant::U32(2)); + } + state + .remote_desktop_sessions + .set(req.session_handle.0.to_string(), session); + reply.ok(&CreateSessionReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); +} + +fn dbus_select_devices( + state: &Rc, + req: SelectDevices, + reply: PendingReply>, +) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_select_devices(req, reply); + } +} + +fn dbus_start(state: &Rc, req: Start, reply: PendingReply>) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_start(req, reply); + } +} + +fn dbus_connect_to_eis( + state: &Rc, + req: ConnectToEIS, + reply: PendingReply, +) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_connect_to_eis(req, reply); + } +} + +fn get_session( + state: &Rc, + reply: &PendingReply, + handle: &str, +) -> Option> { + let res = state.remote_desktop_sessions.get(handle); + if res.is_none() { + let msg = format!("Remote desktop session `{}` does not exist", handle); + reply.err(&msg); + } + res +} diff --git a/src/portal/ptl_remote_desktop/remote_desktop_gui.rs b/src/portal/ptl_remote_desktop/remote_desktop_gui.rs new file mode 100644 index 00000000..1963a32f --- /dev/null +++ b/src/portal/ptl_remote_desktop/remote_desktop_gui.rs @@ -0,0 +1,159 @@ +use { + crate::{ + ifs::wl_seat::{wl_pointer::PRESSED, BTN_LEFT}, + portal::{ + ptl_display::{PortalDisplay, PortalOutput, PortalSeat}, + ptl_remote_desktop::{RemoteDesktopPhase, RemoteDesktopSession}, + ptr_gui::{ + Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow, + OverlayWindowOwner, + }, + }, + theme::Color, + utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt}, + }, + std::rc::Rc, +}; + +const H_MARGIN: f32 = 30.0; +const V_MARGIN: f32 = 20.0; + +pub struct SelectionGui { + remote_desktop_session: Rc, + dpy: Rc, + surfaces: CopyHashMap>, +} + +pub struct SelectionGuiSurface { + gui: Rc, + output: Rc, + overlay: Rc, +} + +struct StaticButton { + surface: Rc, + role: ButtonRole, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum ButtonRole { + Accept, + Reject, +} + +impl SelectionGui { + pub fn kill(&self, upwards: bool) { + for surface in self.surfaces.lock().drain_values() { + surface.overlay.data.kill(false); + } + if let RemoteDesktopPhase::Selecting(s) = self.remote_desktop_session.phase.get() { + s.guis.remove(&self.dpy.id); + if upwards && s.guis.is_empty() { + self.remote_desktop_session.kill(); + } + } + } +} + +fn create_accept_gui(surface: &Rc) -> Rc { + let app = &surface.gui.remote_desktop_session.app; + let text = if app.is_empty() { + format!("An application wants to generate/monitor input") + } else { + format!("`{}` wants to generate/monitor input", app) + }; + let label = Rc::new(Label::default()); + *label.text.borrow_mut() = text; + let accept_button = static_button(surface, ButtonRole::Accept, "Allow"); + let reject_button = static_button(surface, ButtonRole::Reject, "Reject"); + for button in [&accept_button, &reject_button] { + button.border_color.set(Color::from_gray(100)); + button.border.set(2.0); + button.padding.set(5.0); + } + accept_button.bg_color.set(Color::from_rgb(170, 200, 170)); + accept_button + .bg_hover_color + .set(Color::from_rgb(170, 255, 170)); + reject_button.bg_color.set(Color::from_rgb(200, 170, 170)); + reject_button + .bg_hover_color + .set(Color::from_rgb(255, 170, 170)); + let flow = Rc::new(Flow::default()); + flow.orientation.set(Orientation::Vertical); + flow.cross_align.set(Align::Center); + flow.in_margin.set(V_MARGIN); + flow.cross_margin.set(H_MARGIN); + *flow.elements.borrow_mut() = vec![label, accept_button, reject_button]; + flow +} + +impl OverlayWindowOwner for SelectionGuiSurface { + fn kill(&self, upwards: bool) { + self.gui.dpy.windows.remove(&self.overlay.data.surface.id); + self.gui.surfaces.remove(&self.output.global_id); + if upwards && self.gui.surfaces.is_empty() { + self.gui.kill(true); + } + } +} + +impl SelectionGui { + pub fn new(ss: &Rc, dpy: &Rc) -> Rc { + let gui = Rc::new(SelectionGui { + remote_desktop_session: ss.clone(), + dpy: dpy.clone(), + surfaces: Default::default(), + }); + for output in dpy.outputs.lock().values() { + let sgs = Rc::new(SelectionGuiSurface { + gui: gui.clone(), + output: output.clone(), + overlay: OverlayWindow::new(output), + }); + let element = create_accept_gui(&sgs); + sgs.overlay.data.content.set(Some(element)); + gui.dpy + .windows + .set(sgs.overlay.data.surface.id, sgs.overlay.data.clone()); + gui.surfaces.set(output.global_id, sgs); + } + gui + } +} + +impl ButtonOwner for StaticButton { + fn button(&self, _seat: &PortalSeat, button: u32, state: u32) { + if button != BTN_LEFT || state != PRESSED { + return; + } + match self.role { + ButtonRole::Accept => { + log::info!("User has accepted the request"); + let selecting = match self.surface.gui.remote_desktop_session.phase.get() { + RemoteDesktopPhase::Selecting(selecting) => selecting, + _ => return, + }; + for gui in selecting.guis.lock().drain_values() { + gui.kill(false); + } + selecting.starting(&self.surface.output.dpy); + } + ButtonRole::Reject => { + log::info!("User has rejected the remote desktop request"); + self.surface.gui.remote_desktop_session.kill(); + } + } + } +} + +fn static_button(surface: &Rc, role: ButtonRole, text: &str) -> Rc