diff --git a/Cargo.lock b/Cargo.lock index a7f31fc..bd6a3e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2073,6 +2073,7 @@ dependencies = [ "fff-search", "freedesktop-desktop-entry", "iced_accessibility", + "iced_winit", "image", "jiff", "libc", @@ -6119,7 +6120,6 @@ dependencies = [ [[package]] name = "smithay" version = "0.7.0" -source = "git+https://github.com/Smithay/smithay?rev=8a270232f1bfc416c781273c98a3f52a812a0bba#8a270232f1bfc416c781273c98a3f52a812a0bba" dependencies = [ "appendlist", "bitflags 2.13.2", diff --git a/Cargo.toml b/Cargo.toml index a9dd002..9f8f28b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "portal", ] resolver = "3" +exclude = ["vendor/smithay"] [workspace.package] version = "0.1.0" @@ -72,3 +73,6 @@ wayland-client = "0.31.15" wayland-scanner = "0.31.11" wayland-server = "0.31.14" wayland-protocols-wlr = { version = "0.3.12", features = ["client"] } + +[patch."https://github.com/Smithay/smithay"] +smithay = { path = "vendor/smithay" } diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 9e14efb..9a3436d 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -31,7 +31,7 @@ use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneE use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter; use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType}; use smithay::backend::egl::EGLDisplay; -use smithay::backend::input::InputEvent; +use smithay::backend::input::{Device as _, InputEvent}; use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; use smithay::backend::renderer::element::{RenderElement, UnderlyingStorage}; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; @@ -425,6 +425,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() } InputEvent::DeviceRemoved { device } => { + state.super_tap.device_removed(&device.id()); state.cancel_window_interaction(); if let Some(backend) = state.direct_backend.as_mut() { backend @@ -439,6 +440,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() .handle() .insert_source(notifier, move |event, _, state| match event { SessionEvent::PauseSession => { + state.super_tap.reset(); state.cancel_window_interaction(); state.set_binding_mode("default"); let handle = state.loop_handle.clone(); diff --git a/compositor/src/backends/direct/transaction.rs b/compositor/src/backends/direct/transaction.rs index 8994966..03c1349 100644 --- a/compositor/src/backends/direct/transaction.rs +++ b/compositor/src/backends/direct/transaction.rs @@ -450,8 +450,8 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> { } } -/// All allocations/tests precede commits. Logical publication waits for the -/// final usable inventory; devices are independently applied and restored. +/// A complete device test precedes application. Release former connector owners +/// before per-surface tests; publish only after the final inventory is usable. pub(super) fn apply_device( state: &mut Ferese, node: DrmNode, @@ -483,6 +483,17 @@ pub(super) fn apply_device( .filter(|crtc| !selections.contains_key(crtc)) .copied() .collect::>(); + // A connector replacement can reuse the same CRTC, so it is not a removal + // from the keyed inventory. Its old routing/resources must still be released + // before staging any other output's new mode (for example HDMI -> DP). + let mut releases = removals.clone(); + releases.extend(selections.iter().filter_map(|(crtc, selection)| { + device + .outputs + .get(crtc) + .and_then(|output| (output.connector != selection.connector.handle()).then_some(*crtc)) + })); + releases.sort_by_key(|crtc| u32::from(*crtc)); let hardware_changes = changed .iter() .filter(|crtc| { @@ -500,7 +511,7 @@ pub(super) fn apply_device( staged: HashMap::new(), replaced: HashSet::new(), }; - let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &removals); + let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &releases); let mut staged = std::mem::take(&mut driver.staged); if let Err(failure) = result { for crtc in failure.unavailable { diff --git a/compositor/src/config/bindings.rs b/compositor/src/config/bindings.rs index 76a229a..7ebdad9 100644 --- a/compositor/src/config/bindings.rs +++ b/compositor/src/config/bindings.rs @@ -123,6 +123,7 @@ pub(super) fn parse_binding( BindingMatch::Keysym => BindingTrigger::Keysym(identity.key), BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)), BindingMatch::Pointer => BindingTrigger::Pointer(PointerTrigger::from_key(identity.key)), + BindingMatch::SuperTap => BindingTrigger::SuperTap, BindingMatch::Swipe => BindingTrigger::Swipe { fingers: identity.key / 4, direction: [ @@ -154,6 +155,21 @@ pub(super) fn binding_identity( configured: &BindingConfig, keymap: &xkb::Keymap, ) -> Result { + if matches!( + configured.keys.trim().to_ascii_lowercase().as_str(), + "super" | "logo" | "mod4" + ) { + if configured.match_mode != BindingMatch::Keysym { + return Err(ConfigError::Binding( + "Super tap cannot use physical key matching".into(), + )); + } + return Ok(BindingIdentity { + modifiers: BindingModifiers::default(), + match_mode: BindingMatch::SuperTap, + key: 0, + }); + } let (modifiers, key) = parse_chord(&configured.keys)?; if let Some(trigger) = PointerTrigger::parse(&key)? { if configured.match_mode != BindingMatch::Keysym { @@ -192,7 +208,7 @@ pub(super) fn binding_identity( configured.keys )) })?, - BindingMatch::Swipe | BindingMatch::Pointer => { + BindingMatch::Swipe | BindingMatch::Pointer | BindingMatch::SuperTap => { return Err(ConfigError::Binding( "gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(), )); @@ -211,6 +227,7 @@ pub(super) fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key), BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()), BindingTrigger::Pointer(trigger) => (BindingMatch::Pointer, trigger.key()), + BindingTrigger::SuperTap => (BindingMatch::SuperTap, 0), BindingTrigger::Swipe { fingers, direction } => { (BindingMatch::Swipe, fingers * 4 + direction as u32) } @@ -574,6 +591,7 @@ pub(super) enum BindingTrigger { Keysym(u32), Physical(Keycode), Pointer(PointerTrigger), + SuperTap, Swipe { fingers: u32, direction: crate::gestures::SwipeDirection, @@ -679,6 +697,7 @@ impl Binding { }) } (BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right, + (BindingTrigger::SuperTap, BindingTrigger::SuperTap) => true, _ => false, } } @@ -702,7 +721,9 @@ impl Binding { && match self.trigger { BindingTrigger::Keysym(expected) => keysyms.contains(&expected), BindingTrigger::Physical(expected) => expected == keycode, - BindingTrigger::Swipe { .. } | BindingTrigger::Pointer(_) => false, + BindingTrigger::Swipe { .. } + | BindingTrigger::Pointer(_) + | BindingTrigger::SuperTap => false, } } @@ -797,6 +818,7 @@ impl Binding { } } parts.push(match self.trigger { + BindingTrigger::SuperTap => "Super (tap)".into(), BindingTrigger::Pointer(trigger) => trigger.label(), BindingTrigger::Keysym(symbol) => { let name = xkb::keysym_get_name(xkb::Keysym::new(symbol)); @@ -852,6 +874,8 @@ pub(super) enum BindingMatch { Swipe, #[serde(skip)] Pointer, + #[serde(skip)] + SuperTap, } #[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)] diff --git a/compositor/src/config/bindings/index.rs b/compositor/src/config/bindings/index.rs index 9bce980..e9bfa87 100644 --- a/compositor/src/config/bindings/index.rs +++ b/compositor/src/config/bindings/index.rs @@ -27,7 +27,10 @@ impl From> for BindingSet { if matches!( identity.match_mode, - BindingMatch::Keysym | BindingMatch::Physical | BindingMatch::Pointer + BindingMatch::Keysym + | BindingMatch::Physical + | BindingMatch::Pointer + | BindingMatch::SuperTap ) { index.entry(identity).or_insert(position); } @@ -46,6 +49,16 @@ impl std::ops::Deref for BindingSet { } impl BindingSet { + pub(crate) fn super_tap(&self) -> Option<&Binding> { + self.index + .get(&BindingIdentity { + modifiers: BindingModifiers::default(), + match_mode: BindingMatch::SuperTap, + key: 0, + }) + .map(|index| &self.ordered[*index]) + } + pub(crate) fn binding_for_pointer( &self, trigger: super::PointerTrigger, diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index 785bbb8..c5d840a 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -1,14 +1,17 @@ use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::renderer::utils::on_commit_buffer_handler; -use smithay::reexports::wayland_server::Client; +use smithay::reexports::calloop::Interest; use smithay::reexports::wayland_server::protocol::wl_buffer; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::reexports::wayland_server::{Client, Resource}; use smithay::wayland::buffer::BufferHandler; use smithay::wayland::compositor::{ - CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role, - is_sync_subsurface, + BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes, + add_blocker, add_pre_commit_hook, get_parent, get_role, is_sync_subsurface, with_states, +}; +use smithay::wayland::dmabuf::{ + DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf, }; -use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}; use smithay::wayland::shm::{ShmHandler, ShmState}; use super::{layer_shell, xdg_shell}; @@ -29,6 +32,39 @@ impl CompositorHandler for Ferese { fn new_surface(&mut self, surface: &WlSurface) { self.update_surface_preferences(surface); + // A commit is not proof that the GPU has finished writing its dma-buf. + // Keep the previous surface state renderable until the acquire fence + // signals, including during resize snapshots and subsurface updates. + add_pre_commit_hook::(surface, |state, _dh, surface| { + let dmabuf = with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .buffer + .as_ref() + .and_then(|assignment| match assignment { + BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).ok().cloned(), + _ => None, + }) + }); + let Some((dmabuf, client)) = dmabuf.zip(surface.client()) else { + return; + }; + if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) { + let inserted = state.loop_handle.insert_source(source, move |_, _, state| { + let dh = state.display_handle.clone(); + state + .client_compositor_state(&client) + .blocker_cleared(state, &dh); + Ok(()) + }); + match inserted { + Ok(_) => add_blocker(surface, blocker), + Err(error) => tracing::warn!(%error, "could not monitor surface GPU readiness"), + } + } + }); } fn new_subsurface(&mut self, surface: &WlSurface, _parent: &WlSurface) { diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index 8e03697..7602e53 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -81,6 +81,7 @@ impl SeatHandler for Ferese { } fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { + self.super_tap.cancel(); self.pending_layer_focus = None; // Smithay has already sent enter. last_enter uses a separate mutex; // querying current_focus here would reenter the held keyboard lock. @@ -157,6 +158,7 @@ impl KeyboardShortcutsInhibitHandler for Ferese { .and_then(|keyboard| keyboard.current_focus()); if focused.as_ref() == Some(inhibitor.wl_surface()) { + self.super_tap.cancel(); inhibitor.activate(); self.active_shortcuts_inhibitor = Some(inhibitor); } diff --git a/compositor/src/handlers/virtual_keyboard.rs b/compositor/src/handlers/virtual_keyboard.rs index a2e0ca2..1111dc6 100644 --- a/compositor/src/handlers/virtual_keyboard.rs +++ b/compositor/src/handlers/virtual_keyboard.rs @@ -19,6 +19,7 @@ impl VirtualKeyboardHandler for Ferese { _ => None, }; if let Some(device) = device { + self.super_tap.cancel(); keyboard.release_source(self, device.source()); } return; @@ -78,7 +79,10 @@ impl VirtualKeyboardHandler for Ferese { context.set_modifier_mask(mods_depressed, mods_latched, mods_locked, group) }); } - InputEvent::DeviceRemoved { device } => keyboard.release_source(self, device.source()), + InputEvent::DeviceRemoved { device } => { + self.super_tap.cancel(); + keyboard.release_source(self, device.source()); + } _ => (), } } diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 3a8058b..d02ab12 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -1,6 +1,7 @@ mod window; pub(crate) use window::WindowInteraction; mod pointer; +pub(crate) mod super_tap; use ferese_layout::Direction; pub(crate) use pointer::ScrollBindings; use smithay::backend::input::{ @@ -150,6 +151,16 @@ impl Ferese { event: InputEvent, source: KeyboardSource, ) { + match &event { + InputEvent::Keyboard { event } => self.super_tap.observe_key(source, event.key_code()), + InputEvent::PointerButton { event } => self.super_tap.pointer_button( + event.device().id(), + event.button_code(), + event.state(), + ), + InputEvent::DeviceRemoved { device } => self.super_tap.device_removed(&device.id()), + _ => self.super_tap.cancel(), + } if self.input_capture.active() { self.set_binding_mode("default"); } @@ -320,6 +331,10 @@ impl Ferese { let keyboard = self.seat.get_keyboard().expect("seat has a keyboard"); let keycode = event.key_code(); let state = event.state(); + let keys_were_clear = keyboard.pressed_keys().is_empty(); + let tap_focus = keyboard.current_focus().map(|surface| surface.id()); + let tap_allowed = self.super_tap_allowed(&keyboard); + let mut tap_action = None; let was_captured = self.input_capture.active(); if self.input_capture.captures(1) { @@ -349,6 +364,18 @@ impl Ferese { serial, event.time(), |data, modifiers, keysym| { + if state == KeyState::Released { + if data.super_tap.release(source, keycode, tap_allowed, tap_focus.clone()) { + tap_action = data.binding_modes.bindings(&data.bindings) + .super_tap().map(|binding| binding.action.clone()); + } + } else if keys_were_clear && tap_allowed + && !modifiers.ctrl && !modifiers.alt && !modifiers.shift + && keysym.raw_syms().iter().any(|symbol| matches!(symbol.raw(), keysyms::KEY_Super_L | keysyms::KEY_Super_R)) + && data.binding_modes.bindings(&data.bindings).super_tap().is_some() + { + data.super_tap.arm(source, keycode, tap_focus.clone()); + } if data.focus_cycle.is_some() { if data.session_lock.active() || data.input_capture.captures(1) { data.cancel_focus_cycle(); @@ -530,6 +557,11 @@ impl Ferese { FilterResult::Intercept(()) }, ); + // Finish forwarding the original release to its owner before + // changing focus or opening a layer surface for the action. + if let Some(action) = tap_action { + self.execute_binding(action); + } if state == KeyState::Pressed && intercepted.is_none() && client_grab { self.record_activation_input(serial, interaction_focus); } diff --git a/compositor/src/input/super_tap.rs b/compositor/src/input/super_tap.rs new file mode 100644 index 0000000..b2b49d1 --- /dev/null +++ b/compositor/src/input/super_tap.rs @@ -0,0 +1,88 @@ +//! A modifier tap is armed only by an otherwise isolated Super press. It never +//! owns the keyboard event: normal clients, IMEs and Super chords keep theirs. +use std::collections::HashSet; + +use smithay::backend::input::ButtonState; +use smithay::input::keyboard::{KeyboardHandle, KeyboardSource, Keycode}; +use smithay::reexports::wayland_server::backend::ObjectId; +use smithay::wayland::input_method::InputMethodKeyboardGrab; +use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat; + +#[derive(Default)] +pub(crate) struct SuperTap { + candidate: Option<(KeyboardSource, Keycode, Option)>, + pointer_buttons: HashSet<(String, u32)>, +} + +impl SuperTap { + pub(crate) fn cancel(&mut self) { + self.candidate = None; + } + + pub(crate) fn reset(&mut self) { + self.cancel(); + self.pointer_buttons.clear(); + } + + pub(crate) fn device_removed(&mut self, device: &str) { + self.cancel(); + self.pointer_buttons.retain(|(owner, _)| owner != device); + } + + pub(super) fn pointer_button(&mut self, device: String, button: u32, state: ButtonState) { + self.cancel(); + if state == ButtonState::Pressed { + self.pointer_buttons.insert((device, button)); + } else { + self.pointer_buttons.remove(&(device, button)); + } + } + + // Observe before Smithay deduplicates a second input source's identical + // keycode. That second source must invalidate an isolated tap as well. + pub(super) fn observe_key(&mut self, source: KeyboardSource, key: Keycode) { + if self + .candidate + .as_ref() + .is_some_and(|(owner, held, _)| *owner != source || *held != key) + { + self.cancel(); + } + } + + pub(super) fn arm(&mut self, source: KeyboardSource, key: Keycode, focus: Option) { + self.candidate = Some((source, key, focus)); + } + + pub(super) fn release( + &mut self, + source: KeyboardSource, + key: Keycode, + allowed: bool, + focus: Option, + ) -> bool { + self.candidate.take().is_some_and(|(owner, held, initial)| { + allowed && owner == source && held == key && focus == initial + }) + } +} + +impl crate::Ferese { + pub(super) fn super_tap_allowed(&self, keyboard: &KeyboardHandle) -> bool { + !self.session_lock.active() + && !self.input_capture.active() + && !self.seat.keyboard_shortcuts_inhibited() + && self.active_shortcuts_inhibitor.is_none() + && self.pointer_operation.is_none() + && self.super_tap.pointer_buttons.is_empty() + && self.seat.get_pointer().is_none_or(|pointer| !pointer.is_grabbed()) + // A native Rime grab is part of normal text input. Popup/client + // grabs still retain their exclusive keyboard interaction. + && keyboard.with_grab(|_, grab| grab.is::()).unwrap_or(true) + } +} + +#[cfg(test)] +mod security_tests; +#[cfg(test)] +mod tests; diff --git a/compositor/src/input/super_tap/security_tests.rs b/compositor/src/input/super_tap/security_tests.rs new file mode 100644 index 0000000..dbc28f5 --- /dev/null +++ b/compositor/src/input/super_tap/security_tests.rs @@ -0,0 +1,181 @@ +use crate::Ferese; +use crate::input::binding_mode_tests::key; +use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window}; +use smithay::reexports::calloop::EventLoop; +use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::{ + ext_session_lock_manager_v1::ExtSessionLockManagerV1, ext_session_lock_v1::ExtSessionLockV1, +}; +use smithay::reexports::wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::server::zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1; +use smithay::reexports::wayland_server::{Client, Resource}; +use smithay::utils::SERIAL_COUNTER; +use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat; +use std::io::Read; +use std::os::unix::net::UnixStream; + +const SOURCE: &str = "binding Super binding-mode tapped\nbinding-mode tapped { binding a none; }\n"; + +fn fixture() -> (Ferese, EventLoop<'static, Ferese>) { + let (mut state, events, _) = crate::input::layer_focus_tests::fixture(); + state.reload_config_source(SOURCE.into()).unwrap(); + (state, events) +} + +fn bind_seat(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream) { + request(wire, 1, 1, &[7], None); + dispatch(events, state); + wire.set_nonblocking(true).unwrap(); + let mut bytes = Vec::new(); + let _ = wire.read_to_end(&mut bytes); + wire.set_nonblocking(false).unwrap(); + let mut offset = 0; + let mut seat_global = None; + while offset < bytes.len() { + let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); + if object == 7 && header as u16 == 0 { + let length = + u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize; + if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" { + seat_global = Some(u32::from_ne_bytes( + bytes[offset + 8..offset + 12].try_into().unwrap(), + )); + } + } + offset += (header >> 16) as usize; + } + let mut args = vec![seat_global.unwrap(), 8]; + args.extend( + b"wl_seat\0" + .chunks_exact(4) + .map(|word| u32::from_ne_bytes(word.try_into().unwrap())), + ); + args.extend([7, 8]); + request(wire, 7, 0, &args, None); + dispatch(events, state); +} + +fn create_manager(state: &mut Ferese, client: &Client) -> R +where + Ferese: smithay::reexports::wayland_server::Dispatch, +{ + client + .create_resource::( + &state.display_handle, + 1, + smithay::wayland::GlobalData, + ) + .unwrap() +} + +#[test] +fn inhibitor_protocol_cancels_a_held_tap_even_if_destroyed_before_release() { + if !crate::startup_tests::private_runtime( + "input::super_tap::security_tests::inhibitor_protocol_cancels_a_held_tap_even_if_destroyed_before_release", + ) { + return; + } + let (mut state, mut events) = fixture(); + let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030); + let surface = window.toplevel().unwrap().wl_surface().clone(); + let client = surface.client().unwrap(); + let keyboard = state.seat.get_keyboard().unwrap(); + keyboard.set_focus( + &mut state, + Some(surface.clone()), + SERIAL_COUNTER.next_serial(), + ); + bind_seat(&mut state, &mut events, &mut wire); + let manager = create_manager::(&mut state, &client); + key(&mut state, 133, true); + assert!(state.super_tap.candidate.is_some()); + let opcode = ZwpKeyboardShortcutsInhibitManagerV1::interface() + .requests + .iter() + .position(|request| request.name == "inhibit_shortcuts") + .unwrap() as u32; + request( + &mut wire, + manager.id().protocol_id(), + opcode, + &[9, surface.id().protocol_id(), 8], + None, + ); + dispatch(&mut events, &mut state); + assert!(state.seat.keyboard_shortcuts_inhibited()); + assert!( + state.super_tap.candidate.is_none(), + "activation must cancel immediately" + ); + request(&mut wire, 9, 0, &[], None); + dispatch(&mut events, &mut state); + assert!(!state.seat.keyboard_shortcuts_inhibited()); + key(&mut state, 133, false); + assert_eq!(state.binding_modes.active(), None); + key(&mut state, 133, true); + key(&mut state, 133, false); + assert_eq!(state.binding_modes.active(), Some("tapped")); +} + +#[test] +fn session_lock_protocol_cancels_pending_tap_before_further_keyboard_input() { + if !crate::startup_tests::private_runtime( + "input::super_tap::security_tests::session_lock_protocol_cancels_pending_tap_before_further_keyboard_input", + ) { + return; + } + let (mut state, mut events) = fixture(); + let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030); + let surface = window.toplevel().unwrap().wl_surface().clone(); + let client = surface.client().unwrap(); + state.seat.get_keyboard().unwrap().set_focus( + &mut state, + Some(surface), + SERIAL_COUNTER.next_serial(), + ); + let manager = create_manager::(&mut state, &client); + key(&mut state, 133, true); + assert!(state.super_tap.candidate.is_some()); + let opcode = ExtSessionLockManagerV1::interface() + .requests + .iter() + .position(|request| request.name == "lock") + .unwrap() as u32; + request(&mut wire, manager.id().protocol_id(), opcode, &[7], None); + dispatch(&mut events, &mut state); + assert!(state.session_lock.active()); + assert!( + client + .object_from_protocol_id::(&state.display_handle, 7) + .is_ok() + ); + assert!(state.super_tap.candidate.is_none()); + key(&mut state, 133, false); + key(&mut state, 133, true); + key(&mut state, 133, false); + assert_eq!(state.binding_modes.active(), None); +} + +#[test] +fn focus_departure_and_return_do_not_restore_a_held_tap() { + if !crate::startup_tests::private_runtime( + "input::super_tap::security_tests::focus_departure_and_return_do_not_restore_a_held_tap", + ) { + return; + } + let (mut state, mut events) = fixture(); + let (window, _wire) = test_window(&mut state, &mut events, 0xff102030); + let surface = window.toplevel().unwrap().wl_surface().clone(); + let keyboard = state.seat.get_keyboard().unwrap(); + keyboard.set_focus( + &mut state, + Some(surface.clone()), + SERIAL_COUNTER.next_serial(), + ); + key(&mut state, 133, true); + assert!(state.super_tap.candidate.is_some()); + keyboard.set_focus(&mut state, None, SERIAL_COUNTER.next_serial()); + keyboard.set_focus(&mut state, Some(surface), SERIAL_COUNTER.next_serial()); + assert!(state.super_tap.candidate.is_none()); + key(&mut state, 133, false); + assert_eq!(state.binding_modes.active(), None); +} diff --git a/compositor/src/input/super_tap/tests.rs b/compositor/src/input/super_tap/tests.rs new file mode 100644 index 0000000..e5b0017 --- /dev/null +++ b/compositor/src/input/super_tap/tests.rs @@ -0,0 +1,508 @@ +use super::*; +use crate::Ferese; +use crate::config::{BindingAction, Config}; +use crate::input::binding_mode_tests::key; +use crate::input::{Event, InputBackend, InputEvent, PointerButtonEvent, PointerMotionEvent}; +use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; +use smithay::backend::input::{ + Axis, AxisRelativeDirection, AxisSource, InputTime, KeyState, PointerAxisEvent, UnusedEvent, +}; +use smithay::backend::winit::WinitVirtualDevice; +use smithay::input::keyboard::{GrabStartData, KeyboardGrab, KeyboardInnerHandle, ModifiersState}; +use smithay::reexports::calloop::EventLoop; +use smithay::reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface}; +use smithay::utils::{SERIAL_COUNTER, Serial}; +use std::io::Read; +use std::os::unix::net::UnixStream; +use std::sync::Arc; + +macro_rules! private { + ($test:ident) => { + if !crate::startup_tests::private_runtime(concat!( + "input::super_tap::tests::", + stringify!($test) + )) { + return; + } + }; +} +const SOURCE: &str = "binding Super binding-mode tapped\nbinding-mode tapped { binding a none; }\nbinding Super+Space binding-mode chord\nbinding-mode chord { binding a none; }\n"; +fn fixture(source: &str) -> (Ferese, EventLoop<'static, Ferese>) { + let (mut state, events, _) = crate::input::layer_focus_tests::fixture(); + state.reload_config_source(source.into()).unwrap(); + (state, events) +} +fn tap(state: &mut Ferese, code: u32) { + key(state, code, true); + key(state, code, false); +} +fn tapped(state: &Ferese) -> bool { + state.binding_modes.active() == Some("tapped") +} + +#[test] +fn parsing_indexes_taps_separately_and_preserves_space_chord() { + let config = Config::parse_source("binding Super toggle-launcher\n").unwrap(); + let bindings: crate::config::BindingSet = config + .bindings(&config.input_settings().unwrap()) + .unwrap() + .into(); + assert!(matches!( + bindings.super_tap().unwrap().action, + BindingAction::ToggleLauncher + )); + assert!( + bindings + .binding_for_key( + Keycode::new(133), + &[smithay::input::keyboard::keysyms::KEY_Super_L], + true, + false, + false, + false + ) + .is_none() + ); + assert!(matches!( + bindings + .binding_for_key( + Keycode::new(65), + &[smithay::input::keyboard::keysyms::KEY_space], + true, + false, + false, + false + ) + .unwrap() + .action, + BindingAction::ToggleLauncher + )); + let guide = bindings.super_tap().unwrap().guide_entry(None).unwrap(); + assert_eq!(guide["keys"], "Super (tap)"); + for source in [ + "binding Super toggle-launcher\nbinding Logo none\n", + "binding Super match=physical action=toggle-launcher\n", + "binding Ctrl toggle-launcher\n", + ] { + let config = Config::parse_source(source).unwrap(); + assert!( + config.bindings(&config.input_settings().unwrap()).is_err(), + "{source}" + ); + } + let disabled = Config::parse_source("binding Super disabled=#true\n").unwrap(); + let bindings: crate::config::BindingSet = disabled + .bindings(&disabled.input_settings().unwrap()) + .unwrap() + .into(); + assert!(bindings.super_tap().is_none()); +} + +#[test] +fn left_right_taps_execute_on_release_and_repeat_does_not_rearm() { + private!(left_right_taps_execute_on_release_and_repeat_does_not_rearm); + let (mut state, _events) = fixture(SOURCE); + for code in [133, 134] { + key(&mut state, code, true); + assert!(!tapped(&state)); + key(&mut state, code, true); // backend repeat, not another isolated tap + assert!(!tapped(&state)); + key(&mut state, code, false); + assert!(tapped(&state)); + state.set_binding_mode("default"); + key(&mut state, code, false); // duplicate release + assert!(!tapped(&state)); + assert!(state.seat.get_keyboard().unwrap().pressed_keys().is_empty()); + assert!( + state.intercepted_keys.is_empty(), + "tap must not own either half of Super" + ); + } +} + +#[test] +fn existing_chords_held_keys_and_both_super_keys_never_become_taps() { + private!(existing_chords_held_keys_and_both_super_keys_never_become_taps); + let (mut state, _events) = fixture(SOURCE); + for sequence in [ + vec![(133, true), (38, true), (38, false), (133, false)], + vec![(38, true), (133, true), (38, false), (133, false)], + vec![(133, true), (134, true), (133, false), (134, false)], + vec![(134, true), (133, true), (134, false), (133, false)], + vec![(37, true), (133, true), (37, false), (133, false)], + vec![(133, true), (50, true), (50, false), (133, false)], + ] { + for (code, down) in sequence { + key(&mut state, code, down); + } + assert!(!tapped(&state)); + assert!(state.seat.get_keyboard().unwrap().pressed_keys().is_empty()); + } + key(&mut state, 133, true); + tap(&mut state, 65); // existing Super+Space, action is deliberately distinct + assert_eq!(state.binding_modes.active(), Some("chord")); + key(&mut state, 133, false); + assert_eq!(state.binding_modes.active(), Some("chord")); +} + +#[derive(Debug)] +struct PointerInput; +impl InputBackend for PointerInput { + type Device = WinitVirtualDevice; + type KeyboardKeyEvent = UnusedEvent; + type PointerButtonEvent = Button; + type PointerMotionEvent = Motion; + type PointerAxisEvent = Scroll; + type PointerMotionAbsoluteEvent = UnusedEvent; + type GestureSwipeBeginEvent = UnusedEvent; + type GestureSwipeUpdateEvent = UnusedEvent; + type GestureSwipeEndEvent = UnusedEvent; + type GesturePinchBeginEvent = UnusedEvent; + type GesturePinchUpdateEvent = UnusedEvent; + type GesturePinchEndEvent = UnusedEvent; + type GestureHoldBeginEvent = UnusedEvent; + type GestureHoldEndEvent = UnusedEvent; + type TouchDownEvent = UnusedEvent; + type TouchUpEvent = UnusedEvent; + type TouchMotionEvent = UnusedEvent; + type TouchCancelEvent = UnusedEvent; + type TouchFrameEvent = UnusedEvent; + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; + type SwitchToggleEvent = UnusedEvent; + type SpecialEvent = UnusedEvent; +} +#[derive(Debug)] +struct Button(ButtonState); +#[derive(Debug)] +struct Motion; +#[derive(Debug)] +struct Scroll; +macro_rules! pointer_event { + ($event:ty) => { + impl Event for $event { + fn time(&self) -> InputTime { + InputTime::from_millis(1) + } + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } + } + }; +} +pointer_event!(Button); +pointer_event!(Motion); +pointer_event!(Scroll); +impl PointerButtonEvent for Button { + fn button_code(&self) -> u32 { + 0x111 + } + fn state(&self) -> ButtonState { + self.0 + } +} +impl PointerMotionEvent for Motion { + fn delta_x(&self) -> f64 { + 1. + } + fn delta_y(&self) -> f64 { + 0. + } + fn delta_x_unaccel(&self) -> f64 { + 1. + } + fn delta_y_unaccel(&self) -> f64 { + 0. + } +} +impl PointerAxisEvent for Scroll { + fn amount(&self, axis: Axis) -> Option { + (axis == Axis::Vertical).then_some(15.) + } + fn amount_v120(&self, axis: Axis) -> Option { + (axis == Axis::Vertical).then_some(120.) + } + fn source(&self) -> AxisSource { + AxisSource::Wheel + } + fn relative_direction(&self, _: Axis) -> AxisRelativeDirection { + AxisRelativeDirection::Identical + } +} +fn button(state: &mut Ferese, down: bool) { + state.process_input_event::(InputEvent::PointerButton { + event: Button(if down { + ButtonState::Pressed + } else { + ButtonState::Released + }), + }); +} +#[test] +fn mouse_motion_buttons_scroll_and_already_held_button_cancel_taps() { + private!(mouse_motion_buttons_scroll_and_already_held_button_cancel_taps); + let (mut state, _events) = fixture(SOURCE); + key(&mut state, 133, true); + button(&mut state, true); + button(&mut state, false); + key(&mut state, 133, false); + assert!(!tapped(&state)); + button(&mut state, true); + tap(&mut state, 133); + assert!(!tapped(&state)); + button(&mut state, false); + key(&mut state, 133, true); + state.process_input_event::(InputEvent::PointerMotion { event: Motion }); + key(&mut state, 133, false); + assert!(!tapped(&state)); + key(&mut state, 133, true); + state.process_input_event::(InputEvent::PointerAxis { event: Scroll }); + key(&mut state, 133, false); + assert!(!tapped(&state)); + tap(&mut state, 133); + assert!( + tapped(&state), + "fresh isolated tap must recover after mouse use" + ); +} + +#[test] +fn reload_focus_loss_and_second_source_do_not_resurrect_pending_taps() { + private!(reload_focus_loss_and_second_source_do_not_resurrect_pending_taps); + let (mut state, _events) = fixture(SOURCE); + key(&mut state, 133, true); + state.reload_config_source(SOURCE.into()).unwrap(); + // An unchanged config need not replace bindings; an actual input/binding + // edit must invalidate the press even if its action stays the same. + state + .reload_config_source(format!("{SOURCE}\nbinding b none\n")) + .unwrap(); + key(&mut state, 133, false); + assert!(!tapped(&state)); + key(&mut state, 133, true); + state.release_nested_keyboard(); + key(&mut state, 133, false); + assert!(!tapped(&state)); + let auxiliary = KeyboardSource::new_auxiliary(); + state + .super_tap + .arm(KeyboardSource::MAIN, Keycode::new(133), None); + state.super_tap.observe_key(auxiliary, Keycode::new(133)); + assert!( + !state + .super_tap + .release(KeyboardSource::MAIN, Keycode::new(133), true, None) + ); + tap(&mut state, 134); + assert!(tapped(&state)); +} + +struct ClientGrab(GrabStartData); +impl KeyboardGrab for ClientGrab { + fn input( + &mut self, + data: &mut Ferese, + handle: &mut KeyboardInnerHandle<'_, Ferese>, + key: Keycode, + state: KeyState, + mods: Option, + serial: Serial, + time: InputTime, + ) { + handle.input(data, key, state, mods, serial, time); + } + fn set_focus( + &mut self, + data: &mut Ferese, + handle: &mut KeyboardInnerHandle<'_, Ferese>, + focus: Option, + serial: Serial, + ) { + handle.set_focus(data, focus, serial); + } + fn start_data(&self) -> &GrabStartData { + &self.0 + } + fn unset(&mut self, _: &mut Ferese) {} +} +#[test] +fn native_ime_grab_allows_tap_but_client_grab_keeps_keyboard() { + private!(native_ime_grab_allows_tap_but_client_grab_keeps_keyboard); + let (mut state, mut _events) = fixture(SOURCE); + let keyboard = state.seat.get_keyboard().unwrap(); + // Use a real input-method grab and its own virtual keyboard, as Rime + // does, rather than an empty grab whose protocol resources do not exist. + use smithay::reexports::wayland_protocols_misc::zwp_input_method_v2::server::{ + zwp_input_method_manager_v2::ZwpInputMethodManagerV2, + zwp_input_method_v2::ZwpInputMethodV2, + }; + use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1; + use smithay::wayland::input_method::InputMethodSeat; + use std::os::fd::AsRawFd; + let (server, mut wire) = UnixStream::pair().unwrap(); + let client = state + .display_handle + .insert_client(server, Arc::new(crate::state::ClientState::default())) + .unwrap(); + let request = crate::state::test_client_request; + request(&mut wire, 1, 1, &[2], None); // display.get_registry + crate::state::dispatch_test_client(&mut _events, &mut state); + wire.set_nonblocking(true).unwrap(); + let mut bytes = Vec::new(); + let _ = wire.read_to_end(&mut bytes); + wire.set_nonblocking(false).unwrap(); + let mut offset = 0; + let mut seat_global = None; + while offset < bytes.len() { + let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); + if object == 2 && header as u16 == 0 { + let length = + u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize; + if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" { + seat_global = Some(u32::from_ne_bytes( + bytes[offset + 8..offset + 12].try_into().unwrap(), + )); + } + } + offset += (header >> 16) as usize; + } + let mut bind = vec![seat_global.unwrap(), 8]; + bind.extend( + b"wl_seat\0" + .chunks_exact(4) + .map(|word| u32::from_ne_bytes(word.try_into().unwrap())), + ); + bind.extend([7, 3]); + request(&mut wire, 2, 0, &bind, None); + crate::state::dispatch_test_client(&mut _events, &mut state); + let manager = client + .create_resource::( + &state.display_handle, + 1, + smithay::wayland::GlobalData, + ) + .unwrap(); + let vk_manager = client + .create_resource::( + &state.display_handle, + 1, + smithay::wayland::GlobalData, + ) + .unwrap(); + request(&mut wire, manager.id().protocol_id(), 0, &[3, 4], None); + crate::state::dispatch_test_client(&mut _events, &mut state); + let grab_opcode = ZwpInputMethodV2::interface() + .requests + .iter() + .position(|request| request.name == "grab_keyboard") + .unwrap() as u32; + request(&mut wire, 4, grab_opcode, &[5], None); + request(&mut wire, vk_manager.id().protocol_id(), 0, &[3, 6], None); + let fd = tempfile::tempfile().unwrap(); + request(&mut wire, 6, 0, &[0, 0], Some(fd.as_raw_fd())); // no_keymap, retain seat mapping + crate::state::dispatch_test_client(&mut _events, &mut state); + assert_eq!( + state.seat.input_method().keyboard_grab_client(), + Some(client) + ); + key(&mut state, 133, true); + assert!(state.super_tap.candidate.is_some()); + request(&mut wire, 6, 1, &[1, 125, 1], None); // trusted Rime echo of physical Super press + crate::state::dispatch_test_client(&mut _events, &mut state); + assert!( + state.super_tap.candidate.is_some(), + "trusted IME echo must bypass auxiliary tap cancellation" + ); + assert!(!tapped(&state)); + key(&mut state, 133, false); + assert!(tapped(&state)); + request(&mut wire, 6, 1, &[2, 125, 0], None); // trusted release echo after the physical tap + crate::state::dispatch_test_client(&mut _events, &mut state); + assert!(tapped(&state)); + assert!(keyboard.pressed_keys().is_empty()); + state.set_binding_mode("default"); + keyboard.unset_grab(&mut state); + keyboard.set_grab( + &mut state, + ClientGrab(GrabStartData { focus: None }), + SERIAL_COUNTER.next_serial(), + ); + tap(&mut state, 133); + assert!(!tapped(&state)); + keyboard.unset_grab(&mut state); + tap(&mut state, 133); + assert!(tapped(&state)); +} + +fn launcher_events(state: &mut Ferese, wire: &mut UnixStream, shell: &FereseShellV1) -> usize { + state.display_handle.flush_clients().unwrap(); + wire.set_nonblocking(true).unwrap(); + let mut bytes = Vec::new(); + let _ = wire.read_to_end(&mut bytes); + let opcode = FereseShellV1::interface() + .events + .iter() + .position(|e| e.name == "toggle_launcher") + .unwrap() as u16; + let (mut offset, mut count) = (0, 0); + while offset < bytes.len() { + let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); + if object == shell.id().protocol_id() && header as u16 == opcode { + count += 1; + } + offset += (header >> 16) as usize; + } + count +} +#[test] +fn actual_launcher_protocol_is_sent_once_after_release_and_space_still_works() { + private!(actual_launcher_protocol_is_sent_once_after_release_and_space_still_works); + let (mut state, _events) = fixture("binding Super toggle-launcher\n"); + let (server, mut wire) = UnixStream::pair().unwrap(); + let client = state + .display_handle + .insert_client(server, Arc::new(crate::state::ClientState::default())) + .unwrap(); + let shell = client + .create_resource::( + &state.display_handle, + 10, + Default::default(), + ) + .unwrap(); + state.shell_resources.push(shell.downgrade()); + for code in [133, 134] { + key(&mut state, code, true); + assert_eq!(launcher_events(&mut state, &mut wire, &shell), 0); + key(&mut state, code, false); + assert_eq!(launcher_events(&mut state, &mut wire, &shell), 1); + } + key(&mut state, 133, true); + tap(&mut state, 65); + key(&mut state, 133, false); + assert_eq!( + launcher_events(&mut state, &mut wire, &shell), + 1, + "Space chord must not also produce a tap" + ); +} + +#[test] +fn lost_pointer_devices_and_session_reset_do_not_leave_taps_disabled() { + let mut tap = SuperTap::default(); + tap.pointer_button("mouse-a".into(), 0x110, ButtonState::Pressed); + tap.pointer_button("mouse-b".into(), 0x110, ButtonState::Pressed); + tap.device_removed("mouse-a"); + assert_eq!(tap.pointer_buttons.len(), 1); + tap.device_removed("mouse-b"); + assert!(tap.pointer_buttons.is_empty()); + tap.pointer_button("mouse-a".into(), 0x111, ButtonState::Pressed); + tap.arm(KeyboardSource::MAIN, Keycode::new(133), None); + tap.reset(); + assert!(tap.pointer_buttons.is_empty()); + assert!(tap.candidate.is_none()); +} diff --git a/compositor/src/output_transaction.rs b/compositor/src/output_transaction.rs index ae626ee..603d4ea 100644 --- a/compositor/src/output_transaction.rs +++ b/compositor/src/output_transaction.rs @@ -17,30 +17,39 @@ pub(crate) struct Failure { /// Caller must validate the *complete* device plan first. No logical publication /// is permitted until success. Unavailable rollback results must be retired. +/// `releases` includes removed outputs and old connector owners whose CRTC is +/// reused by a change. Release those resources before per-surface staging tests. pub(crate) fn apply( driver: &mut T, changes: &[T::Key], - removals: &[T::Key], + releases: &[T::Key], ) -> Result<(), Failure> { - let mut touched = Vec::new(); + let mut touched_changes = Vec::new(); + let mut touched_releases = Vec::new(); let result = (|| { + for key in releases { + touched_releases.push(*key); + driver.disable(*key)?; + } for key in changes { - touched.push(*key); + touched_changes.push(*key); driver.stage(*key)?; } for key in changes { driver.commit(*key)?; } - for key in removals { - touched.push(*key); - driver.disable(*key)?; - } Ok::<_, String>(()) })(); if let Err(error) = result { driver.abort_staged(); - let unavailable = touched + // Drop new surfaces first, then restore surviving changed outputs before + // re-enabling former owners. Restoring a released CRTC too early can + // exceed the very resource budget that required its release. + let unavailable = touched_changes .into_iter() + .rev() + .filter(|key| !touched_releases.contains(key)) + .chain(touched_releases.iter().rev().copied()) .filter(|key| driver.restore(*key).is_err()) .collect(); return Err(Failure { error, unavailable }); @@ -48,6 +57,149 @@ pub(crate) fn apply( Ok(()) } +#[cfg(test)] +mod resource_tests { + use super::*; + use std::collections::BTreeMap; + + /// Per-CRTC staging probes see the current hardware plus that one proposed + /// mode. The final plan fits, but an old HDMI route consumes the budget that + /// a surviving DP output's higher refresh needs. CRTC 589 is then reused by + /// another connector, so it appears in both changes and pre-releases. + struct LimitedHardware { + active: BTreeMap, + previous: BTreeMap, + desired: BTreeMap, + events: Vec<(&'static str, u16)>, + fail: Option<(&'static str, u16)>, + restore_fails: Option, + } + + impl LimitedHardware { + fn new() -> Self { + let previous = BTreeMap::from([(205, 0), (397, 1), (589, 2)]); + Self { + active: previous.clone(), + previous, + desired: BTreeMap::from([(205, 0), (397, 2), (589, 1)]), + events: vec![], + fail: None, + restore_fails: None, + } + } + + fn fits(&self, key: u16, cost: u8) -> Result<(), String> { + let mut proposed = self.active.clone(); + proposed.insert(key, cost); + if proposed.values().copied().sum::() > 3 { + Err("intermediate topology exceeds device resources".into()) + } else { + Ok(()) + } + } + + fn event(&mut self, operation: &'static str, key: u16) -> Result<(), String> { + self.events.push((operation, key)); + if self.fail == Some((operation, key)) { + Err(format!("injected {operation} failure on {key}")) + } else { + Ok(()) + } + } + } + + impl HardwareTransaction for LimitedHardware { + type Key = u16; + + fn stage(&mut self, key: u16) -> Result<(), String> { + self.fits(key, self.desired[&key])?; + self.event("stage", key) + } + + fn commit(&mut self, key: u16) -> Result<(), String> { + self.fits(key, self.desired[&key])?; + self.active.insert(key, self.desired[&key]); + // A failed operation can have already changed hardware. + self.event("commit", key) + } + + fn disable(&mut self, key: u16) -> Result<(), String> { + self.active.remove(&key); + self.event("disable", key) + } + + fn abort_staged(&mut self) { + self.events.push(("abort", 0)); + self.active.retain(|key, _| self.previous.contains_key(key)); + } + + fn restore(&mut self, key: u16) -> Result<(), String> { + self.events.push(("restore", key)); + if self.restore_fails == Some(key) { + self.active.remove(&key); + return Err("previous output unavailable".into()); + } + if let Some(cost) = self.previous.get(&key).copied() { + self.fits(key, cost)?; + self.active.insert(key, cost); + } else { + self.active.remove(&key); + } + Ok(()) + } + } + + #[test] + fn reused_crtc_releases_its_old_connector_before_other_modes_are_tested() { + let mut hardware = LimitedHardware::new(); + assert_eq!(hardware.desired.values().copied().sum::(), 3); + apply(&mut hardware, &[397, 589], &[589]).unwrap(); + assert_eq!(hardware.active, hardware.desired); + assert_eq!(hardware.events[0], ("disable", 589)); + assert!(hardware.events.iter().all(|(_, key)| *key != 205)); + } + + #[test] + fn rollback_reverts_new_modes_before_restoring_released_connector_owners() { + for failure in [("stage", 589), ("commit", 397), ("commit", 589)] { + let mut hardware = LimitedHardware::new(); + hardware.fail = Some(failure); + let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err(); + assert!(result.error.starts_with("injected"), "{}", result.error); + assert!(result.unavailable.is_empty(), "{result:?}"); + assert_eq!(hardware.active, hardware.previous); + assert_eq!( + &hardware.events[hardware.events.len() - 3..], + &[("abort", 0), ("restore", 397), ("restore", 589)] + ); + } + } + + #[test] + fn partially_failed_release_restores_the_old_output_without_staging_changes() { + let mut hardware = LimitedHardware::new(); + hardware.fail = Some(("disable", 589)); + let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err(); + assert_eq!(result.error, "injected disable failure on 589"); + assert!(result.unavailable.is_empty()); + assert_eq!(hardware.active, hardware.previous); + assert_eq!( + hardware.events, + [("disable", 589), ("abort", 0), ("restore", 589)] + ); + } + + #[test] + fn failed_rollback_reports_lost_output_and_still_restores_the_released_owner() { + let mut hardware = LimitedHardware::new(); + hardware.fail = Some(("commit", 589)); + hardware.restore_fails = Some(397); + let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err(); + assert_eq!(result.unavailable, [397]); + assert_eq!(hardware.active, BTreeMap::from([(205, 0), (589, 2)])); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/compositor/src/render/backdrop_gpu_tests.rs b/compositor/src/render/backdrop_gpu_tests.rs new file mode 100644 index 0000000..d20ae40 --- /dev/null +++ b/compositor/src/render/backdrop_gpu_tests.rs @@ -0,0 +1,182 @@ +//! Compare damaged swapchain frames with an independently repainted scene. +use super::*; +use smithay::backend::renderer::damage::OutputDamageTracker; +use smithay::backend::renderer::element::solid::SolidColorBuffer; +use smithay::utils::user_data::UserDataMap; + +fn blur( + renderer: &mut GlesRenderer, + program: &BlurProgram, + scale: f64, + size: Size, + transform: Transform, +) -> BlurRenderElement { + let sample = Rectangle::::new((0, 10).into(), (145, 80).into()); + let visible = Rectangle::::new((10, 20).into(), (120, 55).into()); + let sample_physical = sample.to_physical_precise_round(scale); + let framebuffer = framebuffer_clip_rect(sample_physical, size, transform.invert()); + let capture = framebuffer_capture_rect(framebuffer); + let texture = renderer + .create_buffer(Fourcc::Abgr8888, (capture[2], capture[3]).into()) + .unwrap(); + BlurRenderElement::new( + texture, + program.0.clone(), + &MaterialParameters { + panel: None, + corner_radii: [0.; 4], + presentation_alpha: 1., + background_opacity: 0., + blur: (8. * scale) as f32, + sample_geometry: sample, + sample_physical, + sample_framebuffer: framebuffer, + shadow_rect: [0.; 4], + shadow_values: [0.; 2], + shadow_bounds: visible, + geometry: visible, + tint: [0.; 4], + generation: 0, + opaque: false, + visible_framebuffer: framebuffer_clip_rect( + visible.to_physical_precise_round(scale), + size, + transform.invert(), + ), + }, + Arc::new(AtomicBool::new(true)), + ) +} + +fn pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec { + let size = texture.size(); + let target = renderer.bind(texture).unwrap(); + let mapping = renderer + .copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888) + .unwrap(); + renderer.map_texture(&mapping).unwrap().to_vec() +} + +#[test] +#[ignore = "requires an EGL rendering device"] +fn blur_behind_moving_opaque_window_matches_full_frames_at_buffer_age_three() { + let mut renderer = { + use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; + let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some(); + let device = EGLDevice::enumerate() + .unwrap() + .find(|device| device.is_software() == software) + .unwrap(); + // SAFETY: The enumerated device remains owned by the EGL display/context. + let display = unsafe { EGLDisplay::new(device).unwrap() }; + // SAFETY: This test owns the renderer's fresh context exclusively. + unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() } + }; + let mut resources = RenderResources::default(); + let program = blur_program(&mut resources, &mut renderer).unwrap(); + for scale in [1., 1.5, 2.] { + for transform in [Transform::Normal, Transform::Flipped180] { + let size: Size = ((160. * scale) as i32, (100. * scale) as i32).into(); + let mut tracker = OutputDamageTracker::new(size, scale, transform); + let mut targets = (0..3) + .map(|_| { + renderer + .create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()) + .unwrap() + }) + .collect::>(); + let mut reference: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()) + .unwrap(); + let effect = blur(&mut renderer, &program, scale, size, transform); + let mut background = SolidColorBuffer::new((160, 100), [0.1, 0.7, 0.2, 1.]); + let foreground = SolidColorBuffer::new((60, 90), [0.2, 0.1, 0.8, 1.]); + for frame_index in 0..9 { + if frame_index % 3 == 0 { + background.set_color([0.1, 0.3 + frame_index as f32 * 0.05, 0.2, 1.]); + } + let solid = || { + SolidColorRenderElement::from_buffer( + &background, + (0, 0), + scale, + 1., + RenderElementKind::Unspecified, + ) + }; + let front = SolidColorRenderElement::from_buffer( + &foreground, + (((100 - frame_index * 3) as f64 * scale) as i32, 0), + scale, + 1., + RenderElementKind::Unspecified, + ); + let elements: Vec = + vec![front.clone().into(), effect.clone().into(), solid().into()]; + backdrop::update(&elements, scale); + let slot = frame_index as usize % 3; + { + let mut target = renderer.bind(&mut targets[slot]).unwrap(); + tracker + .render_output( + &mut renderer, + &mut target, + if frame_index < 3 { 0 } else { 3 }, + &elements, + Color32F::TRANSPARENT, + ) + .unwrap() + .sync + .wait() + .unwrap(); + } + + // Paint every layer without opaque culling or damage reuse. + // A separate capture texture prevents the oracle from repairing + // the incremental renderer's cached copy as a side effect. + let reference_blur = blur(&mut renderer, &program, scale, size, transform); + let full_elements: Vec = + vec![front.into(), reference_blur.into(), solid().into()]; + { + let mut target = renderer.bind(&mut reference).unwrap(); + let mut frame = renderer + .render(&mut target, size, transform.invert()) + .unwrap(); + let full = Rectangle::from_size(size); + frame.clear(Color32F::TRANSPARENT, &[full]).unwrap(); + for element in full_elements.iter().rev() { + let cache = UserDataMap::default(); + let rect = element.geometry(scale.into()); + if element.is_framebuffer_effect() { + element + .capture_framebuffer(&mut frame, element.src(), rect, &cache) + .unwrap(); + } + element + .draw( + &mut frame, + element.src(), + rect, + &[Rectangle::from_size(rect.size)], + &[], + element.is_framebuffer_effect().then_some(&cache), + ) + .unwrap(); + } + frame.finish().unwrap().wait().unwrap(); + } + let actual = pixels(&mut renderer, &mut targets[slot]); + let expected = pixels(&mut renderer, &mut reference); + let differing = actual + .iter() + .zip(&expected) + .filter(|(a, b)| a.abs_diff(**b) > 1) + .count(); + assert_eq!( + differing, 0, + "scale={scale} transform={transform:?} frame={frame_index}" + ); + } + } + } +} diff --git a/compositor/src/render/capture.rs b/compositor/src/render/capture.rs index 88c2aee..2b4a790 100644 --- a/compositor/src/render/capture.rs +++ b/compositor/src/render/capture.rs @@ -18,7 +18,7 @@ pub(crate) fn capture_resize_snapshot( ) -> Result, GlesError> { // Called before on_commit_buffer_handler: renderer surface state still // owns the previous buffer during an actual resize handoff. - render_window_texture(renderer, window, geometry, scale, remaining, None) + render_window_texture(renderer, window, geometry, scale, remaining, None, true) } fn render_window_texture( @@ -28,6 +28,7 @@ fn render_window_texture( scale: f64, remaining: usize, reusable: Option, + before_source_release: bool, ) -> Result, GlesError> { let Some(toplevel) = window.toplevel() else { return Ok(None); @@ -68,7 +69,13 @@ fn render_window_texture( let damage = Rectangle::from_size(size); frame.clear(Color32F::new(0.0, 0.0, 0.0, 0.0), &[damage])?; draw_render_elements(&mut frame, scale, &content, &[damage])?; - let _ = frame.finish()?; + let copied = frame.finish()?; + if before_source_release { + // This function runs before the old wl_buffer is replaced. The + // client may immediately reuse it after release, so the detached + // animation image must be complete before that release is possible. + copied.wait().map_err(|_| GlesError::SyncInterrupted)?; + } } Ok(Some(ResizeSnapshot { texture, @@ -118,6 +125,7 @@ pub(crate) fn capture_window_frame( scale, 128 * 1024 * 1024, reusable, + false, ) .map_err(|error| error.to_string())? .ok_or("Window has no capturable content")?; @@ -422,6 +430,7 @@ pub(crate) fn capture_stream_window( info.scale, 128 * 1024 * 1024, reusable, + false, ) .map_err(|error| error.to_string())? .ok_or("Window has no capturable content")?; diff --git a/compositor/src/render/layer_popup_tests.rs b/compositor/src/render/layer_popup_tests.rs new file mode 100644 index 0000000..86c7b99 --- /dev/null +++ b/compositor/src/render/layer_popup_tests.rs @@ -0,0 +1,138 @@ +//! Real input-method protocol resources and SHM bounds; no host GUI is used. +use super::layer_popup_geometry; +use crate::Ferese; +use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window}; +use smithay::desktop::PopupKind; +use smithay::reexports::calloop::EventLoop; +use smithay::reexports::wayland_protocols_misc::zwp_input_method_v2::server::zwp_input_method_manager_v2::ZwpInputMethodManagerV2; +use smithay::reexports::wayland_server::protocol::{ + wl_compositor::WlCompositor, wl_shm::WlShm, wl_surface::WlSurface, +}; +use smithay::reexports::wayland_server::Resource; +use smithay::utils::{Physical, Point, Rectangle}; +use smithay::wayland::input_method::PopupParent; +use smithay::wayland::text_input::TextInputSeat; +use std::io::{Read, Write}; +use std::os::fd::AsRawFd; +use std::os::unix::net::UnixStream; + +fn bind_seat(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream) { + request(wire, 1, 1, &[7], None); + dispatch(events, state); + wire.set_nonblocking(true).unwrap(); + let mut bytes = Vec::new(); + let _ = wire.read_to_end(&mut bytes); + wire.set_nonblocking(false).unwrap(); + let mut offset = 0; + let mut seat = None; + while offset < bytes.len() { + let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); + if object == 7 && header as u16 == 0 { + let length = + u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize; + if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" { + seat = Some(u32::from_ne_bytes( + bytes[offset + 8..offset + 12].try_into().unwrap(), + )); + } + } + offset += (header >> 16) as usize; + } + let mut args = vec![seat.unwrap(), 8]; + args.extend( + b"wl_seat\0" + .chunks_exact(4) + .map(|word| u32::from_ne_bytes(word.try_into().unwrap())), + ); + args.extend([7, 8]); + request(wire, 7, 0, &args, None); + dispatch(events, state); +} + +#[test] +fn input_method_popup_preserves_layer_origin_and_uses_own_surface_bounds() { + if !crate::startup_tests::private_runtime( + "render::scene::layer_popup_tests::input_method_popup_preserves_layer_origin_and_uses_own_surface_bounds", + ) { + return; + } + let mut events = EventLoop::try_new().unwrap(); + let mut state = crate::startup_tests::state(&mut events); + let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030); + let parent = window.toplevel().unwrap().wl_surface().clone(); + let client = parent.client().unwrap(); + state.seat.text_input().set_focus(Some(parent.clone())); + bind_seat(&mut state, &mut events, &mut wire); + let manager = client + .create_resource::( + &state.display_handle, + 1, + smithay::wayland::GlobalData, + ) + .unwrap(); + let compositor = client + .create_resource::( + &state.display_handle, + 6, + smithay::wayland::GlobalData, + ) + .unwrap(); + let shm = client + .create_resource::( + &state.display_handle, + 1, + smithay::wayland::GlobalData, + ) + .unwrap(); + request(&mut wire, manager.id().protocol_id(), 0, &[8, 9], None); + request(&mut wire, compositor.id().protocol_id(), 0, &[10], None); + request(&mut wire, 9, 4, &[11, 10], None); // get_input_popup_surface + let mut file = tempfile::tempfile().unwrap(); + file.write_all(&0xff204060u32.to_ne_bytes().repeat(364 * 80)) + .unwrap(); + request( + &mut wire, + shm.id().protocol_id(), + 0, + &[12, 364 * 80 * 4], + Some(file.as_raw_fd()), + ); + request(&mut wire, 12, 0, &[13, 0, 364, 80, 364 * 4, 0], None); + request(&mut wire, 10, 1, &[13, 0, 0], None); + request(&mut wire, 10, 6, &[], None); + dispatch(&mut events, &mut state); + let surface = client + .object_from_protocol_id::(&state.display_handle, 10) + .unwrap(); + let Some(PopupKind::InputMethod(mut popup)) = state.popups.find_popup(&surface) else { + panic!("input-method popup must be tracked"); + }; + + // Parent geometry is global, but the layer renderer receives output-local + // coordinates. Cover a primary output and a secondary output left/above it. + for output_origin in [(0, 0), (-1920, -300)] { + let layer_origin = Point::from((360, 241)); + let global_parent = + Rectangle::new(layer_origin + Point::from(output_origin), (560, 70).into()); + popup.set_parent(Some(PopupParent { + surface: parent.clone(), + location: global_parent, + })); + popup.set_location((148, 55).into()); + let kind = PopupKind::InputMethod(popup.clone()); + assert_eq!(kind.geometry(), global_parent); + let (bounds, content_origin) = layer_popup_geometry(layer_origin, popup.location(), &kind); + assert_eq!(content_origin, Point::from((508, 296))); + assert_eq!( + bounds, + Rectangle::new((508, 296).into(), (364, 80).into()), + "candidate bounds must come from its own buffer, not its parent" + ); + for scale in [1.0, 1.5, 2.0] { + let pixels: Rectangle = bounds.to_physical_precise_round(scale); + assert_eq!(pixels.loc, content_origin.to_physical_precise_round(scale)); + assert_eq!(pixels.size.h, (80.0 * scale) as i32); + } + } +} diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index 9d3de05..132cf17 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -1,4 +1,6 @@ mod backdrop; +#[cfg(test)] +mod backdrop_gpu_tests; mod capture; mod closing; mod window_blur; @@ -272,9 +274,25 @@ impl Element for BlurRenderElement { fn kind(&self) -> RenderElementKind { RenderElementKind::Unspecified } + + fn is_framebuffer_effect(&self) -> bool { + // The complete sampling region must be repainted before capture, + // including pixels hidden by opaque elements above this blur. + true + } } impl RenderElement for BlurRenderElement { + fn capture_framebuffer( + &self, + frame: &mut GlesFrame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _cache: &smithay::utils::user_data::UserDataMap, + ) -> Result<(), GlesError> { + self.capture(frame) + } + fn draw( &self, frame: &mut GlesFrame<'_, '_>, @@ -282,14 +300,37 @@ impl RenderElement for BlurRenderElement { dst: Rectangle, damage: &[Rectangle], opaque_regions: &[Rectangle], - _cache: Option<&smithay::utils::user_data::UserDataMap>, + cache: Option<&smithay::utils::user_data::UserDataMap>, ) -> Result<(), GlesError> { + // Simple full-frame renderers do not implement the effect callback. + // Damage-tracked outputs must use its callback, never the dirty flag: + // the tracker knows when occlusion or buffer restoration needs a copy. + if cache.is_none() && self.capture_dirty.load(Ordering::Relaxed) { + self.capture(frame)?; + } + frame.render_texture_from_to( + &self.texture, + src, + dst, + damage, + opaque_regions, + Transform::Normal, + 1.0, + Some(&self.program), + &self.uniforms, + ) + } + + fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option> { + None + } +} + +impl BlurRenderElement { + fn capture(&self, frame: &mut GlesFrame<'_, '_>) -> Result<(), GlesError> { let texture = self.texture.tex_id(); let [x, y, width, height] = self.capture_rect; - // Buffer-age restoration and unrelated partial damage must reuse the - // clean captured scene. Recapturing a partly repainted framebuffer - // would feed previous blurred pixels back into the blur. - if self.capture_dirty.load(Ordering::Relaxed) { + { // SAFETY: with_context makes this frame context current. GL writes into live local // scalars, and the capture texture belongs to this renderer with the allocated // framebuffer bounds. @@ -341,22 +382,7 @@ impl RenderElement for BlurRenderElement { } self.capture_dirty.store(false, Ordering::Relaxed); } - - frame.render_texture_from_to( - &self.texture, - src, - dst, - damage, - opaque_regions, - Transform::Normal, - 1.0, - Some(&self.program), - &self.uniforms, - ) - } - - fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option> { - None + Ok(()) } } @@ -538,6 +564,47 @@ fn clipped_surface_damage( } } +fn sampled_surface_damage( + size: Size, + source: Size, + transform: Transform, + clip_changed: bool, + client_damage: impl FnOnce() -> DamageSet, +) -> DamageSet { + clipped_surface_damage(size, clip_changed, || { + // Linear filtering also changes destination samples just outside the + // changed texels. Smithay maps their rectangle to physical coordinates, + // but does not include this half-texel sampling footprint. Expanding in + // physical pixels prevents stale fringes on fractional-scale redraws. + let source = transform.transform_size(source); + let footprint = |pixels: i32, texels: f64| { + if texels > 0.0 { + (f64::from(pixels) / texels * 0.5).ceil() as i32 + } else { + 0 + } + }; + let x = footprint(size.w, source.w); + let y = footprint(size.h, source.h); + let bounds = Rectangle::from_size(size); + client_damage() + .into_iter() + .filter(|rect| !rect.is_empty()) + .filter_map(|rect| { + Rectangle::new( + (rect.loc.x.saturating_sub(x), rect.loc.y.saturating_sub(y)).into(), + ( + rect.size.w.saturating_add(x.saturating_mul(2)), + rect.size.h.saturating_add(y.saturating_mul(2)), + ) + .into(), + ) + .intersection(bounds) + }) + .collect() + }) +} + impl Element for RoundedSurfaceRenderElement { fn id(&self) -> &Id { self.inner.id() @@ -564,9 +631,13 @@ impl Element for RoundedSurfaceRenderElement { scale: RenderScale, commit: Option, ) -> DamageSet { - clipped_surface_damage(self.inner.geometry(scale).size, self.clip_changed, || { - self.inner.damage_since(scale, commit) - }) + sampled_surface_damage( + self.inner.geometry(scale).size, + self.inner.src().size, + self.inner.transform(), + self.clip_changed, + || self.inner.damage_since(scale, commit), + ) } fn opaque_regions(&self, scale: RenderScale) -> OpaqueRegions { @@ -688,6 +759,39 @@ mod tests { shadow_bounds, }; + #[test] + fn filtered_surface_damage_stays_local_and_preserves_full_clip_changes() { + use smithay::backend::renderer::utils::DamageSet; + let size = (120, 80).into(); + let source = (80.0, 40.0).into(); + for (transform, x_padding) in [(Transform::Normal, 1), (Transform::_90, 2)] { + let damage = super::sampled_surface_damage(size, source, transform, false, || { + DamageSet::from_slice(&[ + Rectangle::new((0, 0).into(), (4, 3).into()), + Rectangle::new((118, 78).into(), (2, 2).into()), + ]) + }); + assert_eq!( + damage.iter().copied().collect::>(), + vec![ + Rectangle::new((0, 0).into(), (4 + x_padding, 4).into()), + Rectangle::new((118 - x_padding, 77).into(), (2 + x_padding, 3).into(),), + ], + ); + assert!( + super::sampled_surface_damage(size, source, transform, false, DamageSet::default) + .is_empty() + ); + let full = super::sampled_surface_damage(size, source, transform, true, || { + panic!("a changed clip must keep full damage without consulting the client") + }); + assert_eq!( + full.iter().copied().collect::>(), + vec![Rectangle::from_size(size)] + ); + } + } + #[test] #[ignore = "requires an EGL rendering device"] fn solid_corner_mask_preserves_premultiplied_color() { diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index 14f7cd0..b208432 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -556,9 +556,7 @@ pub(super) fn layer_elements( let mut elements = Vec::new(); for (geometry, layer) in layers { for (popup, offset) in PopupManager::popups_for_surface(layer.wl_surface()) { - let mut bounds = popup.geometry(); - let content_origin = geometry.loc + offset - bounds.loc; - bounds.loc = geometry.loc + offset; + let (bounds, content_origin) = layer_popup_geometry(geometry.loc, offset, &popup); append_material_surface( state, renderer, @@ -587,6 +585,27 @@ pub(super) fn layer_elements( elements } +fn layer_popup_geometry( + layer_origin: Point, + offset: Point, + popup: &smithay::desktop::PopupKind, +) -> (Rectangle, Point) { + if matches!(popup, smithay::desktop::PopupKind::InputMethod(_)) { + // IME geometry describes its parent in global coordinates, unlike + // xdg window geometry. Its location is already parent-relative, and + // its candidate surface can extend beyond the parent's bounds. + let origin = layer_origin + offset; + return ( + smithay::desktop::utils::bbox_from_surface_tree(popup.wl_surface(), origin), + origin, + ); + } + let mut bounds = popup.geometry(); + let origin = layer_origin + offset - bounds.loc; + bounds.loc = layer_origin + offset; + (bounds, origin) +} + fn layer_output_crop(size: Size, transform: Transform) -> Rectangle { Rectangle::from_size(transform.transform_size(size)) } @@ -675,6 +694,10 @@ pub(super) fn cursor_elements( } } +#[cfg(test)] +#[path = "layer_popup_tests.rs"] +mod layer_popup_tests; + #[cfg(test)] mod layer_crop_tests { use super::*; diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index e3a8403..8ec4be2 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -452,6 +452,7 @@ impl SessionLockHandler for Ferese { } fn lock(&mut self, confirmation: SessionLocker) { + self.super_tap.cancel(); self.dismiss_launcher(); if !self.session_lock.lifecycle.can_acquire() { return; diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 1e767f1..10e399d 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -505,6 +505,7 @@ pub struct Ferese { pub(crate) binding_modes: crate::config::BindingModes, pub(crate) binding_mode_pressed: HashSet, pub(crate) binding_mode_repeats: HashMap, + pub(crate) super_tap: crate::input::super_tap::SuperTap, workspace_auto_back_and_forth: bool, pub(crate) window_rules: Vec, pub(crate) theme_settings: ThemeSettings, @@ -847,6 +848,7 @@ impl Ferese { binding_modes: config.binding_modes, binding_mode_pressed: HashSet::new(), binding_mode_repeats: HashMap::new(), + super_tap: Default::default(), workspace_auto_back_and_forth: config.workspace_auto_back_and_forth, window_rules: config.window_rules, theme_settings: config.theme_settings, @@ -1073,6 +1075,7 @@ impl Ferese { self.gap_config = config.gap_config; self.input_settings = config.input_settings; if bindings_changed || input_changed { + self.super_tap.cancel(); self.binding_mode_repeats.clear(); self.bindings = config.bindings.into(); self.binding_modes = config.binding_modes; diff --git a/compositor/src/state/capture_privacy_tests.rs b/compositor/src/state/capture_privacy_tests.rs index a5dd5ee..a048775 100644 --- a/compositor/src/state/capture_privacy_tests.rs +++ b/compositor/src/state/capture_privacy_tests.rs @@ -1,4 +1,9 @@ //! Private protocol clients and offscreen EGL; no host desktop or DRM required. +#[path = "resize_scene_gpu_tests.rs"] +mod resize_scene_gpu_tests; +#[path = "partial_shm_gpu_tests.rs"] +mod partial_shm_gpu_tests; + use std::io::{Read, Write}; use std::os::fd::{AsRawFd, RawFd}; use std::os::unix::net::UnixStream; diff --git a/compositor/src/state/partial_shm_gpu_tests.rs b/compositor/src/state/partial_shm_gpu_tests.rs new file mode 100644 index 0000000..826f427 --- /dev/null +++ b/compositor/src/state/partial_shm_gpu_tests.rs @@ -0,0 +1,461 @@ +//! Streaming SHM updates compared with independent full imports and full repaint. +use super::*; +use std::borrow::Cow; +use std::fs::File; +use std::os::unix::fs::FileExt; + +use smithay::backend::renderer::damage::OutputDamageTracker; +use smithay::backend::renderer::element::{Element, RenderElement}; +use smithay::backend::renderer::{Color32F, Frame, Renderer}; +use smithay::utils::{Buffer, Physical, Size}; + +const WIDTH: u32 = 193; +const HEIGHT: u32 = 137; +const STRIDE: u32 = (WIDTH + 7) * 4; +const OFFSET: u32 = 64; + +struct ReusableShm { + file: File, + buffer: u32, + version: usize, + format: u32, +} + +fn buffer_pixels(pixels: &[u8], format: u32, switching_formats: bool) -> Cow<'_, [u8]> { + if !switching_formats { + return Cow::Borrowed(pixels); + } + let mut encoded = pixels.to_vec(); + for pixel in encoded.chunks_exact_mut(4) { + if format == 1 { + // XRGB's unused high byte may legally be zero. + pixel[3] = 0; + } else { + // A valid premultiplied translucent ARGB surface makes a stale + // opaque shader variant visibly differ in RGB as well as alpha. + for channel in &mut pixel[..3] { + *channel = (u16::from(*channel) * 128 / 255) as u8; + } + pixel[3] = 128; + } + } + Cow::Owned(encoded) +} + +fn write_region(file: &File, pixels: &[u8], rect: Rectangle) { + for y in rect.loc.y..rect.loc.y + rect.size.h { + let source = (y as u32 * WIDTH + rect.loc.x as u32) as usize * 4; + let target = u64::from(OFFSET + y as u32 * STRIDE + rect.loc.x as u32 * 4); + file.write_all_at(&pixels[source..source + rect.size.w as usize * 4], target) + .unwrap(); + } +} + +fn pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec { + let size = smithay::backend::renderer::Texture::size(texture); + let target = renderer.bind(texture).unwrap(); + let mapping = renderer + .copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888) + .unwrap(); + renderer.map_texture(&mapping).unwrap().to_vec() +} + +#[test] +#[ignore = "requires a private Wayland socket and an EGL rendering device"] +fn reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale() { + const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale"; + if !crate::startup_tests::private_runtime(TEST) { + return; + } + run_shm_scenarios(&[(1, None), (3, None)]); +} + +#[test] +#[ignore = "requires a private Wayland socket and an EGL rendering device"] +fn same_sized_argb_xrgb_switches_match_fresh_full_uploads() { + const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::same_sized_argb_xrgb_switches_match_fresh_full_uploads"; + if !crate::startup_tests::private_runtime(TEST) { + return; + } + run_shm_scenarios(&[(2, Some(0)), (2, Some(1))]); +} + +fn run_shm_scenarios(scenarios: &[(usize, Option)]) { + let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some(); + let device = EGLDevice::enumerate() + .unwrap() + .find(|device| device.is_software() == software) + .expect("requested EGL device"); + eprintln!( + "partial SHM EGL software={software}, render node={:?}", + device.render_device_path() + ); + // SAFETY: The display owns the enumerated device; each renderer below owns + // a fresh, unshared context used only by this test thread. + let display = unsafe { EGLDisplay::new(device).unwrap() }; + let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; + let scale = 1.5; + let physical: Size = (480, 360).into(); + let mut mismatching_frames = 0; + + for &(buffer_count, first_format) in scenarios { + let switching_formats = first_format.is_some(); + let mut events = EventLoop::try_new().unwrap(); + let config = crate::config::Config::parse_source( + "animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n", + ) + .unwrap() + .runtime_config() + .unwrap(); + let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap(); + state.animation_test_time = Some(Duration::ZERO); + state.focus_effect.active_opacity = 1.0; + state.focus_effect.inactive_opacity = 1.0; + state.focus_effect.inactive_dim = 0.0; + state.theme_settings.window_radius = 12.0; + let output = Output::new( + "partial-shm".into(), + PhysicalProperties { + serial_number: String::new(), + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + output.change_current_state( + Some(Mode { + size: physical, + refresh: 60_000, + }), + Some(Transform::Normal), + Some(Scale::Fractional(scale)), + None, + ); + state.space.map_output(&output, (0, 0)); + state.register_output(&output, "partial-shm".into()); + let (window, mut wire) = window(&mut state, &mut events, 0xff203c5c); + let id = state.windows.ids()[&window]; + state.focused_window = Some(id); + state.set_floating_window_geometry( + &window, + (56, 64).into(), + (WIDTH as i32, HEIGHT as i32).into(), + ); + state.display_handle.flush_clients().unwrap(); + ack_configure(&mut wire, 3); + let client = window.toplevel().unwrap().wl_surface().client().unwrap(); + let shm = client + .create_resource::( + &state.display_handle, + 1, + smithay::wayland::GlobalData, + ) + .unwrap(); + let compositor = client + .create_resource::( + &state.display_handle, + 6, + smithay::wayland::GlobalData, + ) + .unwrap(); + let mut raster = 0xff203c5cu32 + .to_ne_bytes() + .repeat((WIDTH * HEIGHT) as usize); + let full: Rectangle = + Rectangle::from_size((WIDTH as i32, HEIGHT as i32).into()); + let mut buffers = Vec::new(); + for slot in 0..buffer_count { + let format = first_format.map_or(0, |first| first ^ (slot as u32 % 2)); + let file = tempfile::tempfile().unwrap(); + let len = OFFSET + STRIDE * HEIGHT; + file.set_len(u64::from(len)).unwrap(); + // Nontrivial offset/stride/padding exercise pixel-store state too. + file.write_all_at(&vec![0xa5; len as usize], 0).unwrap(); + write_region( + &file, + &buffer_pixels(&raster, format, switching_formats), + full, + ); + let pool = 7 + slot as u32 * 2; + let buffer = pool + 1; + request( + &mut wire, + shm.id().protocol_id(), + 0, + &[pool, len], + Some(file.as_raw_fd()), + ); + request( + &mut wire, + pool, + 0, + &[buffer, OFFSET, WIDTH, HEIGHT, STRIDE, format], + None, + ); + buffers.push(ReusableShm { + file, + buffer, + version: 0, + format, + }); + } + let region = 7 + buffer_count as u32 * 2; + request(&mut wire, compositor.id().protocol_id(), 1, &[region], None); + if switching_formats { + // ARGB is genuinely translucent in this scenario; XRGB receives + // automatic opacity from its declared SHM format. + request(&mut wire, 2, 4, &[0], None); + } else { + request(&mut wire, region, 1, &[0, 0, WIDTH, HEIGHT], None); + request(&mut wire, 2, 4, &[region], None); + } + request(&mut wire, region, 0, &[], None); + request(&mut wire, 3, 3, &[0, 0, WIDTH, HEIGHT], None); + request(&mut wire, 2, 1, &[buffers[0].buffer, 0, 0], None); + request(&mut wire, 2, 9, &[0, 0, WIDTH, HEIGHT], None); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + state.windows.update(id, |record| record.opening = None); + state.animations_enabled = true; + state.advance_animations_at(Duration::from_secs(1), Duration::from_secs(1)); + let fixed_geometry = state.windows.geometry(&id).unwrap().visual.current; + assert_eq!(window.geometry().size, (WIDTH as i32, HEIGHT as i32).into()); + + let mut tracker = OutputDamageTracker::from_output(&output); + let mut targets: Vec = (0..3) + .map(|_| { + renderer + .create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into()) + .unwrap() + }) + .collect(); + let mut history = Vec::new(); + let mut total_changed_bytes = 0; + let mut partial_frames = 0; + for frame_index in 0..24 { + let mut changed = None; + if frame_index > 0 { + let rect = if switching_formats { + full + } else { + Rectangle::::new( + ( + (frame_index * 37 % (WIDTH as usize - 29)) as i32, + (frame_index * 23 % (HEIGHT as usize - 13)) as i32, + ) + .into(), + (29, 13).into(), + ) + }; + let color = 0xff000000u32 + | (((frame_index * 41) % 220 + 20) as u32) << 16 + | (((frame_index * 67) % 220 + 20) as u32) << 8 + | (((frame_index * 97) % 220 + 20) as u32); + for y in rect.loc.y..rect.loc.y + rect.size.h { + for x in rect.loc.x..rect.loc.x + rect.size.w { + let at = (y as u32 * WIDTH + x as u32) as usize * 4; + raster[at..at + 4].copy_from_slice(&color.to_ne_bytes()); + } + } + history.push(rect); + let buffer = &mut buffers[frame_index % buffer_count]; + let encoded = buffer_pixels(&raster, buffer.format, switching_formats); + // A recycled client buffer catches up only the rectangles changed + // since its last use. Surface damage describes this frame's delta + // from the preceding surface commit, independent of buffer age. + for pending in &history[buffer.version..] { + write_region(&buffer.file, &encoded, *pending); + total_changed_bytes += pending.size.w * pending.size.h * 4; + } + buffer.version = history.len(); + request(&mut wire, 2, 1, &[buffer.buffer, 0, 0], None); + request( + &mut wire, + 2, + 9, + &[ + rect.loc.x as u32, + rect.loc.y as u32, + rect.size.w as u32, + rect.size.h as u32, + ], + None, + ); + request(&mut wire, 2, 6, &[], None); + dispatch(&mut events, &mut state); + changed = Some((rect, color)); + partial_frames += 1; + } + state.advance_animations_at( + Duration::from_millis(16), + Duration::from_millis(1016 + frame_index as u64 * 16), + ); + let scene = state.sample_frame(&output, Duration::ZERO); + assert_eq!( + scene.windows[&id].presentation.bounds.current, fixed_geometry, + "streaming moved the window" + ); + assert_eq!(scene.windows[&id].presentation.alpha(), 1.0); + let elements = crate::render::sampled_output_elements( + &mut state, + &mut renderer, + &output, + false, + &scene, + ); + assert!( + elements + .iter() + .all(|element| !element.is_framebuffer_effect()) + ); + let slot = frame_index % 3; + { + let mut target = renderer.bind(&mut targets[slot]).unwrap(); + tracker + .render_output( + &mut renderer, + &mut target, + if frame_index < 3 { 0 } else { 3 }, + &elements, + Color32F::new(0.035, 0.04, 0.055, 1.0), + ) + .unwrap() + .sync + .wait() + .unwrap(); + } + let actual = pixels(&mut renderer, &mut targets[slot]); + let cached_format = smithay::backend::renderer::utils::with_renderer_surface_state( + window.toplevel().unwrap().wl_surface(), + |surface| { + surface + .texture::(renderer.context_id()) + .and_then(smithay::backend::renderer::Texture::format) + }, + ) + .flatten(); + + // A fresh, unshared context cannot reuse the display path's imported + // SHM texture. Smithay creates a new texture with a complete TexImage2D + // upload regardless of partial damage, then this oracle repaints every + // layer without damage history or opaque-region culling. + let mut oracle = + unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; + let oracle_context = oracle.context_id().erased(); + let mut reference: GlesTexture = oracle + .create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into()) + .unwrap(); + let full_elements = crate::render::sampled_output_elements( + &mut state, + &mut oracle, + &output, + false, + &scene, + ); + let fresh_format = smithay::backend::renderer::utils::with_renderer_surface_state( + window.toplevel().unwrap().wl_surface(), + |surface| { + surface + .texture::(oracle.context_id()) + .and_then(smithay::backend::renderer::Texture::format) + }, + ) + .flatten(); + { + let mut target = oracle.bind(&mut reference).unwrap(); + let mut frame = oracle + .render(&mut target, physical, Transform::Normal) + .unwrap(); + frame + .clear( + Color32F::new(0.035, 0.04, 0.055, 1.0), + &[Rectangle::from_size(physical)], + ) + .unwrap(); + for element in full_elements.iter().rev() { + let rect = element.geometry(scale.into()); + element + .draw( + &mut frame, + element.src(), + rect, + &[Rectangle::from_size(rect.size)], + &[], + None, + ) + .unwrap(); + } + frame.finish().unwrap().wait().unwrap(); + } + let expected = pixels(&mut oracle, &mut reference); + let differing = actual + .iter() + .zip(&expected) + .filter(|(a, b)| a.abs_diff(**b) > 1) + .count(); + if differing > 0 { + mismatching_frames += 1; + let differences = actual + .chunks_exact(4) + .zip(expected.chunks_exact(4)) + .enumerate() + .filter(|(_, (a, b))| a.iter().zip(*b).any(|(x, y)| x.abs_diff(*y) > 1)) + .map(|(at, (a, b))| { + ( + (at % physical.w as usize, at / physical.w as usize), + a.to_vec(), + b.to_vec(), + ) + }) + .collect::>(); + let bounds = differences.iter().fold( + (usize::MAX, usize::MAX, 0, 0), + |(l, t, r, b), ((x, y), _, _)| (l.min(*x), t.min(*y), r.max(*x), b.max(*y)), + ); + eprintln!( + "reused client buffers={buffer_count}, partial frame={frame_index}, different channels={differing}, pixel bounds={bounds:?}, samples={:?}", + &differences[..differences.len().min(8)] + ); + if switching_formats { + let rgb_differing = actual + .chunks_exact(4) + .zip(expected.chunks_exact(4)) + .filter(|(a, b)| { + a[..3].iter().zip(&b[..3]).any(|(x, y)| x.abs_diff(*y) > 1) + }) + .count(); + eprintln!( + "same-size SHM first_format={first_format:?} attached_format={} cached_texture_format={cached_format:?} fresh_texture_format={fresh_format:?} RGB-mismatching-pixels={rgb_differing}", + buffers[frame_index % buffer_count].format + ); + } + } + if !switching_formats && let Some((rect, color)) = changed { + let x = ((fixed_geometry.x + f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.) + * scale) as usize; + let y = ((fixed_geometry.y + f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.) + * scale) as usize; + let at = (y * physical.w as usize + x) * 4; + let cpu_rgba = [(color >> 16) as u8, (color >> 8) as u8, color as u8, 255]; + assert_eq!( + &actual[at..at + 4], + &cpu_rgba, + "partial upload missed CPU raster at frame={frame_index}" + ); + } + state.render.forget_context(&oracle_context); + } + assert_eq!(partial_frames, 23); + if !switching_formats { + assert!(total_changed_bytes < (WIDTH * HEIGHT * 4 * partial_frames as u32) as i32); + } + eprintln!( + "stable scale={scale}, reused wl_buffers={buffer_count}: 24 age-3 frames, {partial_frames} partial commits, {total_changed_bytes} locally written SHM bytes, 24 fresh-context full-upload oracle frames" + ); + } + assert_eq!( + mismatching_frames, 0, + "partial SHM frames differ from complete fresh uploads" + ); +} diff --git a/compositor/src/state/resize_scene_gpu_tests.rs b/compositor/src/state/resize_scene_gpu_tests.rs new file mode 100644 index 0000000..28b2867 --- /dev/null +++ b/compositor/src/state/resize_scene_gpu_tests.rs @@ -0,0 +1,341 @@ +//! Exercise the display scene's damage history, independently of capture repainting. +use super::*; +use smithay::backend::renderer::damage::OutputDamageTracker; +use smithay::backend::renderer::element::{Element, RenderElement}; +use smithay::backend::renderer::{Color32F, Frame, Renderer, Texture}; +use smithay::utils::{Physical, Size}; + +fn commit_raster( + state: &mut Ferese, + events: &mut EventLoop<'static, Ferese>, + window: &Window, + wire: &mut UnixStream, + object: &mut u32, + size: (u32, u32), + phase: u32, +) { + let client = window.toplevel().unwrap().wl_surface().client().unwrap(); + let shm = client + .create_resource::( + &state.display_handle, + 1, + smithay::wayland::GlobalData, + ) + .unwrap(); + let compositor = client + .create_resource::( + &state.display_handle, + 6, + smithay::wayland::GlobalData, + ) + .unwrap(); + let (width, height) = size; + // A high-contrast grid catches stale/cropped pixels, unlike a solid window. + let mut raster = Vec::with_capacity((width * height * 4) as usize); + for y in 0..height { + for x in 0..width { + let color = if (x / 13 + y / 11 + phase) % 2 == 0 { + 0xff26b976_u32 + } else { + 0xffd44381_u32 + }; + raster.extend_from_slice(&color.to_ne_bytes()); + } + } + let mut file = tempfile::tempfile().unwrap(); + file.write_all(&raster).unwrap(); + request( + wire, + shm.id().protocol_id(), + 0, + &[*object, width * height * 4], + Some(file.as_raw_fd()), + ); + request( + wire, + *object, + 0, + &[*object + 1, 0, width, height, width * 4, 0], + None, + ); + request(wire, 3, 3, &[0, 0, width, height], None); + request(wire, 2, 1, &[*object + 1, 0, 0], None); + // Real opaque desktop clients declare an opaque region. Without it the + // damage tracker cannot exercise the rounded element's occlusion path. + request(wire, compositor.id().protocol_id(), 1, &[*object + 2], None); + request(wire, *object + 2, 1, &[0, 0, width, height], None); + request(wire, 2, 4, &[*object + 2], None); + request(wire, *object + 2, 0, &[], None); + request(wire, 2, 2, &[0, 0, width, height], None); + request(wire, 2, 6, &[], None); + *object += 3; + dispatch(events, state); +} + +fn read_pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec { + let size = texture.size(); + let target = renderer.bind(texture).unwrap(); + let mapping = renderer + .copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888) + .unwrap(); + renderer.map_texture(&mapping).unwrap().to_vec() +} + +#[test] +#[ignore = "requires a private Wayland socket and an EGL rendering device"] +fn floating_maximize_handoff_matches_full_repaint_at_buffer_age_three() { + const TEST: &str = "state::capture_privacy::tests::resize_scene_gpu_tests::floating_maximize_handoff_matches_full_repaint_at_buffer_age_three"; + if !crate::startup_tests::private_runtime(TEST) { + return; + } + let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some(); + let device = EGLDevice::enumerate() + .unwrap() + .find(|device| device.is_software() == software) + .expect("requested EGL rendering device"); + eprintln!( + "resize scene EGL software={software}, render node={:?}", + device.render_device_path() + ); + // SAFETY: This test owns the enumerated device and the fresh EGL context. + let display = unsafe { EGLDisplay::new(device).unwrap() }; + // SAFETY: GL work remains on this renderer's owning test thread. + let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; + + for scale in [1.0, 1.5] { + let mut events = EventLoop::try_new().unwrap(); + let config = crate::config::Config::parse_source( + "animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n", + ) + .unwrap() + .runtime_config() + .unwrap(); + let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap(); + state.animation_test_time = Some(Duration::ZERO); + state.focus_effect.active_opacity = 1.0; + state.focus_effect.inactive_opacity = 1.0; + state.focus_effect.inactive_dim = 0.0; + state.theme_settings.window_radius = 12.0; + let output = Output::new( + "resize-age-three".into(), + PhysicalProperties { + serial_number: String::new(), + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + let physical: Size = ((320.0 * scale) as i32, (240.0 * scale) as i32).into(); + output.change_current_state( + Some(Mode { + size: physical, + refresh: 60_000, + }), + Some(Transform::Normal), + Some(Scale::Fractional(scale)), + None, + ); + state.space.map_output(&output, (0, 0)); + state.register_output(&output, "resize-age-three".into()); + let (window, mut wire) = window(&mut state, &mut events, 0xff26b976); + let id = state.windows.ids()[&window]; + state.focused_window = Some(id); + state.set_floating_window_geometry(&window, (106, 92).into(), (128, 80).into()); + state.display_handle.flush_clients().unwrap(); + ack_configure(&mut wire, 3); + let mut object = 7; + commit_raster( + &mut state, + &mut events, + &window, + &mut wire, + &mut object, + (128, 80), + 0, + ); + state.windows.update(id, |record| record.opening = None); + state.animations_enabled = true; + + let mut tracker = OutputDamageTracker::from_output(&output); + let mut targets: Vec = (0..3) + .map(|_| { + renderer + .create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into()) + .unwrap() + }) + .collect(); + let mut reference: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into()) + .unwrap(); + let mut frame_index = 0; + let mut now = Duration::ZERO; + let mut snapshot_frames = 0; + let mut fill_frames = 0; + let mut moving_frames = 0; + let mut repaint_frames = 0; + let mut native_draw_frames = 0; + let mut opaque_frames = 0; + + // Delayed commits exercise both old-frame cropping on shrink and neutral + // fill on grow. Repeating in both directions also reuses every target. + for (cycle, delay) in [3, 1, 5, 0].into_iter().enumerate() { + state.set_window_maximized(id, cycle % 2 == 0); + state.display_handle.flush_clients().unwrap(); + let (_, configured) = ack_configure_with_size(&mut wire, 3, Some(4)); + let requested = configured.expect("maximize/restore configure"); + for tick in 0..28 { + if tick == delay { + // The offscreen fixture has no DirectBackend. Invoke the + // same capture-before-release operation used by that backend. + let snapshot = crate::render::capture_resize_snapshot( + &mut renderer, + &window, + window.geometry(), + scale, + 32 * 1024 * 1024, + ) + .unwrap() + .expect("old client pixels"); + state.render.set_snapshot(id, snapshot); + commit_raster( + &mut state, + &mut events, + &window, + &mut wire, + &mut object, + requested, + cycle as u32 + 1, + ); + repaint_frames += 1; + } else if tick > delay && tick % 2 == 0 { + // IDE repainting alternates with unchanged client buffers, + // so both client damage and compositor-only motion matter. + commit_raster( + &mut state, + &mut events, + &window, + &mut wire, + &mut object, + requested, + cycle as u32 + tick as u32, + ); + repaint_frames += 1; + } + now += Duration::from_millis(16); + state.advance_animations_at(Duration::from_millis(16), now); + let scene = state.sample_frame(&output, Duration::from_millis(5)); + let sample = &scene.windows[&id]; + assert_eq!(sample.presentation.alpha(), 1.0, "ordinary opaque window"); + if sample.geometry.visual.current != sample.geometry.visual.target { + moving_frames += 1; + } + snapshot_frames += usize::from(state.render.snapshot(&id).is_some()); + let source = window.geometry().size; + let visual = sample.presentation.bounds.current; + fill_frames += usize::from( + f64::from(source.w) < visual.width || f64::from(source.h) < visual.height, + ); + let elements = crate::render::sampled_output_elements( + &mut state, + &mut renderer, + &output, + false, + &scene, + ); + native_draw_frames += + usize::from(state.render.snapshot(&id).is_some_and(|snapshot| { + elements.iter().any(|element| element.id() == &snapshot.id) + })); + opaque_frames += usize::from( + elements + .iter() + .any(|element| !element.opaque_regions(scale.into()).is_empty()), + ); + assert!( + elements + .iter() + .all(|element| !element.is_framebuffer_effect()), + "this regression must cover opaque resize without backdrop blur" + ); + let slot = frame_index % 3; + { + let mut target = renderer.bind(&mut targets[slot]).unwrap(); + tracker + .render_output( + &mut renderer, + &mut target, + if frame_index < 3 { 0 } else { 3 }, + &elements, + Color32F::new(0.035, 0.04, 0.055, 1.0), + ) + .unwrap() + .sync + .wait() + .unwrap(); + } + // Independent oracle: no damage history, no opaque-region + // culling, and every layer painted back-to-front into a clean FBO. + { + let mut target = renderer.bind(&mut reference).unwrap(); + let mut frame = renderer + .render(&mut target, physical, Transform::Normal) + .unwrap(); + frame + .clear( + Color32F::new(0.035, 0.04, 0.055, 1.0), + &[Rectangle::from_size(physical)], + ) + .unwrap(); + for element in elements.iter().rev() { + let rect = element.geometry(scale.into()); + element + .draw( + &mut frame, + element.src(), + rect, + &[Rectangle::from_size(rect.size)], + &[], + None, + ) + .unwrap(); + } + frame.finish().unwrap().wait().unwrap(); + } + let actual = read_pixels(&mut renderer, &mut targets[slot]); + let expected = read_pixels(&mut renderer, &mut reference); + let differing = actual + .iter() + .zip(&expected) + .filter(|(a, b)| a.abs_diff(**b) > 1) + .count(); + assert_eq!( + differing, 0, + "scale={scale} cycle={cycle} tick={tick} frame={frame_index} source={source:?} visual={visual:?}" + ); + frame_index += 1; + } + } + assert!(snapshot_frames > 20, "resize handoff was not exercised"); + assert!( + fill_frames > 0, + "growing native content never needed resize fill" + ); + eprintln!( + "opaque resize scale={scale}: {frame_index} age-3 frames, {snapshot_frames} snapshot frames, {native_draw_frames} snapshot draw frames, {fill_frames} fill frames, {moving_frames} moving frames, {repaint_frames} client repaint frames, {opaque_frames} opaque frames" + ); + assert!(moving_frames > 20, "maximize/restore animation did not run"); + assert!( + repaint_frames > 40, + "client repainting did not overlap motion" + ); + assert!( + native_draw_frames > 20, + "retained snapshots were not in the display scene" + ); + assert!( + opaque_frames > 20, + "opaque-region culling was not exercised" + ); + } +} diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 81afff2..fe3384c 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -271,6 +271,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() impl Ferese { pub(crate) fn release_nested_keyboard(&mut self) { + self.super_tap.reset(); // Host shortcuts can consume releases while this window is unfocused. // Release only the physical/nested source, including modifiers, without // executing compositor shortcuts. Auxiliary keyboards retain ownership. diff --git a/compositor/src/xwayland/integration_tests.rs b/compositor/src/xwayland/integration_tests.rs index 6b9b850..1d3dd43 100644 --- a/compositor/src/xwayland/integration_tests.rs +++ b/compositor/src/xwayland/integration_tests.rs @@ -1778,7 +1778,7 @@ mod manager_loop { state .reload_config_source( "xwayland { enabled #true; startup \"on-demand\"; path \"".to_owned() - + &wrapper(&directory, &[]).display().to_string() + + wrapper(&directory, &[]).display().to_string().as_str() + "\"; }", ) .expect("the reload must be accepted"); diff --git a/crates/ferese-theme/src/calendar.rs b/crates/ferese-theme/src/calendar.rs index c84e073..c1801b5 100644 --- a/crates/ferese-theme/src/calendar.rs +++ b/crates/ferese-theme/src/calendar.rs @@ -103,6 +103,12 @@ fn grid_with_density<'a, M: Clone + 'a>( let month = first.month(); let primary = palette.text.scale_alpha(opacity); let muted = palette.muted.scale_alpha(opacity); + let holiday_color = if palette.appearance == ferese_config::theme::Appearance::Light { + cosmic::iced::Color::from_rgb8(184, 38, 49) + } else { + cosmic::iced::Color::from_rgb8(244, 110, 120) + } + .scale_alpha(opacity); let [previous, today_button, next] = navigation; let header = row![ crate::text( @@ -133,12 +139,19 @@ fn grid_with_density<'a, M: Clone + 'a>( } else { Length::Shrink }); - for label in weekday_labels(ferese_config::locale::resolved()) { + for (weekday, label) in weekday_labels(ferese_config::locale::resolved()) + .into_iter() + .enumerate() + { weekdays = weekdays.push( container( crate::text(label, font) .size(if compact { 9 } else { 11 }) - .class(theme::Text::Color(muted)), + .class(theme::Text::Color(if matches!(weekday, 0 | 6) { + holiday_color + } else { + muted + })), ) .width(cell_width) .center_x(cell_width), @@ -161,14 +174,9 @@ fn grid_with_density<'a, M: Clone + 'a>( let date = date.filter(|_| visible); let is_today = date == Some(today); let holiday = date.and_then(|date| settings.holiday_name(date)); - let holiday_color = if palette.appearance == ferese_config::theme::Appearance::Light { - cosmic::iced::Color::from_rgb8(184, 38, 49) - } else { - cosmic::iced::Color::from_rgb8(244, 110, 120) - } - .scale_alpha(opacity * if in_month { 1. } else { 0.65 }); - let foreground = if holiday.is_some() { - holiday_color + let emphasized = date.is_some() && (holiday.is_some() || matches!(weekday, 0 | 6)); + let foreground = if emphasized { + holiday_color.scale_alpha(if in_month { 1. } else { 0.65 }) } else if is_today || in_month { primary } else { @@ -176,7 +184,7 @@ fn grid_with_density<'a, M: Clone + 'a>( }; let label = date.map_or(String::new(), |date| date.day().to_string()); let font = cosmic::iced::Font { - weight: if holiday.is_some() { + weight: if emphasized { cosmic::iced::font::Weight::Bold } else { cosmic::iced::font::Weight::Normal diff --git a/docs/configuration/bindings.md b/docs/configuration/bindings.md index 2567a97..4f4a687 100644 --- a/docs/configuration/bindings.md +++ b/docs/configuration/bindings.md @@ -18,6 +18,25 @@ Keys use XKB keysyms (`Enter`, letters, `[`/`]`) or physical XKB names (`AD06`). A binding replaces the same normalized chord/match mode. Duplicate bindings are invalid. A disabled binding must omit `action` and `argument`. +A modifier-only `Super` binding is optional: + +```kdl +binding "Super" action="toggle-launcher" +``` + +Either Super key triggers this action when released after an isolated press. +The original press and release still reach the focused application. Other keys, +both Super keys together, pointer movement, clicks, scrolling, gestures and touch +cancel the tap; a mouse button already held also prevents it. Existing Super +chords, including Super+Space, keep their actions. Key repeat does not retrigger +the tap. Locking, shortcut inhibition, client keyboard grabs, focus changes and +device/session teardown cancel pending taps. Native input-method grabs remain +compatible. `Logo` and `Mod4` name the same tap, so they cannot be configured as +separate bindings. Physical matching is not supported for this trigger. + +Use `binding "Super" disabled=#true` to remove a configured tap. Without a tap +binding, Super keeps its existing modifier behavior. + | Actions | Argument | | --- | --- | | `spawn` | Name in `commands` | diff --git a/docs/development/rime-poc.md b/docs/development/rime-poc.md index 0c710a1..0ea671f 100644 --- a/docs/development/rime-poc.md +++ b/docs/development/rime-poc.md @@ -37,6 +37,25 @@ librime 仍是 C++ 引擎,通过一个由安装头文件检查 ABI 的小型 C ## 构建与启动 +### 本机日常会话安装 + +安装清单现包含 `ferese-rime`,Arch 包声明 `librime` 和 `rime-luna-pinyin` +依赖。公共方案与词库仍由系统包维护于 `/usr/share/rime-data`,不复制个人词库。 +直接 DRM 会话默认启用原生输入法,并仅在 Ferese 会话内覆盖继承的 IBus Qt 环境。 +GTK 不强制指定输入模块,由实际的原生 Wayland/X11 后端自行选择; +会话激活环境清空旧的 `GTK_IM_MODULE`,避免 XWayland 应用加载 GTK3 Wayland 模块时崩溃。 +这些变量也导入该会话的 D-Bus/systemd 激活环境。嵌套预览保留原有的显式输入法启动方式。 + +会话在 `$XDG_DATA_HOME/ferese/rime`(默认 `~/.local/share/ferese/rime`)建立独立用户目录, +仅在 `default.custom.yaml` 不存在时安装默认简体拼音和每页九个候选的配置。 +不读取、迁移或覆盖 IBus/Fcitx 的个人词库,也保留该目录内已有的用户定制。 +Rime 由会话固定版本的 systemd 用户临时服务运行,异常后重启并启用十秒输入循环 watchdog; +服务名称由合成器控制套接字稳定生成,Shell 硬崩溃恢复时仅替换该会话的旧输入法实例。 +会话结束时停止,输入法失败保留桌面。可用 `journalctl --user -u 'ferese-rime-session@*'` +查看日志。`FERESE_ENABLE_INPUT_METHOD=0` 可在登录前显式停用此集成。 + +以下预览与硬件验证记录早于此安装集成,不能代替新安装会话的验收。 + 需要仓库的构建依赖,以及 `librime`、`xkbcommon` 和 Wayland 开发文件。 验证环境使用系统 librime 1.17.0、GTK4 4.24.1、RTX 4090、NVIDIA 615.78.08。 Rust 工具链由仓库固定为 1.95.0。 diff --git a/docs/installation.md b/docs/installation.md index 4509df7..a1a0d01 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -39,7 +39,8 @@ sudo pacman -S --needed python base-devel git curl cmake pkgconf wayland libxkbc libinput systemd seatd mesa libdrm fontconfig freetype2 expat dbus \ desktop-file-utils foot pam clang pipewire libpipewire wireplumber polkit \ xdg-desktop-portal xdg-desktop-portal-gtk \ - gstreamer gst-plugins-base gst-plugins-good gst-plugin-pipewire pipewire-pulse slurp + gstreamer gst-plugins-base gst-plugins-good gst-plugin-pipewire pipewire-pulse slurp \ + librime rime-luna-pinyin ``` ### Debian and Ubuntu @@ -61,6 +62,14 @@ other distributions; check the [Smithay dependencies](https://github.com/Smithay/smithay#system-dependencies) for equivalent libraries. +This local fork also builds the native `ferese-rime` service. Outside Arch Linux, +install librime and its development headers plus the Rime Prelude, Luna Pinyin +and Stroke shared schemas under `/usr/share/rime-data` before building and logging +in. The Fedora and Debian/Ubuntu commands above describe the upstream desktop +dependencies and do not yet include these fork-specific Rime packages. Ferese +keeps its user dictionary separate from IBus and Fcitx; see the +[native Rime notes](development/rime-poc.md#本机日常会话安装). + ### X11 support (optional) To run X11-only applications, Ferese launches diff --git a/docs/shortcuts.md b/docs/shortcuts.md index f86fcb4..4d74b8e 100644 --- a/docs/shortcuts.md +++ b/docs/shortcuts.md @@ -24,6 +24,13 @@ current workspace. Power off, restart and logout also require **Alt + Enter**. Run `feresectl launcher toggle` to open or close it from a script. +To also toggle the launcher by pressing and releasing Super alone, add +`binding "Super" action="toggle-launcher"` to your configuration. The action runs +on release; using another key, the pointer or a held mouse button cancels it, so +Super+Space and Super+mouse combinations continue to work. See +[modifier-only bindings](configuration/bindings.md) for input ownership and +cancellation behavior. + ## Move and resize with the mouse | Action | Result | diff --git a/packaging/arch/PKGBUILD.in b/packaging/arch/PKGBUILD.in index 4c8c66c..e8d1fad 100644 --- a/packaging/arch/PKGBUILD.in +++ b/packaging/arch/PKGBUILD.in @@ -7,7 +7,8 @@ url='https://github.com/ferese-wm/ferese' license=('MIT' 'Apache-2.0' 'BSD-2-Clause' 'BSD-3-Clause' 'BSL-1.0' 'CC0-1.0' 'CDLA-Permissive-2.0' 'ISC' 'MPL-2.0' 'Unicode-3.0' 'Unlicense' 'Zlib' 'OFL-1.1' 'Apache-2.0 WITH LLVM-exception') depends=('wayland' 'libxkbcommon' 'libinput' 'systemd-libs' 'seatd' 'mesa' 'libdrm' 'fontconfig' 'freetype2' 'expat' 'dbus' 'python' 'bash' 'util-linux' 'systemd' 'pam' 'polkit' 'foot' 'pipewire' - 'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2') + 'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2' + 'librime' 'rime-luna-pinyin') makedepends=('cargo' 'cmake' 'clang' 'pkgconf' 'python' 'desktop-file-utils') checkdepends=('dbus' 'vulkan-swrast' 'xorg-xauth') optdepends=('brightnessctl: brightness controls' 'slurp: screenshot and recording region selection' diff --git a/packaging/arch/ferese-git/PKGBUILD b/packaging/arch/ferese-git/PKGBUILD index ce88283..5f4c12b 100644 --- a/packaging/arch/ferese-git/PKGBUILD +++ b/packaging/arch/ferese-git/PKGBUILD @@ -7,7 +7,8 @@ url='https://github.com/ferese-wm/ferese' license=('MIT' 'Apache-2.0' 'BSD-2-Clause' 'BSD-3-Clause' 'BSL-1.0' 'CC0-1.0' 'CDLA-Permissive-2.0' 'ISC' 'MPL-2.0' 'Unicode-3.0' 'Unlicense' 'Zlib' 'OFL-1.1' 'Apache-2.0 WITH LLVM-exception') depends=('wayland' 'libxkbcommon' 'libinput' 'systemd-libs' 'seatd' 'mesa' 'libdrm' 'fontconfig' 'freetype2' 'expat' 'dbus' 'python' 'bash' 'util-linux' 'systemd' 'pam' 'polkit' 'foot' 'pipewire' - 'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2') + 'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2' + 'librime' 'rime-luna-pinyin') makedepends=('cargo' 'cmake' 'clang' 'pkgconf' 'python' 'git' 'desktop-file-utils') checkdepends=('dbus' 'vulkan-swrast' 'xorg-xauth') optdepends=('brightnessctl: brightness controls' 'slurp: screenshot and recording region selection' diff --git a/packaging/ferese-session b/packaging/ferese-session index d47c7b2..e1dec94 100644 --- a/packaging/ferese-session +++ b/packaging/ferese-session @@ -33,6 +33,18 @@ unset WAYLAND_SOCKET if [[ $backend == drm ]]; then unset DISPLAY WAYLAND_DISPLAY export FERESE_SESSION_IMPORT_ENV=1 + # This fork owns its native input-method service. Keep GNOME's IBus settings + # outside this session, including apps launched later through activation. + export FERESE_ENABLE_INPUT_METHOD=${FERESE_ENABLE_INPUT_METHOD:-1} + if [[ $FERESE_ENABLE_INPUT_METHOD == 1 ]]; then + # GTK selects its IM module from the actual GDK backend. Forcing its + # Wayland module crashes GTK3 consumers that initialize an X11 display. + unset GTK_IM_MODULE + export QT_IM_MODULE=wayland QT_IM_MODULES=wayland + export IBUS_ADDRESS= + unset SDL_IM_MODULE + export FERESE_RIME_CONTROL=${FERESE_RIME_CONTROL:-$XDG_RUNTIME_DIR/ferese/rime-control.sock} + fi else export FERESE_SESSION_IMPORT_ENV=0 fi diff --git a/packaging/ferese-session-shell b/packaging/ferese-session-shell index 50ce54c..245f231 100755 --- a/packaging/ferese-session-shell +++ b/packaging/ferese-session-shell @@ -4,6 +4,7 @@ session_target_started=0 shell_pid= agent_pid= autostart_pid= +rime_unit= manage_session=0 if [[ ${FERESE_SESSION_MODE:-desktop} == desktop && ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then manage_session=1 @@ -19,6 +20,9 @@ cleanup() { kill "$agent_pid" 2>/dev/null || true wait "$agent_pid" 2>/dev/null || true fi + if [[ -n $rime_unit ]]; then + timeout --kill-after=1s 6s systemctl --user stop "$rime_unit" || true + fi if ((session_target_started)); then systemctl --user stop ferese-session.target || true fi @@ -43,6 +47,10 @@ if ((manage_session)); then XDG_SESSION_DESKTOP XDG_SESSION_TYPE ) + if [[ ${FERESE_ENABLE_INPUT_METHOD:-0} == 1 ]]; then + activation_vars+=("GTK_IM_MODULE=" QT_IM_MODULE QT_IM_MODULES IBUS_ADDRESS FERESE_RIME_CONTROL + "SDL_IM_MODULE=" "XMODIFIERS=${XMODIFIERS:-}") + fi if command -v dbus-update-activation-environment >/dev/null; then if ! env WAYLAND_DISPLAY="$public_display" \ dbus-update-activation-environment --systemd "${activation_vars[@]}"; then @@ -70,6 +78,59 @@ if ((manage_session)); then fi fi release_dir=$(dirname -- "$(readlink -f -- "$0")") +# Start the native input method on the public socket, without shell privileges. +# Pin the executable to this release and let systemd recover both crashes and a +# stalled input loop. A failed IME must leave the compositor and shell usable. +start_rime() { + local user_data=${FERESE_RIME_USER_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/ferese/rime} + local shared_data=${FERESE_RIME_SHARED_DATA:-/usr/share/rime-data} + local identity=${FERESE_SOCKET:-${XDG_RUNTIME_DIR:-}/$public_display} + local name variable load_state deadline + local -a environment=() + name=$(systemd-escape --template=ferese-rime-session@.service "$identity") || return 1 + [[ -x $release_dir/ferese-rime ]] || return 1 + [[ -r $shared_data/luna_pinyin_simp.schema.yaml ]] || { + echo 'Ferese: install the Rime Luna Pinyin schemas before starting native input.' >&2 + return 1 + } + mkdir -p -- "$user_data" || return 1 + # noclobber preserves existing customization, including a concurrent setup. + if [[ ! -e $user_data/default.custom.yaml && ! -L $user_data/default.custom.yaml ]]; then + (set -o noclobber; cat -- "$release_dir/rime/default.custom.yaml" > "$user_data/default.custom.yaml") || return 1 + fi + # SIGKILL cannot run the previous helper's trap. The compositor control socket + # survives helper recovery, so replace only its old IME instance. Bound all + # manager calls and wait for the transient unit to be collected before reuse. + load_state=$(timeout --kill-after=1s 2s systemctl --user show "$name" --property=LoadState --value) || return 1 + if [[ $load_state != not-found ]]; then + timeout --kill-after=1s 6s systemctl --user stop "$name" || return 1 + deadline=$((SECONDS + 3)) + while [[ $load_state != not-found ]]; do + ((SECONDS < deadline)) || return 1 + load_state=$(timeout --kill-after=1s 1s systemctl --user show "$name" --property=LoadState --value) || return 1 + if [[ $load_state != not-found ]]; then sleep .05; fi + done + fi + for variable in HOME PATH LANG XDG_RUNTIME_DIR XDG_CONFIG_HOME XDG_CACHE_HOME XDG_DATA_HOME XDG_STATE_HOME \ + DBUS_SESSION_BUS_ADDRESS XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE \ + FERESE_SOCKET FERESE_RIME_CONTROL; do + if [[ -v $variable ]]; then environment+=("--setenv=$variable=${!variable}"); fi + done + if timeout --kill-after=1s 6s systemd-run --user --collect --quiet --no-block --unit="$name" --service-type=notify \ + --property=Restart=on-failure --property=RestartSec=1 \ + --property=StartLimitIntervalSec=60 --property=StartLimitBurst=5 \ + --property=WatchdogSec=10 --property=WatchdogSignal=SIGKILL \ + --property=TimeoutStartSec=45 --property=TimeoutStopSec=3 \ + --property=BindsTo=ferese-session.target --property=PartOf=ferese-session.target \ + "${environment[@]}" \ + /usr/bin/env -u WAYLAND_SOCKET -u FERESE_SHELL_CONTROL_SOCKET -u FERESE_PRIVATE_X11_COOKIE \ + WAYLAND_DISPLAY="$public_display" "$release_dir/ferese-rime" \ + --user-data "$user_data" --shared-data "$shared_data"; then + rime_unit=$name + else + return 1 + fi +} # Agent failures must not take down a healthy shell. The compositor supervises # this helper (and creates fresh private connections) when the shell itself exits. supervise_agent() { @@ -96,6 +157,11 @@ supervise_agent & agent_pid=$! "$release_dir/ferese-shell" & shell_pid=$! +if ((manage_session && session_target_started)) && [[ ${FERESE_ENABLE_INPUT_METHOD:-0} == 1 ]]; then + if ! start_rime; then + echo 'Ferese: native Rime could not start; the desktop remains available.' >&2 + fi +fi if ((manage_session)) && [[ ${FERESE_CLIENT_RESTART:-0} != 1 && -x $release_dir/feresectl ]]; then env WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \ "$release_dir/feresectl" autostart & diff --git a/packaging/rime/default.custom.yaml b/packaging/rime/default.custom.yaml new file mode 100644 index 0000000..573aa08 --- /dev/null +++ b/packaging/rime/default.custom.yaml @@ -0,0 +1,6 @@ +# First-run Ferese defaults; existing user customization is never overwritten. +patch: + schema_list: + - schema: luna_pinyin_simp + - schema: luna_pinyin + menu/page_size: 9 diff --git a/scripts/installer/install.py b/scripts/installer/install.py index 5b5cd9c..dffd795 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -169,11 +169,11 @@ def validate_bundle(bundle, *, installed=False): actual = {str(path.relative_to(bundle)) for path in bundle.rglob('*') if path.is_file()} required = expected.keys() if installed: - # Installed releases may predate the appearance wallpapers and license. + # Installed releases may predate the appearance assets or native Rime. # New bundles must include them; upgrades and rollback verify the older # release's recorded files without requiring these later additions. required = required - {'wallpapers/ferese-wallpaper-dark.png', 'wallpapers/ferese-wallpaper-light.png', - 'licenses/Ferese-LICENSE.txt'} + 'licenses/Ferese-LICENSE.txt', 'ferese-rime', 'rime/default.custom.yaml'} required = {target for target in required if not target.startswith('activities/')} if not required <= seen or actual != seen | {'manifest.json'}: diff --git a/scripts/installer/inventory.json b/scripts/installer/inventory.json index 8fe1665..8c81f9f 100644 --- a/scripts/installer/inventory.json +++ b/scripts/installer/inventory.json @@ -6,6 +6,7 @@ "feresectl", "ferese-lock", "ferese-polkit-agent", + "ferese-rime", "xdg-desktop-portal-ferese" ], "files": [ @@ -50,6 +51,17 @@ "mode": "0755", "link": true }, + { + "source": "target/release/ferese-rime", + "target": "ferese-rime", + "mode": "0755", + "link": true + }, + { + "source": "packaging/rime/default.custom.yaml", + "target": "rime/default.custom.yaml", + "mode": "0644" + }, { "source": "target/release/xdg-desktop-portal-ferese", "target": "xdg-desktop-portal-ferese", diff --git a/scripts/package.py b/scripts/package.py index 256187c..91281cc 100755 --- a/scripts/package.py +++ b/scripts/package.py @@ -35,16 +35,24 @@ def check_tag(tag, release_version): def licenses(destination, metadata): destination.mkdir(parents=True, exist_ok=True) report = [] + workspace_members = set(metadata.get('workspace_members', [])) + workspace_root = Path(metadata.get('workspace_root', ROOT)) for package in sorted(metadata['packages'], key=lambda p: (p['name'], p['version'])): - if package['source'] is None: + if package.get('id') in workspace_members: continue directory = Path(package['manifest_path']).parent - source_hash = hashlib.sha256(package['source'].encode()).hexdigest()[:8] + source = package['source'] + if source is None: + # A vendored/path dependency is third-party code even though Cargo + # gives it no registry/Git source. Keep notices reproducible across + # checkout locations without exporting absolute manifest paths. + source = 'path+' + Path(os.path.relpath(directory, workspace_root)).as_posix() + source_hash = hashlib.sha256(source.encode()).hexdigest()[:8] name = f"{package['name']}-{package['version']}-{source_hash}" files = set() search = [directory] - if package['source'].startswith('git+'): + if source.startswith('git+'): for ancestor in directory.parents: search.append(ancestor) if (ancestor / '.git').exists(): @@ -66,7 +74,7 @@ def licenses(destination, metadata): shutil.copyfile(path, target) report.append({'name': package['name'], 'version': package['version'], - 'source': package['source'], 'license': package['license'], + 'source': source, 'license': package['license'], 'notice_directory': name}) (destination / 'dependencies.json').write_text(json.dumps(report, indent=2) + '\n') @@ -113,7 +121,11 @@ def stage(destdir, binary_dir, pam): planned.extend([(ROOT / f'packaging/pam.d/ferese-lock.{pam}', 'etc/pam.d/ferese-lock', 0o644), (ROOT / 'LICENSE', 'usr/share/licenses/ferese/LICENSE', 0o644), - (ROOT / 'README.md', 'usr/share/doc/ferese/README.md', 0o644)]) + (ROOT / 'README.md', 'usr/share/doc/ferese/README.md', 0o644), + (ROOT / 'vendor/smithay/UPSTREAM.md', + 'usr/share/doc/ferese/vendor/smithay/UPSTREAM.md', 0o644), + (ROOT / 'vendor/smithay/patches/shm-texture-cache-format.patch', + 'usr/share/doc/ferese/vendor/smithay/shm-texture-cache-format.patch', 0o644)]) # Validate the complete inventory before writing any part of the package. for source, _, _ in planned: if not source.is_file(): diff --git a/scripts/tests/test_launcher_rime_isolated.py b/scripts/tests/test_launcher_rime_isolated.py new file mode 100644 index 0000000..894a987 --- /dev/null +++ b/scripts/tests/test_launcher_rime_isolated.py @@ -0,0 +1,116 @@ +"""Real shell + native Rime test on a private Xvfb/nested Wayland/bus. +No host GUI input or application profile is connected to this fixture. + +FERESE_TEST_LAUNCHER_IME=1 python3 scripts/tests/test_launcher_rime_isolated.py +Build the release compositor/shell/Rime/feresectl and debug ferese-rime-probe first. +Synthetic screenshots and protocol traces stay under .artifacts/. +""" +from pathlib import Path +import hashlib,json,os,re,secrets,signal,shutil,subprocess,tempfile,time,unittest + +REPO=Path(__file__).resolve().parents[2] +BASE=REPO/'.artifacts/launcher-rime-isolated' +COMPOSITOR=REPO/os.environ.get('FERESE_TEST_BINARY','target/release/ferese') +PROBE=REPO/os.environ.get('FERESE_TEST_PROBE','target/debug/ferese-rime-probe') +CTL=REPO/os.environ.get('FERESE_TEST_CTL','target/release/feresectl') +RIME=REPO/os.environ.get('FERESE_TEST_RIME_BINARY','target/release/ferese-rime') + +def wait(fn,label,timeout=20): + end=time.monotonic()+timeout + while time.monotonic()sessionunix:path='+str(runtime/'bus')+'') + env={k:v for k,v in os.environ.items() if k not in ['WAYLAND_DISPLAY','WAYLAND_SOCKET','FERESE_SOCKET','FERESE_SHELL_CONTROL_SOCKET','DBUS_SESSION_BUS_ADDRESS','GTK_IM_MODULE','QT_IM_MODULE','IBUS_ADDRESS','XMODIFIERS']} + env.update(HOME=str(case/'home'),XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(case/'config'),XDG_DATA_HOME=str(case/'data'),XDG_CACHE_HOME=str(case/'cache'),XDG_STATE_HOME=str(case/'state'),XDG_DATA_DIRS='/usr/local/share:/usr/share',DISPLAY=xdisplay,XAUTHORITY=str(xauth),TMPDIR=str(case/'tmp'),FERESE_ENABLE_INPUT_METHOD='1',FERESE_ENABLE_SCREENCOPY='1',FERESE_SESSION_IMPORT_ENV='0',FERESE_RIME_CONTROL=str(runtime/'rime.sock'),LIBGL_ALWAYS_SOFTWARE='1',__EGL_VENDOR_LIBRARY_FILENAMES='/usr/share/glvnd/egl_vendor.d/50_mesa.json',WINIT_UNIX_BACKEND='x11',ICED_BACKEND='tiny-skia',PATH='/usr/bin',RUST_LOG='ferese=info') + children=[];log=(case/'session.log').open('w') + try: + bus=subprocess.Popen(['dbus-daemon','--config-file='+str(conf),'--nofork','--print-address=1'],env=env,stdout=subprocess.PIPE,stderr=log,text=True,start_new_session=True);children.append(bus) + env['DBUS_SESSION_BUS_ADDRESS']=bus.stdout.readline().strip();assert env['DBUS_SESSION_BUS_ADDRESS'].startswith('unix:') + comp=subprocess.Popen([str(COMPOSITOR),'--backend','nested','--supervise-client','--grant-effects','--grant-shell-control','--','/usr/bin/env','WAYLAND_DEBUG=client',str(shell)],env=env,stdout=log,stderr=log,start_new_session=True);children.append(comp) + def find_socket(): + candidates=list(runtime.glob('ferese/instances/*/control.sock')) + return next((p for p in candidates if p.is_socket()),None) + sock=wait(find_socket,'private IPC') + def ctl(*args):return subprocess.check_output([str(CTL),'--socket',str(sock),*map(str,args)],env=env,text=True) + outputs=json.loads(ctl('-j','outputs'));assert len(outputs)==1 and outputs[0]['name']=='ferese-winit',outputs + wayland=wait(lambda:next((p for p in runtime.glob('wayland-*') if p.is_socket()),None),'private Wayland') + child_env=env|{'WAYLAND_DISPLAY':str(wayland),'FERESE_SOCKET':str(sock)} + ime=subprocess.Popen([str(RIME),'--user-data',str(case/'rime'),'--shared-data','/usr/share/rime-data'],env=child_env,stdout=log,stderr=log,start_new_session=True);children.append(ime) + wait(lambda:(runtime/'rime.sock').is_socket(),'native Rime') + def status():return json.loads(subprocess.check_output([str(RIME),'--status'],env=child_env,text=True))['state'] + wait(lambda: 'ferese_shell_manager_v1' in (case/'session.log').read_text(), 'real shell control binding') + time.sleep(.5);ctl('launcher','toggle') + wait(lambda:status()['active'],'launcher text-input enable') + subprocess.run([str(RIME),'--mode','rime'],env=child_env,capture_output=True,text=True,check=True) + def cursor_x(): + return [int(x) for x in re.findall(r'zwp_text_input_v3[@#]\d+\.set_cursor_rectangle\((-?\d+),', (case/'session.log').read_text())] + initial=wait(lambda:cursor_x(),'initial caret')[-1] + testenv=child_env|{'FERESE_TEST_INPUT':'1'} + def probe(*args): + p=subprocess.run([str(PROBE),*args],env=testenv,capture_output=True,text=True,timeout=10) + assert p.returncode==0,p.stderr + probe('--keys','nihao');time.sleep(.3) + (case/'preedit.png').write_bytes(subprocess.check_output([str(CTL),'--socket',str(sock),'screenshot'],env=env)) + probe('--keys',' ');time.sleep(.6) + (case/'commit.png').write_bytes(subprocess.check_output([str(CTL),'--socket',str(sock),'screenshot'],env=env)) + trace=(case/'session.log').read_text();xs=cursor_x() + result={'label':label,'shell':str(shell),'shellSha256':hashlib.sha256(shell.read_bytes()).hexdigest(),'compositorPid':comp.pid,'singlePrivateOutputVerified':True,'nativeRimePid':ime.pid,'launcherImeActive':status()['active'],'committedSyntheticHelloSeenOnProtocol':bool(re.search(r'commit_string\("你好"\)',trace)),'initialCaretX':initial,'caretXs':xs,'committedTextMovedCaret':bool(xs and xs[-1]>initial+8),'privateHeadlessX11Parent':xdisplay,'hostGuiUsed':False,'userProfilesUsed':False,'messagesSent':False} + (case/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n');print(json.dumps(result,ensure_ascii=False)) + probe('--chord','Control_L+a','--key','BackSpace','--key','Escape') + return result + finally: + for child in reversed(children): + if child.poll() is None: + os.killpg(child.pid,signal.SIGTERM) + try:child.wait(timeout=4) + except subprocess.TimeoutExpired:os.killpg(child.pid,signal.SIGKILL);child.wait() + log.close() + # Keep synthetic schema/screenshots/logs as evidence; remove dead private sockets. + import shutil + shutil.rmtree(runtime) + + + +@unittest.skipUnless(os.environ.get('FERESE_TEST_LAUNCHER_IME') == '1', 'opt-in isolated launcher/Rime test') +class LauncherRimeTest(unittest.TestCase): + def test_preedit_and_commit_reach_launcher(self): + for tool in ['Xvfb','xauth','dbus-daemon']: + if shutil.which(tool) is None:self.skipTest('missing fixture tool: '+tool) + shell=REPO/os.environ.get('FERESE_TEST_SHELL','target/release/ferese-shell') + for binary in [COMPOSITOR,PROBE,CTL,RIME,shell]: + if not binary.is_file():self.skipTest('build required binary: '+str(binary)) + BASE.mkdir(mode=0o700,parents=True,exist_ok=True) + auth=BASE/'fixture.Xauthority';cookie=secrets.token_hex(16);xvfb=None + with (BASE/'xvfb.log').open('w') as log: + try: + for number in range(190,230): + if Path(f'/tmp/.X{number}-lock').exists() or Path(f'/tmp/.X11-unix/X{number}').exists():continue + display=':'+str(number) + subprocess.run(['xauth','-f',str(auth),'add',display,'MIT-MAGIC-COOKIE-1',cookie],capture_output=True,check=True) + auth.chmod(0o600) + xvfb=subprocess.Popen(['Xvfb',display,'-screen','0','1600x1000x24','-nolisten','tcp','-auth',str(auth)],stdout=log,stderr=log,start_new_session=True) + try:wait(lambda:Path(f'/tmp/.X11-unix/X{number}').is_socket(),'Xvfb startup',5) + except RuntimeError: + if xvfb.poll() is None:os.killpg(xvfb.pid,signal.SIGTERM);xvfb.wait() + xvfb=None;continue + break + self.assertIsNotNone(xvfb,'No private Xvfb display available') + result=run_case('current',shell.resolve(),display,auth) + self.assertTrue(result['singlePrivateOutputVerified']) + self.assertTrue(result['committedSyntheticHelloSeenOnProtocol']) + self.assertTrue(result['committedTextMovedCaret'], 'Wayland commit arrived but did not update launcher text/caret') + finally: + if xvfb is not None and xvfb.poll() is None:os.killpg(xvfb.pid,signal.SIGTERM);xvfb.wait(timeout=5) + auth.unlink(missing_ok=True) + +if __name__=='__main__':unittest.main() diff --git a/scripts/tests/test_package.py b/scripts/tests/test_package.py index 726ca7a..6cbd5bc 100644 --- a/scripts/tests/test_package.py +++ b/scripts/tests/test_package.py @@ -76,6 +76,14 @@ class PackageTests(unittest.TestCase): self.assertTrue((dest / 'usr/share/ferese/wallpapers/ferese.png').is_file()) self.assertEqual((dest / 'usr/libexec/ferese/ferese-session').stat().st_mode & 0o777, 0o755) self.assertEqual((dest / 'usr/libexec/ferese').stat().st_mode & 0o777, 0o755) + self.assertTrue((dest / 'usr/libexec/ferese/ferese-rime').is_file()) + self.assertIn('schema: luna_pinyin_simp', + (dest / 'usr/libexec/ferese/rime/default.custom.yaml').read_text()) + provenance = dest / 'usr/share/doc/ferese/vendor/smithay' + for name in ('UPSTREAM.md', 'shm-texture-cache-format.patch'): + origin = PACKAGE.ROOT / 'vendor/smithay' / ( + name if name == 'UPSTREAM.md' else 'patches/' + name) + self.assertEqual((provenance / name).read_bytes(), origin.read_bytes()) def test_same_version_from_different_sources_keeps_both_licenses(self): with tempfile.TemporaryDirectory() as directory: @@ -107,6 +115,36 @@ class PackageTests(unittest.TestCase): self.assertEqual(report[0]['license'], 'MIT') self.assertEqual(report[0]['source'], 'registry+test') + def test_vendored_notices_exclude_workspace_members_and_are_relocatable(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + reports = [] + for checkout in ('first-checkout', 'second-checkout'): + workspace = root / checkout / 'ferese' + packages = [] + for name, folder, license_text in ( + ('ferese', workspace / 'compositor', 'workspace notice'), + ('smithay', workspace / 'vendor/smithay', 'Smithay MIT notice'), + ('external', workspace.parent / 'shared/external', 'external notice')): + folder.mkdir(parents=True) + (folder / 'LICENSE.txt').write_text(license_text) + packages.append({'id': f'path+file://{folder}#{name}@0.7.0', + 'name': name, 'version': '0.7.0', 'source': None, + 'manifest_path': str(folder / 'Cargo.toml'), + 'license': 'MIT', 'license_file': None}) + destination = workspace / 'notices' + PACKAGE.licenses(destination, {'packages': packages, + 'workspace_members': [packages[0]['id']], 'workspace_root': str(workspace)}) + report = json.loads((destination / 'dependencies.json').read_text()) + self.assertEqual({p['name']: p['source'] for p in report}, { + 'smithay': 'path+vendor/smithay', 'external': 'path+../shared/external'}) + notices = {p['name']: (destination / p['notice_directory'] / 'LICENSE.txt').read_text() + for p in report} + self.assertEqual(notices, {'smithay': 'Smithay MIT notice', 'external': 'external notice'}) + self.assertNotIn(str(root), (destination / 'dependencies.json').read_text()) + reports.append(report) + self.assertEqual(reports[0], reports[1]) + if __name__ == '__main__': unittest.main() diff --git a/scripts/tests/test_portal_install.py b/scripts/tests/test_portal_install.py index 1881abb..0e081eb 100644 --- a/scripts/tests/test_portal_install.py +++ b/scripts/tests/test_portal_install.py @@ -141,6 +141,18 @@ m.main() self.assertEqual(os.readlink(self.base / 'current'), 'releases/first') self.assertEqual(installer.Installation(self.root).manifest('first')['release'], 'first') + def test_upgrade_and_rollback_accept_installed_release_before_native_rime(self): + self.install() + self.without_files(self.base / 'releases/first', {'ferese-rime', 'rime/default.custom.yaml'}) + # Older releases did not publish the native input command either. + (self.root / 'usr/local/bin/ferese-rime').unlink() + self.install(1) + self.assertTrue((self.root / 'usr/local/bin/ferese-rime').is_symlink()) + self.assertTrue((self.base / 'releases/second/ferese-rime').is_file()) + self.run_command('rollback') + self.assertFalse((self.root / 'usr/local/bin/ferese-rime').exists()) + self.assertEqual(installer.Installation(self.root).manifest('first')['release'], 'first') + def test_upgrade_and_rollback_accept_installed_release_before_appearance_wallpapers(self): self.install() legacy = self.base / 'releases/first' diff --git a/scripts/tests/test_session_startup.py b/scripts/tests/test_session_startup.py index a24c6cb..95581e1 100644 --- a/scripts/tests/test_session_startup.py +++ b/scripts/tests/test_session_startup.py @@ -13,19 +13,46 @@ SCRIPT = Path(__file__).resolve().parents[2] / 'packaging/ferese-session-shell' class SessionStartupTest(unittest.TestCase): def run_session(self, direct=True, fail_target=False, shell_status=0, terminate=False, - display=None, xauthority=None, extra_env=None, programs_override=None): + display=None, xauthority=None, extra_env=None, programs_override=None, + user_customization=None, restart_after_kill=False): with tempfile.TemporaryDirectory() as directory: root = Path(directory) helper = root / 'ferese-session-shell' shutil.copyfile(SCRIPT, helper) + (root / 'rime').mkdir() + shutil.copyfile(SCRIPT.parent / 'rime/default.custom.yaml', root / 'rime/default.custom.yaml') + shared = root / 'rime-shared' + shared.mkdir() + (shared / 'luna_pinyin_simp.schema.yaml').write_text('fixture') + units = root / 'units' + units.mkdir() + foreign_unit = units / 'ferese-rime-session@another-session.service' + foreign_unit.write_text('foreign service') + if user_customization is not None: + user_rime = root / 'data/ferese/rime' + user_rime.mkdir(parents=True) + (user_rime / 'default.custom.yaml').write_text(user_customization) programs = { 'dbus-update-activation-environment': ( 'printf "import:%s\\n" "$WAYLAND_DISPLAY" >> "$TEST_ROOT/events"\n' 'printf "vars:%s\\n" "$*" >> "$TEST_ROOT/events"\n'), 'dbus-send': 'echo reload-bus >> "$TEST_ROOT/events"\n', - 'systemctl': 'echo "$*" >> "$TEST_ROOT/events"\nif [[ $* == "--user start ferese-session.target" && $TEST_FAIL == 1 ]]; then exit 1; fi\n', + 'systemctl': ( + 'echo "$*" >> "$TEST_ROOT/events"\n' + 'if [[ $* == "--user start ferese-session.target" && $TEST_FAIL == 1 ]]; then exit 1; fi\n' + 'if [[ ${2:-} == show && ${3:-} == ferese-rime-session@* ]]; then\n' + ' if [[ -f $TEST_ROOT/units/$3 ]]; then echo loaded; else echo not-found; fi\n' + 'elif [[ ${2:-} == stop && ${3:-} == ferese-rime-session@* ]]; then\n' + ' rm -f -- "$TEST_ROOT/units/$3"\nfi\n'), 'ferese-polkit-agent': 'exec sleep 30\n', 'ferese-shell': 'printf "shell:%s\\n" "$WAYLAND_SOCKET" >> "$TEST_ROOT/events"\nif [[ $TEST_WAIT == 1 ]]; then exec sleep 30; fi\nexit "$TEST_STATUS"\n', + 'ferese-rime': 'exit 0\n', + 'systemd-run': ( + 'for arg; do if [[ $arg == --unit=* ]]; then unit=${arg#--unit=}; fi; done\n' + '(set -o noclobber; echo running > "$TEST_ROOT/units/$unit") || exit 1\n' + 'printf "rime-arg:%s\\n" "$@" >> "$TEST_ROOT/events"\n' + 'printf "rime-config:%s\\n" "$(cat "$XDG_DATA_HOME/ferese/rime/default.custom.yaml")" >> "$TEST_ROOT/events"\n' + 'touch "$TEST_ROOT/rime-started"\n'), } programs.update(programs_override or {}) for name, body in programs.items(): @@ -36,7 +63,10 @@ class SessionStartupTest(unittest.TestCase): FERESE_SESSION_MODE='desktop', FERESE_SESSION_IMPORT_ENV=str(int(direct)), WAYLAND_DISPLAY='private-display', FERESE_PUBLIC_WAYLAND_DISPLAY='public-display', WAYLAND_SOCKET='77', - TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status), TEST_WAIT=str(int(terminate))) + FERESE_SOCKET=str(root / 'control.sock'), + XDG_DATA_HOME=str(root / 'data'), FERESE_RIME_SHARED_DATA=str(shared), + TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status), + TEST_WAIT=str(int(terminate or restart_after_kill))) env.pop('DISPLAY', None) env.pop('XAUTHORITY', None) if display is not None: @@ -44,7 +74,25 @@ class SessionStartupTest(unittest.TestCase): if xauthority is not None: env['XAUTHORITY'] = xauthority env.update(extra_env or {}) - child = subprocess.Popen(['bash', str(helper)], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + child = subprocess.Popen(['bash', str(helper)], env=env, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, start_new_session=True) + if restart_after_kill: + deadline = time.monotonic() + 3 + while not (root / 'rime-started').exists(): + if time.monotonic() > deadline: + os.killpg(child.pid, signal.SIGKILL) + self.fail('Native input unit did not start') + time.sleep(.01) + child.kill() + child.wait(timeout=2) + # The helper cannot run cleanup after SIGKILL. Dispose of fake + # shell/agent processes, while its fake systemd service survives. + os.killpg(child.pid, signal.SIGKILL) + child.communicate(timeout=2) + self.assertEqual(len(list(units.glob('*.service'))), 2) + env.update(TEST_WAIT='0', FERESE_CLIENT_RESTART='1') + child = subprocess.Popen(['bash', str(helper)], env=env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) if terminate: deadline = time.monotonic() + 3 while not (root / 'events').exists() or 'shell:77' not in (root / 'events').read_text(): @@ -54,6 +102,7 @@ class SessionStartupTest(unittest.TestCase): time.sleep(.01) child.send_signal(signal.SIGTERM) child.communicate(timeout=4) + self.assertEqual(foreign_unit.read_text(), 'foreign service') return child.returncode, (root / 'events').read_text().splitlines() @staticmethod @@ -78,6 +127,78 @@ class SessionStartupTest(unittest.TestCase): self.assertEqual(status, 0) self.assertIn('FERESE_SOCKET=/run/user/1000/ferese/control.sock', self.imported_vars(events)) + def test_native_rime_is_release_pinned_supervised_and_stopped_with_its_session(self): + status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'}) + self.assertEqual(status, 0) + args = [event.removeprefix('rime-arg:') for event in events if event.startswith('rime-arg:')] + self.assertIn('--service-type=notify', args) + for setting in ('Restart=on-failure', 'WatchdogSec=10', 'StartLimitBurst=5', + 'BindsTo=ferese-session.target'): + self.assertIn('--property=' + setting, args) + self.assertIn('WAYLAND_DISPLAY=public-display', args) + self.assertIn('WAYLAND_SOCKET', args) + self.assertIn('FERESE_SHELL_CONTROL_SOCKET', args) + executable = next(arg for arg in args if arg.endswith('/ferese-rime')) + self.assertNotEqual(executable, '/usr/bin/ferese-rime') + unit = next(arg.removeprefix('--unit=') for arg in args if arg.startswith('--unit=')) + self.assertIn('--user stop ' + unit, events) + self.assertEqual(events[-1], '--user stop ferese-session.target') + self.assertIn(' - schema: luna_pinyin_simp', events) + + def test_native_rime_preserves_existing_user_customization(self): + status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'}, + user_customization='my-custom-dictionary: preserved\n') + self.assertEqual(status, 0) + self.assertIn('rime-config:my-custom-dictionary: preserved', events) + + def test_hard_killed_helper_replaces_only_its_existing_native_rime_unit(self): + status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'}, + restart_after_kill=True) + self.assertEqual(status, 0) + starts = [event.removeprefix('rime-arg:--unit=') for event in events + if event.startswith('rime-arg:--unit=')] + self.assertEqual(len(starts), 2) + self.assertEqual(starts[0], starts[1]) + stopped = '--user stop ' + starts[0] + start_events = [index for index, event in enumerate(events) if event == 'rime-arg:--unit=' + starts[0]] + self.assertIn(stopped, events[start_events[0] + 1:start_events[1]]) + self.assertNotIn('--user stop ferese-rime-session@another-session.service', events) + + def test_unresponsive_old_rime_unit_blocks_replacement_without_stopping_the_desktop(self): + # Model a timed-out manager request. The helper must not create a second + # input service while it cannot confirm the state of the first one. + status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'}, + programs_override={'systemctl': ( + 'echo "$*" >> "$TEST_ROOT/events"\n' + 'if [[ ${2:-} == show ]]; then exit 124; fi\n')}) + self.assertEqual(status, 0) + self.assertIn('shell:77', events) + self.assertFalse(any(event.startswith('rime-arg:') for event in events)) + + def test_failed_rime_start_leaves_the_desktop_running(self): + status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'}, + programs_override={'systemd-run': 'exit 7\n'}) + self.assertEqual(status, 0) + self.assertIn('shell:77', events) + self.assertFalse(any(event.startswith('--user stop ferese-rime-') for event in events)) + + def test_nested_session_never_starts_native_rime(self): + status, events = self.run_session(direct=False, extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'}) + self.assertEqual(status, 0) + self.assertEqual(events, ['shell:77']) + + def test_native_input_environment_is_imported_for_activated_applications(self): + status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1', + 'GTK_IM_MODULE': '', 'QT_IM_MODULE': 'wayland', 'QT_IM_MODULES': 'wayland', + 'IBUS_ADDRESS': '', 'XMODIFIERS': '@im=ibus', 'FERESE_RIME_CONTROL': '/private/rime.sock'}) + self.assertEqual(status, 0) + imported = self.imported_vars(events) + for name in ('QT_IM_MODULE', 'QT_IM_MODULES', 'IBUS_ADDRESS', 'FERESE_RIME_CONTROL'): + self.assertIn(name, imported) + self.assertIn('GTK_IM_MODULE=', imported) + self.assertIn('SDL_IM_MODULE=', imported) + self.assertIn('XMODIFIERS=@im=ibus', imported) + def test_a_managed_x11_endpoint_is_imported_for_dbus_activators(self): status, events = self.run_session(display=':7', xauthority='/run/user/1000/ferese-xauth-abcd') self.assertEqual(status, 0) @@ -161,5 +282,46 @@ class SessionStartupTest(unittest.TestCase): self.assertEqual(events.count('autostart'), int(restarted == '0')) +class SessionLauncherTest(unittest.TestCase): + def launch(self, nested=False, enabled=None): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + launcher = root / 'ferese-session' + shutil.copyfile(SCRIPT.parent / 'ferese-session', launcher) + for name, body in {'ferese': '/usr/bin/env > "$TEST_ROOT/environment"\n', + 'systemctl': 'exit 0\n'}.items(): + path = root / name + path.write_text('#!/bin/bash\n' + body) + path.chmod(0o755) + env = dict(os.environ, PATH=f'{root}:{os.environ["PATH"]}', TEST_ROOT=directory, + HOME=directory, XDG_RUNTIME_DIR=directory, DBUS_SESSION_BUS_ADDRESS='fixture', + XDG_STATE_HOME=str(root / 'state'), GTK_IM_MODULE='ibus', QT_IM_MODULE='ibus', + QT_IM_MODULES='wayland;ibus', IBUS_ADDRESS='old-ibus', XMODIFIERS='@im=ibus', SDL_IM_MODULE='ibus') + env.pop('FERESE_ENABLE_INPUT_METHOD', None) + if enabled is not None: + env['FERESE_ENABLE_INPUT_METHOD'] = enabled + subprocess.run(['bash', str(launcher), *(['--nested'] if nested else [])], + env=env, capture_output=True, check=True) + return dict(line.split('=', 1) for line in (root / 'environment').read_text().splitlines()) + + def test_drm_session_overrides_inherited_ibus_only_for_native_input(self): + env = self.launch() + self.assertEqual(env['FERESE_ENABLE_INPUT_METHOD'], '1') + self.assertNotIn('GTK_IM_MODULE', env) + for variable in ('QT_IM_MODULE', 'QT_IM_MODULES'): + self.assertEqual(env[variable], 'wayland') + self.assertEqual(env['IBUS_ADDRESS'], '') + self.assertEqual(env['XMODIFIERS'], '@im=ibus') + self.assertNotIn('SDL_IM_MODULE', env) + self.assertTrue(env['FERESE_RIME_CONTROL'].endswith('/ferese/rime-control.sock')) + + def test_nested_and_explicit_opt_out_preserve_input_configuration(self): + for options in ({'nested': True}, {'enabled': '0'}): + with self.subTest(options=options): + env = self.launch(**options) + self.assertEqual(env['QT_IM_MODULE'], 'ibus') + self.assertNotEqual(env.get('FERESE_ENABLE_INPUT_METHOD'), '1') + + if __name__ == '__main__': unittest.main() diff --git a/shell/ferese-shell/Cargo.toml b/shell/ferese-shell/Cargo.toml index 7e2220d..d9b0274 100644 --- a/shell/ferese-shell/Cargo.toml +++ b/shell/ferese-shell/Cargo.toml @@ -24,6 +24,10 @@ jiff.workspace = true libc.workspace = true # Prefer GPU rendering, retaining software rendering as a fallback/override. libcosmic = { workspace = true, features = ["wgpu", "a11y"] } +# The pinned Wayland backend gates text-input-v3 event dispatch on this empty +# feature. Enable it directly without changing the shell's ordinary app::run or +# pulling libcosmic's unrelated single-instance/D-Bus application support. +iced_winit = { git = "https://github.com/ferese-wm/libcosmic", tag = "ferese-v0.1.3-rc.3", default-features = false, features = ["single-instance"] } freedesktop-desktop-entry = { version = "0.8.3", default-features = false } iced_accessibility.workspace = true serde.workspace = true diff --git a/vendor/smithay/.github/ISSUE_TEMPLATE/bug_report.yml b/vendor/smithay/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..f570749 --- /dev/null +++ b/vendor/smithay/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,21 @@ +name: Bug Report +description: File a bug report +type: bug +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + Please don't use generative AI to generate the text of this issue. + See our [AI Policy](https://github.com/Smithay/smithay/blob/master/AI.md) for more information. + - type: checkboxes + id: terms + attributes: + label: AI Policy + options: + - label: I have read the [AI policy](https://github.com/Smithay/smithay/blob/master/AI.md). + required: true + - type: textarea + id: description + attributes: + label: Issue description diff --git a/vendor/smithay/.github/pull_request_template.md b/vendor/smithay/.github/pull_request_template.md new file mode 100644 index 0000000..70a148f --- /dev/null +++ b/vendor/smithay/.github/pull_request_template.md @@ -0,0 +1,10 @@ +## Description + + + + + + +## Checklist + +* [ ] I agree to smithay's [Developer Certificate of Origin](https://github.com/Smithay/smithay/blob/master/DCO.md). diff --git a/vendor/smithay/.github/workflows/ci.yml b/vendor/smithay/.github/workflows/ci.yml new file mode 100644 index 0000000..a95f618 --- /dev/null +++ b/vendor/smithay/.github/workflows/ci.yml @@ -0,0 +1,441 @@ +name: Continuous Integration + +on: + push: + branches: + - master + pull_request: + +jobs: + format: + runs-on: ubuntu-24.04 + steps: + - name: Checkout sources + uses: actions/checkout@v3 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }} + - name: Format + run: cargo fmt --all -- --check + + clippy-check: + runs-on: ubuntu-24.04 + steps: + - name: Checkout sources + uses: actions/checkout@v3 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + - name: Get cargo-cache latest version + id: cargocacheversion + run: echo "::set-output name=version::$(cargo search 'cargo-cache' --limit 1 | head -n 1 | cut -d ' ' -f 3 | cut -d '"' -f 2)" + - name: Cargo binaries cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/cargo-cache + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + key: ${{ runner.os }}-cargo-binaries-${{ steps.cargocacheversion.outputs.version }} + - name: Install cargo-cache + run: cargo install cargo-cache --version ${{ steps.cargocacheversion.outputs.version }} + - name: Clean cargo cache of old items + run: cargo cache clean-unref + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev + - name: Clippy Smithay + run: cargo clippy --features "test_all_features" -- -D warnings + - name: Clippy Anvil + run: cargo clippy --manifest-path "./anvil/Cargo.toml" --features "test_all_features" --no-deps -- -D warnings + - name: Clippy Smallvil + run: cargo clippy --manifest-path "./smallvil/Cargo.toml" --no-deps -- -D warnings + - name: Clippy drm-extras + run: cargo clippy --manifest-path "./smithay-drm-extras/Cargo.toml" --no-deps -- -D warnings + + check-msrv: + env: + RUSTFLAGS: "-D warnings" + + runs-on: ubuntu-24.04 + needs: + - smithay-check-features + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@1.87 + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev + - name: Check + run: cargo check --features "test_all_features" + + check-minimal: + env: + RUSTFLAGS: "-D warnings" + + runs-on: ubuntu-24.04 + needs: + - smithay-check-features + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@nightly + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev + - name: Downgrade to minimal dependencies + run: cargo -Z minimal-versions generate-lockfile + # Needed to convert url -> string in winit + - name: Selectively upgrade url + run: cargo update -p url@2.0.0 --precise 2.3.0 + # Needed to fix WlTouch trait bounds + - name: Selectively upgrade client-toolkit + run: cargo update -p smithay-client-toolkit@0.21.0 --precise 0.21.1 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@1.87 + - name: Check + run: cargo check --features "test_all_features" + + smithay-check-features: + env: + RUSTFLAGS: "-D warnings" + + runs-on: ubuntu-24.04 + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev + + - name: Test features + env: + RUST_BACKTRACE: full + run: cargo hack check --each-feature --no-dev-deps --exclude-features use_bindgen + + smithay-tests: + needs: + - smithay-check-features + + env: + RUSTFLAGS: "-D warnings" + + runs-on: ubuntu-24.04 + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpixman-1-dev + + - name: Run tests + env: + RUST_BACKTRACE: full + run: cargo test --features "test_all_features" + + smallvil-check: + needs: + - smithay-check-features + + env: + RUSTFLAGS: "-D warnings" + + runs-on: ubuntu-24.04 + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libegl1-mesa-dev libwayland-dev + + - name: Test smallvil + env: + RUST_BACKTRACE: full + run: cargo check --manifest-path "./smallvil/Cargo.toml" + + anvil-check-features: + needs: + - smithay-check-features + + env: + RUSTFLAGS: "-D warnings" + + runs-on: ubuntu-24.04 + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev + + - name: Test features + env: + RUST_BACKTRACE: full + run: cargo hack check --each-feature --manifest-path "./anvil/Cargo.toml" --exclude-features profile-with-puffin,profile-with-tracy,profile-with-tracy-mem + + anvil-wlcs: + needs: + - anvil-check-features + + strategy: + fail-fast: false + matrix: + include: + - job_name: "Core tests" + gtest_filter: "SelfTest*:FrameSubmission*" + flag: core + - job_name: "Output tests" + gtest_filter: "XdgOutputV1Test*" + flag: output + - job_name: "Pointer input tests" + gtest_filter: "*/SurfacePointerMotionTest*:RelativePointer*" + flag: pointer-input + - job_name: "Bad Buffer Test" + gtest_filter: "BadBufferTest*" + flag: buffer + + name: "WLCS: ${{ matrix.job_name }}" + + env: + RUSTFLAGS: "-D warnings" + GRCOV_VERSION: "0.8.10" + + runs-on: ubuntu-24.04 + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: llvm-tools-preview + + - name: grcov cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/grcov + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + key: ${{ runner.os }}-grcov-${{ env.GRCOV_VERSION }} + + - name: Install grcov + run: cargo install grcov --version ${{ env.GRCOV_VERSION }} + + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y cmake libgtest-dev google-mock libboost-dev libxkbcommon-dev libwayland-dev libdisplay-info-dev + + - name: WLCS build cache + uses: actions/cache@v3 + with: + path: wlcs + key: ${{ runner.os }}-build-wlcs-${{ hashFiles('./compile_wlcs.sh') }} + + - name: Build WLCS + run: ./compile_wlcs.sh + + - name: Build anvil WLCS plugin + env: + RUST_BACKTRACE: full + RUSTFLAGS: "-Cinstrument-coverage --cfg coverage" + run: cargo build --manifest-path "./wlcs_anvil/Cargo.toml" + + - name: Run test suite + run: ./wlcs/wlcs ./target/debug/libwlcs_anvil.so --gtest_filter="${{ matrix.gtest_filter }}" + env: + LLVM_PROFILE_FILE: "anvil-wlcs-%p-%m.profraw" + + - name: Coverage + run: grcov . --binary-path ./target/debug -s . -t lcov --branch --llvm --ignore-not-existing --ignore '*/examples/*' --ignore '*/tests/*' --ignore '*/.cargo/registry/*' --excl-br-start "mod tests \{" --excl-start "mod tests \{" --excl-br-line "#\[derive\(" --excl-line "#\[derive\(" -o lcov.info + + - name: Upload to codecov.io + uses: codecov/codecov-action@v3 + with: + flags: wlcs-${{ matrix.flag }} + + doc: + name: Documentation on Github Pages + runs-on: ubuntu-24.04 + needs: + - smithay-check-features + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Get date for registry cache + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%d')" + - name: Cargo registry cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2025-09-26 + + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev + + - name: Build Documentation + env: + RUSTDOCFLAGS: --cfg=docsrs -D warnings + run: cargo doc --no-deps --features "test_all_features" -p smithay -p calloop:0.14 -p drm -p gbm -p input -p udev -p wayland-server -p wayland-backend -p wayland-protocols -p winit -p x11rb -p tracing + + - name: Setup index + run: cp ./doc_index.html ./target/doc/index.html + + - name: Upload static files as artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./target/doc + + - name: Deploy to GitHub Pages + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + uses: actions/deploy-pages@v4 + +permissions: + pages: write + id-token: write diff --git a/vendor/smithay/.github/workflows/typos.yml b/vendor/smithay/.github/workflows/typos.yml new file mode 100644 index 0000000..ccaecd9 --- /dev/null +++ b/vendor/smithay/.github/workflows/typos.yml @@ -0,0 +1,18 @@ +--- +name: check_typos + +on: + push: + pull_request: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: typos-action + uses: crate-ci/typos@v1.42.1 diff --git a/vendor/smithay/.gitignore b/vendor/smithay/.gitignore new file mode 100644 index 0000000..84f58ef --- /dev/null +++ b/vendor/smithay/.gitignore @@ -0,0 +1,6 @@ +target +Cargo.lock +*.bk +.vscode +.vagga +/wlcs diff --git a/vendor/smithay/.rustfmt.toml b/vendor/smithay/.rustfmt.toml new file mode 100644 index 0000000..284d219 --- /dev/null +++ b/vendor/smithay/.rustfmt.toml @@ -0,0 +1 @@ +max_width = 110 diff --git a/vendor/smithay/AI.md b/vendor/smithay/AI.md new file mode 100644 index 0000000..2c5f05c --- /dev/null +++ b/vendor/smithay/AI.md @@ -0,0 +1,30 @@ +The following policy applies if you make a contribution to Smithay's +code, documentation, issues, comments, artwork or any other type of resource. + +It is your responsibility to ensure that: +- You fully understand how the code in your contribution works. +- Your change is accurately described in the commit message. +- Your contribution is tested and working as described. +- To the best of your knowledge, you are not introducing bugs; any exceptions to this rule must be presented with clear reasoning. +- You are not violating copyright (such as software licenses, creative commons or any other). (See also our [DCO](https://github.com/Smithay/smithay/blob/master/DCO.md).) +- Use of any AI generated code or documentation is clearly indicated in your commit messages. + +If you repeatedly fail to meet these standards, your contributions will no longer +be considered by Smithay's maintainers and possibly removed without further notice. + +Due to the requirements listed above, we strongly advise against using large +language models or other probabilistic tools such as Copilot and ChatGPT. +Code produced by these systems is usually more difficult to reason about +and fails to express important technical design decisions in a concise form. +This results in additional maintainer burden, draining the (already limited) +time and energy of our reviewers. Additionally, the legal circumstances of the training sets these +tools are based on are subject of active legal proceedings, which puts the copyright of generated +contributions into question. + +Last but not least, we ask you to not use generative AI to write comments or descriptions +on our issue tracker, discussions and pull requests. In our experience these tools tend to generate +very inconcise text and more often incorrect statements about the cause of underlying issues or code. + +If you rely on such tools to make up for a language barrier, we strongly encourage you to use +(ML-based or otherwise) translation tools as our maintainers much prefer to deal with +imperfect english than generated walls of text. Many of us aren't native english speakers as well. diff --git a/vendor/smithay/CHANGELOG.md b/vendor/smithay/CHANGELOG.md new file mode 100644 index 0000000..2ecc234 --- /dev/null +++ b/vendor/smithay/CHANGELOG.md @@ -0,0 +1,841 @@ +# Smithay Changelog + +## Unreleased + +### Breaking changes + +`wayland::virtual_keyboard` no longer sends key events to focused clients on its +own. A virtual keyboard is now an regular input device: +`VirtualKeyboardManagerState::new` requires the state to implement the new +`VirtualKeyboardHandler`, which receives `InputEvent`s +for the compositor to handle like real devices (so special keys, compositor +bindings, idle handling and focus works properly). The compositor has to +activate the device's keymap (`VirtualKeyboardDevice::keymap`) before processing +its key events. Modifiers set with the `modifiers` request are not reset when +the device is destroyed (the masks are absolute, so that would also clear state +not set by the device, like locks set by other devices on a shared keymap). The +compositor is responsible for reconciling the modifier state on `DeviceRemoved` +(e.g., by tracking the bits set by it). `VirtualKeyboardUserData` is no longer +generic over the state type. + +`crate::wayland::selection::data_device::start_dnd` was removed in favor of exposing the +underlying `DnDGrab` and associated types to make it possible to write external Drag&Drop sources +and targets (e.g. for other shell implementations). See the `Additions`-section for more info. + +This also means the `ServerDndGrabHandler` was removed. The `ClientDndGrabHandler` was split into +a generic `DndGrabHandler` and a specific `WaylandDndGrabHandler`. `WaylandDndGrabHandler::started` needs to +be implemented and explicitly start a `DnDGrab` for wayland DnD-operations to work. + +`X11WM::start_wm` now needs a `DisplayHandle`-reference and adds requirements for the State type to +implement `DndGrabHandler` and the `SeatHandler`s `PointerFocus` and `TouchFocus`-types to implement +the new `DndFocus` trait. + +`GbmFramebufferExporter::new` now accepts a `NodeFilter` as import node, which +enables accepting dmabufs originating from any node. + +```diff +-GbmFramebufferExporter::new(gbm, node); ++GbmFramebufferExporter::new(gbm, node.into()); + +-GbmFramebufferExporter::new(gbm, None); ++GbmFramebufferExporter::new(gbm, NodeFilter::None); + +-GbmFramebufferExporter::new(gbm, Some(node)); ++GbmFramebufferExporter::new(gbm, node.into()); + ++GbmFramebufferExporter::new(gbm, NodeFilter::All); +``` + +`DrmOutputManager` does now provide an explicit `lock`-method returning a [`LockedDrmOutputManager`]. +All other methods have moved to this new type, allowing compositors to take the same lock across multiple +operations. + +xdg_shell, layer_shell, and session_lock surfaces now track `last_acked` state for each commit. +You can find it in the `last_acked` field of the corresponding `...CachedState` structs. + +The "current state" and "current serial" attribute fields were removed; use `last_acked` from the new `with_committed_state()` accessors instead (and verify that you didn't actually want `with_pending_state()` instead where you used the "current state"). + +```diff +-ToplevelSurface::current_state(); ++ToplevelSurface::with_committed_state(|state| { ++ // ... ++}); + +-PopupSurface::current_state(); ++PopupSurface::with_committed_state(|state| { ++ // ... ++}); + +-LayerSurface::current_state(); ++LayerSurface::with_committed_state(|state| { ++ // ... ++}); + +-LockSurface::current_state(); ++LockSurface::with_committed_state(|state| { ++ // ... ++}); + +struct XdgToplevelSurfaceRoleAttributes { +- configured: bool, +- configure_serial: Option, +- current: ToplevelState, +- current_serial: Option, +- last_acked: Option, ++ last_acked: Option, + // ... +} + +struct XdgPopupSurfaceRoleAttributes { +- configured: bool, +- configure_serial: Option, +- current: PopupState, +- current_serial: Option, +- committed: bool, +- last_acked: Option, ++ last_acked: Option, + // ... +} + +struct LayerSurfaceAttributes { +- configured: bool, +- configure_serial: Option, +- current: LayerSurfaceState, +- last_acked: Option, ++ last_acked: Option, + // ... +} + +-impl Copy for LayerSurfaceCachedState; +``` + +The following methods are no longer needed as Smithay does them automatically now: +```diff +-ToplevelSurface::reset_initial_configure_sent(); +-PopupSurface::reset_initial_configure_sent(); +``` + +You also no longer need to manually set `LayerSurfaceAttributes::initial_configure_sent`, Smithay handles it automatically. + +The `delegate_*!` macros have been replaced with a single `delegate_dispatch2!` macro, which implements dispatch in terms +of new `Dipsatch2` and `GlobalDispatch2` traits. These will replace `Dispatch` and `GlobalDispatch` in a future version of +`wayland_server`. + +The `SpaceElement::geometry` impl for `X11Surface` is no longer equivalent to its bbox. For clients that set +`_GTK_FRAME_EXTENTS`, the shadow extents are subtracted from the bbox to give the geometry. + +`X11Surface::geometry` has been renamed to `X11Surface::last_configure`. `X11Surface::geometry` now returns the bounding +box minus the surface's frame extents. + +`SessionLockHandler::unlock` will no longer be called if the `ext_session_lock_v1` instance that sent the +`unlock_and_destroy` request is not the same instance as the one that successfully locked the session. + +`SessionLockHandler::new_surface` can be called for multiple `SessionLocker` instances that could be in-flight at the same +time. `LockSurface::ext_session_lock` has been added to allow the compositor to disambiguate, and associate lock surfaces +with the correct lock instance. + +`backend::input` and `input` now use `backend::input::InputTime` for timestamps instead of `u32` or `u64`. `InputTime::now()` +is used for timestamps for synthesized events. + +`TextInputActivation` must be implemented. It allows for hooks to be run when a text input is `activated` and `deactivated`. + +`KeyboardHandle::input_intercept()` now has a `source` argument and early-returns `None` when the key was already pressed/released, matching the behavior of `KeyboardHandle::input()`. + +```diff +-let (res, mods_changed) = keyboard.input_intercept(data, keycode, state, filter); ++let Some((res, mods_changed)) = keyboard.input_intercept( ++ KeyboardSource::MAIN, ++ data, ++ keycode, ++ state, ++ filter, ++) else { ++ return; ++}; +``` + +### Additions + +- Add `WmWindowProperty::Other` to forward unrecognized X11 property changes to the compositor. + + +- The wlr virtual pointer protocol is now available in `smithay::wayland::virtual_pointer`. + +- `Xkb::keymap_as_string` returns the active keymap as `XKB_KEYMAP_FORMAT_TEXT_V1`. + +- `XkbContext::set_modifier_mask` applies raw xkb modifier masks to a keyboard + for clients that report modifier state directly instead of through key presses + (e.g., `zwp_virtual_keyboard_v1.modifiers`). + +- `KeyboardHandle::input_forward_bypassing_grab` sends a key skipping the active + grab, which can be used by compositors to prevent infinite loops when a client + with a grab forwards a key through its own virtual keyboard, which can be + tracked with `InputMethodHandle::keyboard_grab_client` and + `VirtualKeyboardDevice::client`. + +- `VirtualKeyboardDevice::source` gives each virtual keyboard its own + `KeyboardSource` so keys pressed on multiple devices are released when the + last one is released. + +- ExtBackgroundEffect protocol is now available in `smithay::wayland::background_effect` module. + +`crate::input::dnd` was introduced to enable implementation of Drag&Drop operations on custom types. +Internally the same types and traits are used to implement `wayland::data_device` dnd-operations and XDND +operations (see below). + +`DnDGrab` is the new entry-point for DnD operations. It requires `SeatHandler::PointerFocus` or +`SeatHandler::TouchFocus` respectively to implement `DndFocus`, which is used to send events to +types, which can be targets of DnD operations. `Source` is a new trait for types that represent +sources of data for DnD operations. + +`Source::accepted` is called when a target accepts a mime-type, or rejects the offer with `None`. +Sources that proxy an upstream offer need it: a nested compositor forwarding a drag to its own +clients only receives `drop` from the host compositor once it has itself accepted on the upstream +offer, and previously had no way to know when a nested client had done so. It has a default no-op +implementation, so sources that originate their own data are unaffected. + +The Xwayland WM can now handle XDND operations and bridge them over to the new generic DnD interface +allowing DnD operations between X11 and Wayland clients (both directions). + +`X11Surface` now has a new `surface_under`-method, which is also internally used by `SpaceElement::is_in_input_region` and `crate::desktop::Window::surface_under`. Any direct usage of `under_from_surface_tree` on the underlying `wl_surface` of an `X11Surface` should be replaced with this method for XDND to work correctly. + +`X11Surface` now parses and exposes the Motif WM hints via `motif_hints()`. + +`X11Wm` allows you to remove and clear XSETTINGS settings. + +xdg_shell and layer_shell now enforce the client acking a configure before committing a buffer, as required by the protocols. + +```rs +struct ToplevelCachedState { + /// Configure last acknowledged by the client at the time of the commit. + last_acked: Option, +} + +/// Provides access to the current committed cached state. +fn ToplevelSurface::with_cached_state(&self, f: impl FnOnce(&ToplevelCachedState) -> T) -> T; +/// Provides access to the current committed state. +fn ToplevelSurface::with_committed_state(&self, f: impl FnOnce(Option<&ToplevelState>) -> T) -> T; + +struct PopupCachedState { + /// Configure last acknowledged by the client at the time of the commit. + last_acked: Option, +} + +/// Provides access to the current committed cached state. +fn PopupSurface::with_cached_state(&self, f: impl FnOnce(&PopupCachedState) -> T) -> T; +/// Provides access to the current committed state. +fn PopupSurface::with_committed_state(&self, f: impl FnOnce(Option<&PopupState>) -> T) -> T; + +struct LayerSurfaceCachedState { + /// Configure last acknowledged by the client at the time of the commit. + last_acked: Option, + // ... +} + +/// Provides access to the current committed cached state. +fn LayerSurface::with_cached_state(&self, f: impl FnOnce(&LayerSurfaceCachedState) -> T) -> T; +/// Provides access to the current committed state. +fn LayerSurface::with_committed_state(&self, f: impl FnOnce(Option<&LayerSurfaceState>) -> T) -> T; + +struct LockSurfaceCachedState { + /// Configure last acknowledged by the client at the time of the commit. + last_acked: Option, +} + +/// Provides access to the current committed cached state. +fn LockSurface::with_cached_state(&self, f: impl FnOnce(&LockSurfaceCachedState) -> T) -> T; +/// Provides access to the current committed state. +fn LockSurface::with_committed_state(&self, f: impl FnOnce(Option<&LockSurfaceState>) -> T) -> T; + +struct LockSurfaceAttributes { + server_pending: Option, + pending_configures: Vec, + last_acked: Option, +} + +type LockSurfaceData = Mutex; + +struct LockSurfaceConfigure { + state: LockSurfaceState, + serial: Serial, +} +``` + +Added `Element::is_framebuffer_effect` and `RenderElement::capture_framebuffer` to better support render elements, +that take the current framebuffers contents and modify them, e.g. blurring the backgrounds of windows. +See the documentation for these functions for how to make use of them, but note, that they provide +default implementations, which result in skipping the new functionality. As such any custom `RenderElements` wrappers +(not using `crate::render_elements!`) need to be updated. + +`Space` has new functionality that allows for more fine-grained control over window stacking: + +- `map_element_above()`: maps a new element directly above an existing one. +- `raise_element_above()`: same as above, but moves an already-mapped element. +- `lower_element()`: lowers an element to the bottom of the stack, respecting its z-index group. +- `relocate_element()`: moves an element to a new location in the space without changing the stacking order. + +`Output` now has `owns_xdg_output()` which allows you to match an XDG output protocol object with an `Output`. + +Added `Renderer::invalidate_caches`, which unconditionally drops all renderer-internal caches, +including entries `Renderer::cleanup_texture_cache` retains while their source buffers are still +alive. + +Added `GpuManager::cleanup_texture_cache` and `GpuManager::invalidate_caches`, which apply the +corresponding `Renderer` method to every enumerated device. `GpuManager::invalidate_caches` +additionally drops the buffers cached for copying between a render and a target node, which no +`Renderer` owns. + +### Bugfixes + +`input_method_v2`: a second input method binding a seat that already has one is now the object told +it is `unavailable`, per the protocol, leaving the existing instance in place. Previously the event +went to the existing instance and the newcomer was dropped untracked. Destroying such an inert +newcomer no longer clears the active instance, and its requests no longer drive the seat. + +`DrmSurface::use_vrr(false)` now succeeds as a no-op when using legacy DRM. Attempts to enable VRR +on legacy DRM still return `UnknownProperty`. + +The `xdg_toplevel` request handler now posts an `invalid_size` error when a client performs a `set_max_size` or `set_min_size` request with a negative width or height. + +`DrmDeviceFd::new` no longer tries to become DRM master on a render node, which the kernel always +refuses, so it no longer warns about it. + +`SimpleCrtcMapper` (in `smithay-drm-extras`) now releases the CRTC reservation of any connector that +is no longer connected, including connectors that have disappeared from the resource list entirely +rather than being reported as disconnected. Previously such connectors (for example DP-MST sink +connectors when a dock is unplugged, or across suspend/resume) leaked their CRTC reservation +indefinitely, which could accumulate until a newly connected output could no longer be assigned a +CRTC. + +XWayland now honors WM_HINTS request to map the window in IconicState(minimized). Compositors should +check X11Surface::is_hidden() at mapping time and minimize the window appropriately. + +Space::refresh() now clears stale outputs before emitting any enter events so no wl_surface.enter events +are emitted with a stale wl_output anymore. + +The XWayland WM implementation previously incorrectly prefixed large transfers from X11 windows with +the four INCR bytes. + +`MultiRenderer::cleanup_texture_cache` now also cleans up the devices other than the render and +target device. Buffers that cannot be imported on the render node directly are imported on their +source device, so those devices accumulate cached imports that previously were never released. +Cleanup is now also attempted on every device even if it fails on one of them, with every failure +logged and the first error returned. + +`send_frames_surface_tree` (and `Window::send_frame`, `LayerSurface::send_frame`) no longer sends frame +callbacks to surfaces whose renderer state has no buffer, or to their subsurfaces. + +`GlesRenderer::cleanup_texture_cache` and `GlesRenderer::invalidate_caches` now only make their EGL +context current when resources are queued for destruction. This avoids waking idle GPUs and +blocking the compositor while they resume. + +`ext_image_copy_capture_session_v1` now raises the `duplicate_frame` protocol error when a client +calls `create_frame` while the session still has an active frame, as the protocol requires. Previously +the request was accepted and the extra frame was silently added to the session. + +## 0.7.0 + +### Breaking changes +`DrmSyncobjHandler::drm_syncobj_state` is now Optional. +Whenever state is None `ImportTimeline` request will result in `InvalidTimeline` protocol error. +This allows one to destroy DrmSyncobjState by calling `DrmSyncobjState::into_global()` +```diff +-fn DrmSyncobjHandler::drm_syncobj_state(&mut self) -> &mut DrmSyncobjState ++fn DrmSyncobjHandler::drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState> +``` + +`DrmTimeline` constructor now accepts OwnedFd: +```diff +-fn DrmTimeline::new(device: &DrmDeviceFd, fd: BorrowedFd<'_>) -> io::Result ++fn DrmTimeline::new(device: &DrmDeviceFd, fd: OwnedFd) -> io::Result +``` +`DrmSyncPoint::eventfd` now returns `Arc`: +```diff +-fn DrmSyncPoint::eventfd(&self) -> io::Result ++fn DrmSyncPoint::eventfd(&self) -> io::Result> +``` +`GbmFramebufferExporter` constructor now accepts `import_node`, +it will be used to filter DMA-BUFs to only those originating from a specific device before considering them for direct scanout. +If `import_node` is `None` direct-scanout of client-buffers won't be used. +```diff +-fn GbmFramebufferExporter::new(gbm: Device) -> Self ++fn GbmFramebufferExporter::new(gbm: Device, import_node: Option) -> Self +``` + +### Additions + +#### WeakDrmDeviceFd +```rs +/// Returns a weak reference to the underlying device +fn DrmDeviceFd::downgrade(&self) -> WeakDrmDeviceFd; +/// Construct an empty Weak reference, that will never upgrade successfully +fn WeakDrmDeviceFd::new() -> Self; +/// Try to upgrade to a strong reference +fn WeakDrmDeviceFd::upgrade(&self) -> Option; +``` + +#### KeyboardHandle::set_modifier_state +```rs +/// Set the modifiers state. +fn KeyboardHandle::set_modifier_state(&self, mods_state: ModifiersState) -> u32; +/// Serialize modifier state back to be sent to xkb. +fn ModifiersState::serialize_back(&self, state: &xkb::State) -> SerializedMods; +``` + +#### XWaylandKeyboardGrab +```rs +/// Get the `zwp_xwayland_keyboard_grab_v1` object that created the grab +fn XWaylandKeyboardGrab::grab(&self) -> &ZwpXwaylandKeyboardGrabV1; +/// Grab is now cloneable +impl Clone for XWaylandKeyboardGrab; +``` + +#### X11Surface +```rs +/// Returns the hints for the underlying X11 window +fn X11Surface::hints(&self) -> Option; +/// Get the client PID associated with the X11 window. +fn X11Surface::get_client_pid(&self) -> Result>; +``` + +#### DrmSyncobjState +```rs +/// Destroys the state and returns the `GlobalId` for compositors to disable/destroy. +fn DrmSyncobjState::into_global(self) -> GlobalId; +/// Sets a new `import_device` to import the syncobj fds and wait on them. +fn DrmSyncobjState::update_device(&mut self, import_device: DrmDeviceFd); +``` + +#### xdg-toplevel-tag-v1 +XdgToplevelTag protocol is now available in `smithay::wayland::xdg_toplevel_tag` module. + +#### cursor-shape-v1 +CursorShape is now updated to version `2` + + +## 0.6.0 + +### Breaking Changes + +`RenderContext::draw` callback now accepts a mutable reference +```diff +-fn smithay::backend::renderer::element::texture::RenderContext::draw(&mut self, f: impl FnOnce(&T)) ++fn smithay::backend::renderer::element::texture::RenderContext::draw(&mut self, f: impl FnOnce(&mut T)) +``` + +Framebuffer now requires `Texture` implementation +```rs +type smithay::backend::renderer::RendererSuper::Framebuffer: smithay::backend::renderer::Texture +``` + +`Output::client_outputs` no longer returns a Vec +```diff +-fn smithay::output::Output::client_outputs(&self, client: &Client) -> Vec; ++fn smithay::output::Output::client_outputs(&self, client: &Client) -> impl Iterator; +``` +DamageBag/DamageSnapshot damage getters got renamed +```diff +-fn smithay::backend::renderer::utils::DamageBag::damage(&self) -> impl Iterator> ++fn smithay::backend::renderer::utils::DamageBag::raw(&self) -> impl Iterator> +-fn smithay::backend::renderer::utils::DamageSnapshot::damage(&self) -> impl Iterator> ++fn smithay::backend::renderer::utils::DamageSnapshot::raw(&self) -> impl Iterator> +``` +RendererSurfaceState::damage now returns a DamageSnapshot +```diff +-fn smithay::backend::renderer::utils::RendererSurfaceState::damage(&self) -> impl core::iter::traits::iterator::Iterator>> ++fn smithay::backend::renderer::utils::RendererSurfaceState::damage(&self) -> smithay::backend::renderer::utils::DamageSnapshot +``` +Client scale can now be fractional +```diff +-fn smithay::wayland::compositor::CompositorClientState::client_scale(&self) -> u32 ++fn smithay::wayland::compositor::CompositorClientState::client_scale(&self) -> f64 +-fn smithay::wayland::compositor::CompositorClientState::set_client_scale(&self, new_scale: u32) ++fn smithay::wayland::compositor::CompositorClientState::set_client_scale(&self, new_scale: f64) +``` + +Raw `renderer_id` got replaced with new `smithay::backend::renderer::ContextId` newtype +```diff +-fn smithay::backend::renderer::gles::GlesFrame::id(&self) -> usize; ++fn smithay::backend::renderer::gles::GlesFrame::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::gles::GlesRenderer::id(&self) -> usize; ++fn smithay::backend::renderer::gles::GlesRenderer::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::glow::GlowFrame::id(&self) -> usize; ++fn smithay::backend::renderer::glow::GlowFrame::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::glow::GlowRenderer::id(&self) -> usize; ++fn smithay::backend::renderer::glow::GlowRenderer::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::multigpu::MultiFrame::id(&self) -> usize; ++fn smithay::backend::renderer::multigpu::MultiFrame::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::multigpu::MultiRenderer::id(&self) -> usize; ++fn smithay::backend::renderer::multigpu::MultiRenderer::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::pixman::PixmanFrame::id(&self) -> usize; ++fn smithay::backend::renderer::pixman::PixmanFrame::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::pixman::PixmanRenderer::id(&self) -> usize; ++fn smithay::backend::renderer::pixman::PixmanRenderer::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::test::DummyFrame::id(&self) -> usize; ++fn smithay::backend::renderer::test::DummyFrame::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::test::DummyRenderer::id(&self) -> usize; ++fn smithay::backend::renderer::test::DummyRenderer::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::Frame::id(&self) -> usize; ++fn smithay::backend::renderer::Frame::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::Renderer::id(&self) -> usize; ++fn smithay::backend::renderer::Renderer::context_id(&self) -> ContextId; +-fn smithay::backend::renderer::element::texture::TextureRenderElement::from_static_texture(id: Id, renderer_id: usize, ...) -> Self; ++fn smithay::backend::renderer::element::texture::TextureRenderElement::from_static_texture(id: Id, context_id: ContextId, ...) -> Self; +-fn smithay::backend::renderer::element::texture::TextureRenderElement::from_texture_with_damage(id: Id, renderer_id: usize, ...) -> Self; ++fn smithay::backend::renderer::element::texture::TextureRenderElement::from_texture_with_damage(id: Id, context_id: ContextId, ...) -> Self; +-fn smithay::backend::renderer::utils::RendererSurfaceState::texture(&self, id: usize) -> Option<&TextureId>; ++fn smithay::backend::renderer::utils::RendererSurfaceState::texture(&self, id: &ContextId) -> Option<&TextureId>; +``` + +`CursorShapeDeviceUserData` now has an additional generic argument +```diff +-struct smithay::wayland::cursor_shape::CursorShapeDeviceUserData; ++struct smithay::wayland::cursor_shape::CursorShapeDeviceUserData; +``` + +The explicit frame buffers got introduced, but for the sake of my sanity those changes are not described here, you can look at: https://github.com/Smithay/smithay/commit/df08c6f29eb6ebfa2fce6fc374590483bcbaf21a + +### API Additions + +It is now possible to check if the OpenGL context is shared with another. +```rs +fn smithay::backend::egl::context::EGLContext::is_shared(); +``` +It is now possible to check that there are no other references to the underlying GL texture. +```rs +fn smithay::backend::renderer::gles::GlesTexture::is_unique_reference(); +``` + +There is a new gles capability for support of fencing and exporting to EGL +```rs +smithay::backend::renderer::gles::Capability::ExportFence; +``` + +There is a new BlitFrame trait for frames that support blitting contents from/to the current framebuffer to/from another. +```rs +trait smithay::backend::renderer::BlitFrame; +impl BlitFrame for smithay::backend::renderer::gles::GlesFrame; +impl BlitFrame for smithay::backend::renderer::glow::GlowFrame; +impl BlitFrame for smithay::backend::renderer::multigpu::MultiFrame; +``` + +It is now possible to iterate over all known tokens and their associated data +```rs +fn smithay::wayland::xdg_activation::XdgActivationState::tokens() -> impl Iterator; +``` + +There are new errors for missing DRM crtc/connector/plane mapping +```rs +smithay::backend::drm::DrmError::{UnknownConnector, UnknownCrtc, UnknownPlane}; +``` + +Texture has a few new implementations +```rs +impl Texture for smithay::backend::renderer::gles::GlesTarget; +impl Texture for smithay::backend::renderer::multigpu:MultiFramebuffer; +impl Texture for smithay::backend::renderer::pixman::PixmanTarget; +impl Texture for smithay::backend::renderer::test::DummyFramebuffer +``` + +It is now possible to access WlKeyboard/WlPointer instances +```rs +fn smithay::input::keyboard::KeyboardHandle::client_keyboards(&self, client: &Client) -> impl Iterator; +fn smithay::input::pointer::PointerHandle::client_pointers(&self, client: &:Client) -> impl Iterator; +``` + +New APIs for X11 randr output management +```rs +enum smithay::xwayland::xwm::PrimaryOutputError { OutputUnknown, X11Error(x11rb::errors::ReplyError) }; + +impl From for smithay::xwayland::xwm::PrimaryOutputError; +fn smithay::xwayland::xwm::PrimaryOutputError::from(value: x11rb::errors::ConnectionError) -> Self; +fn smithay::xwayland::xwm::X11Wm::get_randr_primary_output(&self) -> Result, x11rb::errors::ReplyError>; +fn smithay::xwayland::xwm::X11Wm::set_randr_primary_output(&mut self, output: Option<&smithay::output::Output>) -> Result<(), smithay::xwayland::xwm::PrimaryOutputError>; +fn smithay::xwayland::xwm::XwmHandler::randr_primary_output_change(&mut self, xwm: smithay::xwayland::xwm::XwmId, output_name: Option); +``` + +It is now possible to get the DrmNode of the device the buffer was allocated on +```rs +fn smithay::backend::allocator::gbm::GbmBuffer::device_node(&self) -> Option; +``` + +It is now possible to create a `GbmBuffer` from an existing `BufferObject` explicitly defining the device node +```rs +fn smithay::backend::allocator::gbm::GbmBuffer::from_bo_with_node(bo: gbm::buffer_object::BufferObject<()>, implicit: bool, drm_node: core::option::Option) -> Self; +``` + +It is now possible to access the `Allocator` of this output manager +```rs +fn smithay::backend::drm::output::DrmOutputManager::allocator(&self) -> &Allocator; +``` + +Is is now possible to check if EGLDevice is backed by actual device node or is it a software device. +```rs +fn smithay::backend::egl::EGLDevice::is_software(&self) -> bool +``` + +This adds a way to query next deadline of a commit timing barrier. +Allows a compositor to schedule re-evaluating commit timers without +busy looping. +```rs +fn smithay::wayland::commit_timing::CommitTimerBarrierState::next_deadline(&self) -> Option; +``` + +Support for casting `Timestamp` back to `Time` +This might be useful to compare the next deadline with a monotonic time +from the presentation clock +```rs +impl From for smithay::utils::Time; +``` + +Support for creating a weak reference to a `Seat` +```rs +fn smithay::input::Seat::downgrade(&self) -> smithay::input::WeakSeat; + +fn smithay::input::WeakSeat::is_alive(&self) -> bool; +pub fn smithay::input::WeakSeat::upgrade(&self) -> Option; +``` + +## 0.5.0 + +### API Changes +Items either removed or deprecated from the public API +```rs +/// Use Clone instead +impl Copy for smithay::utils::HookId; +/// Use `from_extremities` instead +fn smithay::utils::Rectangle::from_extemities(topleft, bottomright) -> Self; +``` + +Items added to the public API +```rs +/// Replaces deprecated `from_extemities` +fn smithay::utils::Rectangle::from_extremities(topleft, bottomright) -> Self; +/// Access the active text-input instance for the currently focused surface. +fn smithay::wayland::text_input::TextInputHandle::with_active_text_input(&self, f); +/// Just a new protocol +mod smithay::wayland::selection::ext_data_control; +``` + +Items changed in the public API +```diff +# create_external_token now accepts `XdgActivationTokenData` instead of `String` +-fn smithay::wayland::xdg_activation::XdgActivationState::create_external_token(&mut self, app_id: impl Into>); ++fn smithay::wayland::xdg_activation::XdgActivationState::create_external_token(&mut self, data: impl Into>); +``` + +### New protocols +* Introduce ext data control protocol by @PolyMeilex in https://github.com/Smithay/smithay/pull/1577 +* Update idle notify to version 2 by @PolyMeilex in https://github.com/Smithay/smithay/pull/1618 + +### TextInput improvements +* Revert "Fix repeated key input issue in Chrome with multiple windows" by @Drakulix in https://github.com/Smithay/smithay/pull/1647 +* text-input: fix active instance tracking by @kchibisov in https://github.com/Smithay/smithay/pull/1648 +* text-input: properly handle double buffered state by @kchibisov in https://github.com/Smithay/smithay/pull/1649 + +### Miscellaneous +* clock: Fix current monotonic time in millis u32 overflow panic by @YaLTeR in https://github.com/Smithay/smithay/pull/1645 +* xwm: Update override-redirect flag on map request by @Ottatop in https://github.com/Smithay/smithay/pull/1656 +* utils: Rework `HookId` recycle logic by @Paraworker in https://github.com/Smithay/smithay/pull/1657 +* rename Rectangle::from_extemities to Rectangle::from_extremities by @m4rch3n1ng in https://github.com/Smithay/smithay/pull/1646 +* xdg_activation: Allow passing all data in XdgActivationState::create_external_token by @bbb651 in https://github.com/Smithay/smithay/pull/1658 + +## 0.4.0 + +### Breaking Changes + +**`wayland-server` was updated to 0.30:** +- Most of the wayland frontend API is changed to follow the new request dispatching mechanism built around the `Dispatch` trait from `wayland-server` +- Modules that provide handlers for Wayland globals now provide `DelegateDispatch` implementations, as well as macros to simplify the dispatching from your main state + +#### Clients & Protocols + +- Remove `xdg-shell-unstable-v6` backwards compatibility +- `XdgPositionerState` moved to `XdgPopupState` and added to `XdgRequest::NewPopup` +- `PopupSurface::send_configure` now checks the protocol version and returns an `Result` +- `KeyboardHandle::input` filter closure now receives a `KeysymHandle` instead of a `Keysym` and returns a `FilterResult`. +- `PointerButtonEvent::button` now returns an `Option`. +- `MouseButton` is now non-exhaustive. +- Remove `Other` and add `Forward` and `Back` variants to `MouseButton`. Use the new `PointerButtonEvent::button_code` in place of `Other`. +- `GrabStartData` has been renamed to `PointerGrabStartData` +- The `slot` method on touch events no longer returns an `Option` and multi-touch capability is thus opaque to the compositor +- `wayland::output::Output` now is created separately from it's `Global` as reflected by [`Output::new`] and the new [`Output::create_global] method. +- `PointerHandle` no longer sends an implicit motion event when a grab is set, `time` has been replaced by an explicit `focus` parameter in [`PointerHandle::set_grab`] +- `ToplevelSurface::send_configure`/`PopupSurface::send_configure`/`LayerSurface::send_configure` now always send a configure event regardless of changes and return + the serial of the configure event. `send_pending_configure` can be used to only send a configure event on pending changes. + +#### Backends + +- Rename `WinitInputBacked` to `WinitEventLoop`. +- Rename `WinitInputError` to `WinitError`; +- `WinitInputBackend` no longer implements `InputBackend`. Input events are now received from the `WinitEvent::Input` variant. +- All winit backend internal event types now use `WinitInput` as the backend type. +- `WinitEventLoop::dispatch_new_events` is now used to receive some `WinitEvent`s. +- Added `TabletToolType::Unknown` as an option for tablet events +- `render_texture` was removed from `Frame`, use `render_texture_at` or `render_texture_from_to` instead or use `Gles2Renderer::render_texture` as a direct replacement. +- Remove `InputBackend::dispatch_new_events`, turning `InputBackend` into a definition of backend event types. Future input backends should be a `calloop::EventSource`. +- Remove `InputBackend::EventError` associated type as it is unneeded since `dispatch_new_events` was removed. +- `Swapchain` does not have a generic Userdata-parameter anymore, but utilizes `UserDataMap` instead +- `GbmBufferedSurface::next_buffer` now additionally returns the age of the buffer +- `Present` was merged into the `X11Surface` +- `X11Surface::buffer` now additionally returns the age of the buffer +- `X11Surface` now has an explicit `submit` function +- `X11Surface` is now multi-window capable. +- `Renderer::clear` now expects a second argument to optionally only clear parts of the buffer/surface +- `Transform::transform_size` now takes a `Size` instead of two `u32` +- `Gles2Renderer` now automatically flips the `render` result to account for OpenGLs coordinate system +- `Frame::clear`, `Frame::render_texture_at` and `Frame::render_texture_from_to` now have an additional damage argument +- `EGLNativeSurface` implementations overriding `swap_buffers` now receive and additional `damage` attribute to be used with `eglSwapBuffersWithDamageEXT` if desired +- `EGLSurface::swap_buffers` now accepts an optional `damage` parameter +- `WinitGraphicsBackend` does no longer provide a `render`-method and exposes its `Renderer` directly instead including new functions `bind` and `submit` to handle swapping buffers. +- `ImportShm` was renamed to `ImportMem` +- `ImportMem` and `ImportDma` were split and do now have accompanying traits `ImportMemWl` and `ImportDmaWl` to import wayland buffers. +- Added `EGLSurface::get_size` +- `EGLDisplay::get_extensions` was renamed to `extensions` and now returns a `&[String]`. +- Added gesture input events, which are supported with the libinput backend. + +### Additions + +#### Clients & Protocols + +- `xdg_activation_v1` support +- `wlr-layer-shell-unstable-v1` support +- Added public api constants for the roles of `wl_shell_surface`, `zxdg_toplevel` and `xdg_toplevel`. See the + `shell::legacy` and `shell::xdg` modules for these constants. +- Whether a surface is toplevel equivalent can be determined with the new function `shell::is_toplevel_equivalent`. +- Setting the parent of a toplevel surface is now possible with the `xdg::ToplevelSurface::set_parent` function. +- Add support for the zxdg-foreign-v2 protocol. +- Support for `xdg_wm_base` protocol version 3 +- Added the option to initialize the dmabuf global with a client filter +- `wayland::output::Output` now has user data attached to it and more functions to query its properties +- Added a `KeyboardGrab` similar to the existing `PointerGrab` +- `wayland::output::Output` now has a `current_scale` method to quickly retrieve its set scale. +- `wayland::shell::wlr_layer::KeyboardInteractivity` now implements `PartialEq` and `Eq`. +- Added `TouchHandle` for Wayland client touch support (see `Seat::get_touch`) +- `wayland::output::Scale` was introduced to handle fractional scale values better +- Support for `wl_output` global version 4 +- Support for `wl_seat` global version 7 +- Support for `wl_compositor` global version 5 +- Support for the `wp_viewporter` protocol +- Support for the `zwp_input_method_v2` protocol +- Support for the `zwp_text_input_v3` protocol + +#### Backends + +- New `x11` backend to run the compositor as an X11 client. Enabled through the `backend_x11` feature. +- `x11rb` event source integration used in anvil's XWayland implementation is now part of smithay at `utils::x11rb`. Enabled through the `x11rb_event_source` feature. +- `KeyState`, `MouseButton`, `ButtonState` and `Axis` in `backend::input` now derive `Hash`. +- New `DrmNode` type in drm backend. This is primarily for use a backend which needs to run as client inside another session. +- The button code for a `PointerButtonEvent` may now be obtained using `PointerButtonEvent::button_code`. +- `Renderer` now allows texture filtering methods to be set. +- `backend::renderer` has a new `utils`-module that can take care of client buffer management for you. +- `EGLSurface::buffer_age` can be used to query the surface buffer age. +- `GbmBufferedSurface::reset_buffers` can now be used to reset underlying buffers. +- Added new `Offscreen` trait to create offscreen surfaces for `Renderer`s +- Added functions to `ImportMem` to upload bitmaps from memory +- Added `ExportDma` trait to export framebuffers and textures into dmabufs +- Added `ExportMem` trait to copy framebuffers and textures into memory +- Added `multigpu`-module to the renderer, which makes handling multi-gpu setups easier! +- Added `backend::renderer::utils::import_surface_tree` to be able to import buffers before rendering +- Added `EGLContext::display` to allow getting the underlying display of some context. +- Make `EGLContext::dmabuf_render_formats` and `EGLContext::dmabuf_texture_formats` also accessible from `EGLDisplay`. + +#### Desktop + +- New `desktop` module to handle window placement, tracks popups, layer surface and various rendering helpers including automatic damage-tracking! (+so much more) + +#### Utils + +- `Rectangle` can now also be converted from f64 to i32 variants +- `Rectangle::contains_rect` can be used to check if a rectangle is contained within another +- `Coordinate` is now part of the public api, so it can be used for coordinate agnositic functions outside of the utils module or even out-of-tree + +### Bugfixes + +#### Clients & Protocols + +- `Multicache::has()` now correctly does what is expected of it +- `xdg_shell` had an issue where it was possible that configured state gets overwritten before it was acked/committed. +- `wl_keyboard` rewind the `keymap` file before passing it to the client +- `wl_shm` properly validates parameters when creating a `wl_buffer`. +- `ServerDnDGrab` and `DnDGrab` now correctly send data device `leave` event on button release +- Client are now allowed to reassign the same role to a surface +- `xdg_output` now applies the output transforms to the reported logical size + +#### Backends + +- EGLBufferReader now checks if buffers are alive before using them. +- LibSeat no longer panics on seat disable event. +- X11 backend will report an error when trying to present a dmabuf fails. + +### Anvil + +- Anvil now implements the x11 backend in smithay. Run by passing `--x11` into the arguments when launching. +- Passing `ANVIL_MUTEX_LOG` in environment variables now uses the slower `Mutex` logging drain. +- Only toplevel surfaces now get implicit keyboard focus +- Fix popup drawing for fullscreen windows + +## version 0.3.0 (2021-07-25) + +Large parts of Smithay were changed with numerous API changes. It is thus recommended to +approach version 0.3 as if it was a new crate altogether compared to 0.2. + +The most notable changes are: + +- Deep refactor of the graphics backends around a workflows centered on allocating graphics buffers, + and a Gles2-based renderer abstraction is provided. +- Support for DRM atomic modesetting as well as client-provided DMABUF +- Most backends are now `calloop` event sources generating events. The recommended organization for + your smithay-based compositor is thus to centralize most of your logic on a global state struct, + and delegate event handling to it via the shared data mechanism of `calloop`. Most of the callbacks + you provide to Smithay are given mutable access to this shared data. +- The `wayland::compositor` handling logic now automatically handles state tracking and delayed commit + for wayland surfaces. + +Many thanks to the new contributors to Smithay, who contributed the following: + +- Support for [`libseat`](https://sr.ht/~kennylevinsen/seatd/) as a session backend, by + @PolyMeilex +- Support for graphics tablets via the `tablet` protocol extension, by @PolyMeilex +- Support for running Smithay on `aarch64` architectures, by @cmeissl +- A rework of the `xdg-shell` handlers to better fit the protocol logic and correctly track configure + events, by @cmeissl +- Basic Xwayland support, by @psychon + +## version 0.2.0 (2019-01-03) + +### General + +- **[Breaking]** Upgrade to wayland-rs 0.21 +- **[Breaking]** Moving the public dependencies to a `reexports` module +- Migrate the codebase to Rust 2018 + +### Backends + +- **[Breaking]** WinitBackend: Upgrade to winit 0.18 +- **[Breaking]** Global refactor of the DRM & Session backends +- **[Breaking]** Restructuration of the backends around the `calloop` event-loop + +### Clients & Protocol + +- Basic XWayland support +- Data device & Drag'n'Drop support +- Custom client pointers support + +## version 0.1.0 (2017-10-01) + +### Protocol handling + +- Low-level handling routines for several wayland globals: + - `wayland::shm` handles `wl_shm` + - `wayland::compositor` handles `wl_compositor` and `wl_subcompositor` + - `wayland::shell` handles `wl_shell` and `xdg_shell` + - `wayland::seat` handles `wl_seat` + - `wayland::output` handles `wl_output` + +### Backend + +- Winit backend (EGL context & input) +- DRM backend +- libinput backend +- glium integration diff --git a/vendor/smithay/CONTRIBUTING.md b/vendor/smithay/CONTRIBUTING.md new file mode 100644 index 0000000..78071f8 --- /dev/null +++ b/vendor/smithay/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing + +Smithay is open to contributions from anyone. Here are a few tips to get started if you want to participate. + +## Coordination + +Most discussion about features and their implementations takes place on github. +If you have questions, suggestions, ideas, you can open an issue to discuss it, or add your message in an already existing issue +if it fits its scope. + +If you want a more realtime discussion I (@vberger) have a Matrix room dedicated to Smithay and +my other wayland crates: [#smithay:matrix.org](https://matrix.to/#/#smithay:matrix.org). If you don't want to +use matrix, this room is also bridged to libera.chat IRC on #smithay. + +## Scope + +Smithay attempts to be as generic and un-opinionated as possible. As such, if you have an idea of a feature that would be useful +for your compositor project and would like it to be integrated in Smithay, please consider whether it is in its scope: + +- If this is a very generic feature that probably many different projects would find useful, it can be integrated in Smithay +- If it is a rather specific feature, but can be framed as a special case of a more general feature, this general feature is + likely worth adding to Smithay +- If this feature is really specific to your use-case, it is out of scope for Smithay + +## Structure + +Smithay aims to be a modular hierarchical library: + +- Functionalities should be split into independent modules as much as possible +- There can be dependencies in functionalities +- Even if most people would directly use a high-level functionality, the lower level abstractions it is built on should + still be exposed independently if possible + +The goal is for Smithay to be a "use what you want" library, and features that are not used should have no impact on the +application built with Smithay. diff --git a/vendor/smithay/Cargo.toml b/vendor/smithay/Cargo.toml new file mode 100644 index 0000000..270051a --- /dev/null +++ b/vendor/smithay/Cargo.toml @@ -0,0 +1,150 @@ +[package] +name = "smithay" +version = "0.7.0" +authors = ["Victor Berger ", "Drakulix (Victoria Brekenfeld)"] +license = "MIT" +description = "Smithay is a library for writing wayland compositors." +repository = "https://github.com/Smithay/smithay" +edition = "2024" +readme = "README.md" +homepage = "https://smithay.github.io/" +keywords = ["wayland", "compositor", "graphics", "server"] +categories = ["gui"] +rust-version = "1.87" + +[package.metadata.docs.rs] +features = ["test_all_features", "libseat/docs_rs"] +rustdoc-args = ["--cfg", "docsrs"] + +[workspace] +resolver = "3" +members = [ + "smithay-drm-extras", + "smallvil", + "anvil", + "wlcs_anvil", + "test_clients", +] + +[dependencies] +appendlist = "1.4" +ash = { version = "0.38.0", optional = true } +bitflags = "2.2.1" +calloop = "0.14.5" +cursor-icon = "1.2.0" +downcast-rs = "1.2.0" +drm-fourcc = "^2.2.0" +drm = { version = "0.14.0", optional = true } +drm-ffi = { version = "0.9.1", optional = true } +drm-sys = { version = "0.8.1", optional = true } +errno = "0.3.5" +gbm = { version = "0.18.0", optional = true, default-features = false, features = ["drm-support"] } +glow = { version = "0.16", optional = true } +input = { version = "0.10.0", default-features = false, features=["libinput_1_19"], optional = true } +indexmap = "2.11" +libc = "0.2.103" +libseat = { version = "0.2.3", optional = true, default-features = false } +libloading = { version="0.8.0", optional = true } +rustix = { version = "1.0.7", features = ["event", "fs", "mm", "net", "pipe", "process", "shm", "time", "param"] } +rand = "0.10.0" +scopeguard = { version = "1.1.0", optional = true } +siphasher = { version = "1.0.3", optional = true } +tracing = "0.1.37" +tempfile = { version = "3.0", optional = true } +thiserror = "2.0.12" +udev = { version = "0.9.0", optional = true } +# Require never `wayland-client`/`wayland-cursor` than winit uses to fix `-Z minimal-versions` +# due to issue in older version. +wayland-client = { version = "0.31.14", optional = true } +wayland-cursor = { version = "0.31.14", optional = true } +wayland-egl = { version = "0.32.11", optional = true } +wayland-protocols = { version = "0.32.13", features = ["unstable", "staging", "server"], optional = true } +wayland-protocols-wlr = { version = "0.3.12", features = ["server"], optional = true } +wayland-protocols-misc = { version = "0.3.12", features = ["server"], optional = true } +wayland-server = { version = "0.31.13", optional = true } +wayland-sys = { version = "0.31.11", optional = true } +wayland-backend = { version = "0.3.15", optional = true } +winit = { version = "=0.31.0-beta.3", default-features = false, features = ["wayland", "wayland-dlopen", "x11"], optional = true } +x11rb = { version = "0.13.0", optional = true, features = ["res", "sync"]} +xkbcommon = { version = "0.9.0", features = ["wayland"]} +encoding_rs = { version = "0.8.33", optional = true } +profiling = "1.0.13" +smallvec = "1.11" +pixman = { version = "0.2.1", features = ["drm-fourcc", "sync"], optional = true } +aliasable = { version = "0.1.3", optional = true } +portable-atomic = { version = "1", features = ["float"] } +tracy-client = { version = "0.18.4", default-features = false, optional = true } +reis = { version = "0.7.0", features = ["calloop"] } +glam = { version = "0.33.2", default-features = false, features = ["std"] } + +[dev-dependencies] +clap = { version = "4", features = ["derive"] } +criterion = { version = "0.5" } +image = "0.25" +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } + +[build-dependencies] +gl_generator = { version = "0.14", optional = true } +pkg-config = { version = "0.3.17", optional = true } +cc = { version = "1.0.79", optional = true } + +[features] +default = ["backend_drm", "backend_gbm", "backend_libinput", "backend_udev", "backend_session_libseat", "backend_x11", "backend_winit", "desktop", "renderer_gl", "renderer_pixman", "renderer_multi", "xwayland", "wayland_frontend", "backend_vulkan", "backend_libei"] +backend_winit = ["winit", "backend_egl", "wayland-client", "wayland-cursor", "wayland-egl", "renderer_gl"] +backend_x11 = ["x11rb", "x11rb/dri3", "x11rb/xfixes", "x11rb/xinput", "x11rb/present", "x11rb_event_source", "backend_gbm", "backend_drm", "backend_egl"] +backend_drm = ["drm", "drm-ffi", "drm-sys"] +backend_gbm = ["gbm", "cc", "pkg-config", "backend_drm"] +backend_gbm_has_fd_for_plane = [] +backend_gbm_has_create_with_modifiers2 = [] +backend_egl = ["gl_generator", "libloading"] +backend_libinput = ["input"] +backend_session = [] +backend_udev = ["udev", "input/udev"] +backend_vulkan = ["ash", "scopeguard"] +backend_session_libseat = ["backend_session", "libseat"] +backend_libei = ["wayland_frontend"] +desktop = [] +renderer_gl = ["gl_generator", "backend_egl"] +renderer_glow = ["renderer_gl", "glow"] +renderer_multi = ["backend_drm", "aliasable"] +renderer_pixman = ["pixman"] +renderer_test = [] +tracy_gpu_profiling = ["renderer_gl", "tracy-client"] +use_system_lib = ["wayland_frontend", "wayland-backend/server_system", "wayland-sys", "gbm?/import-wayland"] +use_bindgen = ["drm-ffi/use_bindgen", "gbm/use_bindgen", "input/use_bindgen"] +wayland_frontend = ["wayland-server", "wayland-protocols", "wayland-protocols-wlr", "wayland-protocols-misc", "tempfile", "siphasher"] +x11rb_event_source = ["x11rb"] +xwayland = ["encoding_rs", "wayland_frontend", "x11rb/composite", "x11rb/xfixes", "x11rb/randr", "x11rb_event_source", "scopeguard"] +test_all_features = ["default", "use_system_lib", "renderer_glow", "renderer_test"] + +[[example]] +name = "minimal" +required-features = ["backend_winit", "renderer_gl", "wayland_frontend"] + +[[example]] +name = "seat" +required-features = ["wayland_frontend"] + +[[example]] +name = "compositor" +required-features = ["wayland_frontend"] + +[[example]] +name = "vulkan" +required-features = ["backend_vulkan"] + +[[example]] +name = "buffer_test" +required-features = ["backend_drm", "backend_gbm", "backend_egl", "backend_vulkan", "renderer_gl"] + +[[bench]] +name = "benchmark" +harness = false + +[[bench]] +name = "geometry" +harness = false + +[profile.release-with-debug] +inherits = "release" +debug = true diff --git a/vendor/smithay/DCO.md b/vendor/smithay/DCO.md new file mode 100644 index 0000000..6125c23 --- /dev/null +++ b/vendor/smithay/DCO.md @@ -0,0 +1,34 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2026 Smithay maintainers + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. \ No newline at end of file diff --git a/vendor/smithay/GETTING_STARTED.md b/vendor/smithay/GETTING_STARTED.md new file mode 100644 index 0000000..972095f --- /dev/null +++ b/vendor/smithay/GETTING_STARTED.md @@ -0,0 +1,53 @@ +# Getting started + +So you want to write a new Wayland compositor with Smithay, but don't know where to start? + +## Wayland itself + +Wayland is just a set of XML files describing the protocol by which a compositor and client communicate. +To work on a compositor, you need to establish some understanding of the state mandated by the protocol, +most importantly the lifetime of objects defined by it. + +We recommend the official [wayland-docs](https://wayland.freedesktop.org/docs/html/) to get started, in particular [Chapter 4](https://wayland.freedesktop.org/docs/html/ch04.html), +which talks about the protocol. + +Additionally it can help to familiarize yourself with the viewpoint of clients. The [wayland-book](https://wayland-book.com/) is a good resource here. + +There is also an (unfortunately stalled - at the time of writing this document -) effort to write a [smithay book](https://smithay.github.io/book/) to +cover similar topics, but focused on [wayland-rs](https://github.com/Smithay/wayland-rs) (the underlying Rust-based Wayland implementation) rather than +[libwayland](https://gitlab.freedesktop.org/wayland/wayland/). At least the client side of this is a worthwhile read as well. + +## Rust + +Smithay makes heavy use of Rust's features, which is likely why you selected the framework in the first place. +As such we assume you have *some* experience with the language already. + +If not the [awesome-rust resource section](https://github.com/rust-unofficial/awesome-rust?tab=readme-ov-file#resources) has you covered for various learning resources. + +## Smithay + +Unfortunately, the Smithay book doesn't cover the server-side (yet). + +Which leaves you with the following resources to understand how Smithay works: + +- A large portion of Smithay is build around [wayland-rs](https://github.com/smithay/wayland-rs) `Dispatch` machinery. It is thus highly recommended to read and understand +the documentation of the [wayland-server](https://smithay.github.io/wayland-rs/wayland_server/) crate. +- Smithay itself has [comprehensive documentation](https://smithay.github.io/smithay/) including for its [various](https://smithay.github.io/smithay/smithay/backend/index.html) [modules](https://smithay.github.io/smithay/smithay/wayland/index.html)! +- [smallvil](https://github.com/Smithay/smithay/tree/master/smallvil) is the smallest (somewhat) usable compositor using Smithay and intended as a learning resource and potential starting point. +- [anvil](https://github.com/Smithay/smithay/tree/master/anvil) is smithay's testing ground and thus a much more complete compositor to study. Its code strives to fill a gap between real-world examples and easy-to-understand/maintain code paths, however it is lacking in overall quality. +- Many real world compositors use Smithay, please refer to our [README](https://github.com/Smithay/smithay/blob/master/README.md#other-compositors-that-use-smithay) for a list of them to study. + +### Side-note: Which version of Smithay should I use? + +Smithay itself does not follow a regular release schedule, as such many compositors choose to depend on a given git commit and frequent updates. We strive to do better in this department and try to publish semi-frequently to crates.io. + +However the framework is still evolving a lot, thus we cannot really commit to any sort of ABI stability. Thus you currently need to rely on git-based updates to not only get new features, but also bugfixes. As a result released Smithay versions don't really have a benefit other than coming from crates.io. + +If you are still unsure, check our [Changelog](https://github.com/Smithay/smithay/blob/master/CHANGELOG.md) and see how many unreleased features +are currently waiting for a new Smithay release. The longer the list, the more we recommend to start off with the current git commit to make updating +to the next released version as easy as possible. + +## Further help + +If you still feel stuck trying to work your way through these resources or have questions not covered here, please feel free to reach out in our chatroom. +You can find us on Matrix: [#smithay:matrix.org](https://matrix.to/#/#smithay:matrix.org). If you don't want to use Matrix, this room is also bridged to libera.chat IRC on #smithay. diff --git a/vendor/smithay/LICENSE.txt b/vendor/smithay/LICENSE.txt new file mode 100644 index 0000000..5a8cb67 --- /dev/null +++ b/vendor/smithay/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Victor Berger and Victoria Brekenfeld + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/smithay/README.md b/vendor/smithay/README.md new file mode 100644 index 0000000..f8b4547 --- /dev/null +++ b/vendor/smithay/README.md @@ -0,0 +1,89 @@ +

+ Smithay +

+ +

+ Crates.io + docs.rs + CI + Matrix + IRC +

+ +

+ A smithy for rusty Wayland compositors +

+ +## Goals + +Smithay aims to provide building blocks to create Wayland compositors in Rust. While not +being a full-blown compositor, it'll provide objects and interfaces implementing common +functionalities that pretty much any compositor will need, in a generic fashion. + +It supports the [core Wayland protocols](https://gitlab.freedesktop.org/wayland/wayland), the official [protocol extensions](https://gitlab.freedesktop.org/wayland/wayland-protocols), and *some* external extensions, such as those made by and for [wlroots](https://gitlab.freedesktop.org/wlroots/wlr-protocols) and [KDE](https://invent.kde.org/libraries/plasma-wayland-protocols). + + +Also: + +- **Documented:** Smithay strives to maintain clear and detailed documentation of its API and + functionalities. Compiled documentation is available on [docs.rs](https://docs.rs/smithay) for released + versions, and [here](https://smithay.github.io/smithay) for the master branch. +- **Safety:** Smithay aims to be memory-safe and reliable, thanks to Rust. +- **Modularity:** Smithay is not a framework and does not impose constraints. You are never required + to use components you don’t need. +- **High-level:** You should not have to worry about gory low-level stuff (but Smithay won't + stop you if you really want to dive into it). + +## Getting started + +If you want to learn how to build a compositor with Smithay, the best place to start is the [getting started](https://github.com/Smithay/smithay/blob/master/GETTING_STARTED.md) guide. + +## Anvil + +Smithay as a compositor library has its own sample compositor: `anvil`. + +To get information about it and how you can run it, check out the [`anvil` README](https://github.com/Smithay/smithay/blob/master/anvil/README.md) + +## Other compositors that use Smithay + +- [Cosmic](https://github.com/pop-os/cosmic-epoch): Next-generation Cosmic desktop environment +- [Catacomb](https://github.com/catacombing/catacomb): A Wayland Mobile Compositor +- [emskin](https://github.com/emskin/emskin): A nested Wayland compositor for embedding any app inside Emacs +- [MagmaWM](https://github.com/MagmaWM/MagmaWM): A versatile and customizable Wayland Compositor +- [Niri](https://github.com/YaLTeR/niri): A scrollable-tiling Wayland compositor +- [Strata](https://github.com/StrataWM/strata): A cutting-edge, robust and sleek Wayland compositor +- [Pinnacle](https://github.com/Ottatop/pinnacle): A WIP Wayland compositor, inspired by AwesomeWM +- [Sudbury](https://gitlab.freedesktop.org/bwidawsk/sudbury): Compositor designed for ChromeOS +- [wprs](https://github.com/wayland-transpositor/wprs): Like [xpra](https://en.wikipedia.org/wiki/Xpra), but for Wayland, and written in +Rust. +- [Local Desktop](https://github.com/localdesktop/localdesktop): An Android app for running GUI Linux via PRoot and Wayland. +- [Otto](https://github.com/nongio/otto): A gesture-driven stacking compositor. +- [DriftWM](https://github.com/malbiruk/driftwm): A trackpad-first infinite canvas Wayland compositor. +- [Denial](https://github.com/denialwm/denial): A stacking, tiling, and scrolling-tiling Wayland compositor with a Flutter-based shell. + +## System Dependencies + +(This list can depend on the features you enable) + +- `libwayland` +- `libxkbcommon` +- `libudev` +- `libinput` +- `libgbm` +- [`libseat`](https://git.sr.ht/~kennylevinsen/seatd) +- `xwayland` +- `libEGL` +- `libpixman` +- `libdisplay-info` + +## Contact us + +If you have questions or want to discuss the project with us, our main chatroom is on Matrix: [`#smithay:matrix.org`](https://matrix.to/#/#smithay:matrix.org). + +## Contributing + +General notes on contributing to Smithay can be found [here](https://github.com/Smithay/smithay/blob/master/CONTRIBUTING.md). + +Please note that to submit code to Smithay, you must agree to our [Developer Certificate of Origin](https://github.com/Smithay/smithay/blob/master/DCO.md). + +If you are used to using generative AI, please ensure you read our [Policy](https://github.com/Smithay/smithay/blob/master/AI.md) before starting. diff --git a/vendor/smithay/UPSTREAM.md b/vendor/smithay/UPSTREAM.md new file mode 100644 index 0000000..386146b --- /dev/null +++ b/vendor/smithay/UPSTREAM.md @@ -0,0 +1,17 @@ +# Vendored Smithay + +- Repository: https://github.com/Smithay/smithay +- Revision: `8a270232f1bfc416c781273c98a3f52a812a0bba` +- Source: complete tracked snapshot from `git archive --format=tar 8a270232f1bfc416c781273c98a3f52a812a0bba`. No `.git`, `target`, or untracked Cargo cache markers are included. +- Original archive: 4894720 bytes; SHA-256 `81a42cfd4c3626c9bad8051023970151dcea0fe7bedd02fc09105db360a847c2`. +- Original snapshot: 345 regular files; 4559959 content bytes. File contents, symlink targets and Git executable bits were independently verified against the archive. Archive ownership and write permissions follow the extracting user's normal umask. +- Licenses are preserved in `LICENSE.txt`, `smithay-drm-extras/LICENSE`, and individual source headers. + +## Local patch + +`patches/shm-texture-cache-format.patch` changes the GLES SHM surface-cache compatibility check to match dimensions, GL internal format, and alpha interpretation. Previously an equal-sized ARGB/XRGB change reused stale `has_alpha`, selecting the wrong shader variant. A newly allocated texture also forces a complete upload. Context identity is already part of the cache key. Offset and stride describe each upload's source and are read anew; SHM cache entries always have normal orientation and non-external storage, so those properties do not add compatibility criteria. + +At most 16 debug records per process describe format incompatibilities. Records contain only numeric format/dimension fields, alpha booleans, and a fixed event message. They contain no pixel data, application text, titles, or credentials. + +The independent private-Wayland GPU regression is in Ferese's `compositor/src/state/partial_shm_gpu_tests.rs`. It compares the cached renderer with a new, unshared EGL context and full upload/full repaint on every frame, in both ARGB-first and XRGB-first directions. The ordinary partial-damage regression shares the fixture. + diff --git a/vendor/smithay/anvil/Cargo.toml b/vendor/smithay/anvil/Cargo.toml new file mode 100644 index 0000000..04f4c78 --- /dev/null +++ b/vendor/smithay/anvil/Cargo.toml @@ -0,0 +1,65 @@ +[package] +authors = ["Victor Berger ", "Drakulix (Victoria Brekenfeld)"] +edition = "2024" +license = "MIT" +name = "anvil" +publish = false +version = "0.0.1" + +[dependencies] +bitflags = "2.2.1" +fps_ticker = {version = "1.0.0", optional = true} +image = {version = "0.25.1", default-features = false, optional = true, features = ["png"]} +rand = "0.8" +tracing = { version = "0.1.37", features = ["max_level_trace", "release_max_level_debug"] } +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +thiserror = "1" +xcursor = {version = "0.3.3", optional = true} +xkbcommon = "0.8.0" +renderdoc = {version = "0.11.0", optional = true} +smithay-drm-extras = {path = "../smithay-drm-extras", optional = true} +puffin_http = { version = "0.13", optional = true } +profiling = { version = "1.0" } + +[dependencies.smithay] +default-features = false +features = ["desktop", "wayland_frontend"] +path = ".." + +[dependencies.x11rb] +default-features = false +features = ["composite"] +optional = true +version = "0.13.0" + +[build-dependencies] +gl_generator = "0.14" + +[features] +debug = ["fps_ticker", "image/png", "renderdoc"] +default = ["egl", "winit", "x11", "udev", "xwayland", "libei"] +egl = ["smithay/use_system_lib", "smithay/backend_egl"] +test_all_features = ["default", "debug"] +udev = [ + "smithay-drm-extras", + "smithay/backend_libinput", + "smithay/backend_udev", + "smithay/backend_drm", + "smithay/backend_gbm", + "smithay/backend_vulkan", + "smithay/backend_egl", + "smithay/backend_session_libseat", + "image", + "smithay/renderer_gl", + "smithay/renderer_pixman", + "smithay/renderer_multi", + "xcursor", +] +winit = ["smithay/backend_winit", "smithay/backend_drm"] +x11 = ["smithay/backend_x11", "x11rb", "smithay/renderer_gl", "smithay/backend_vulkan"] +xwayland = ["smithay/xwayland", "x11rb", "smithay/x11rb_event_source", "xcursor"] +libei = ["smithay/backend_libei", "udev"] +profile-with-puffin = ["profiling/profile-with-puffin", "puffin_http"] +profile-with-tracy = ["profiling/profile-with-tracy"] +profile-with-tracy-mem = ["profile-with-tracy"] +renderer_sync = [] diff --git a/vendor/smithay/anvil/README.md b/vendor/smithay/anvil/README.md new file mode 100644 index 0000000..07acd37 --- /dev/null +++ b/vendor/smithay/anvil/README.md @@ -0,0 +1,54 @@ +# Anvil + +A compositor used as a testing ground for new smithay features. +For a simple example compositor consider reading [smallvil](https://github.com/Smithay/smithay/tree/master/smallvil) + +## Dependencies + +You'll need to install the following dependencies (note, that those package +names may vary depending on your OS and linux distribution): + +- `libwayland` +- `libxkbcommon` + +#### These are needed for the "Udev/DRM backend" + +- `libudev` +- `libinput` +- `libgbm` +- [`libseat`](https://git.sr.ht/~kennylevinsen/seatd) + +If you want to enable X11 support (to run X11 applications within anvil), +then you'll need to install the following packages as well: + - `xwayland` + +## Build and run + +You can run it with cargo after having cloned this repository: + +``` +cd anvil; + +cargo run -- --{backend} +``` + +The currently available backends are: + +- `--x11`: start anvil as an X11 client. This allows you to run the compositor inside an X11 session or any compositor supporting XWayland. Should be preferred over the winit backend where possible. +- `--winit`: start anvil as a [Winit](https://github.com/tomaka/winit) application. This allows you to run it + inside of an other X11 or Wayland session. +- `--tty-udev`: start anvil in a tty with udev support. This is the "traditional" launch of a Wayland + compositor. Note that this requires you to start anvil as root if your system does not have logind + available. + +### Supported Environment Variables + +| Variable | Example | Backends | +|-------------------------------|-----------------|-----------| +| ANVIL_DRM_DEVICE | /dev/dri/card0 | tty-udev | +| ANVIL_DISABLE_10BIT | any | tty-udev | +| ANVIL_DISABLE_DIRECT_SCANOUT | any | tty-udev | +| ANVIL_GLES_DISABLE_INSTANCING | any | tty-udev | +| ANVIL_NO_VULKAN | 1,true,yes,y | x11 | +| SMITHAY_USE_LEGACY | 1,true,yes,y | tty-udev | +| SMITHAY_VK_VERSION | 1.3 | | diff --git a/vendor/smithay/anvil/resources/cursor.rgba b/vendor/smithay/anvil/resources/cursor.rgba new file mode 100644 index 0000000..729c1cc Binary files /dev/null and b/vendor/smithay/anvil/resources/cursor.rgba differ diff --git a/vendor/smithay/anvil/resources/numbers.png b/vendor/smithay/anvil/resources/numbers.png new file mode 100644 index 0000000..bc01532 Binary files /dev/null and b/vendor/smithay/anvil/resources/numbers.png differ diff --git a/vendor/smithay/anvil/src/cursor.rs b/vendor/smithay/anvil/src/cursor.rs new file mode 100644 index 0000000..1150b68 --- /dev/null +++ b/vendor/smithay/anvil/src/cursor.rs @@ -0,0 +1,96 @@ +use std::{io::Read, time::Duration}; + +use tracing::warn; +use xcursor::{ + CursorTheme, + parser::{Image, parse_xcursor}, +}; + +static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../resources/cursor.rgba"); + +pub struct Cursor { + icons: Vec, + size: u32, +} + +impl Cursor { + pub fn load() -> Cursor { + let name = std::env::var("XCURSOR_THEME") + .ok() + .unwrap_or_else(|| "default".into()); + let size = std::env::var("XCURSOR_SIZE") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(24); + + let theme = CursorTheme::load(&name); + let icons = load_icon(&theme) + .map_err(|err| warn!("Unable to load xcursor: {}, using fallback cursor", err)) + .unwrap_or_else(|_| { + vec![Image { + size: 32, + width: 64, + height: 64, + xhot: 1, + yhot: 1, + delay: 1, + pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), + pixels_argb: vec![], //unused + }] + }); + + Cursor { icons, size } + } + + pub fn get_image(&self, scale: u32, time: Duration) -> Image { + let size = self.size * scale; + frame(time.as_millis() as u32, size, &self.icons) + } +} + +fn nearest_images(size: u32, images: &[Image]) -> impl Iterator { + // Follow the nominal size of the cursor to choose the nearest + let nearest_image = images + .iter() + .min_by_key(|image| (size as i32 - image.size as i32).abs()) + .unwrap(); + + images + .iter() + .filter(move |image| image.width == nearest_image.width && image.height == nearest_image.height) +} + +fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image { + let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay); + if total == 0 { + return nearest_images(size, images).next().unwrap().clone(); + } + millis %= total; + + for img in nearest_images(size, images) { + if millis < img.delay { + return img.clone(); + } + millis -= img.delay; + } + + unreachable!() +} + +#[derive(thiserror::Error, Debug)] +enum Error { + #[error("Theme has no default cursor")] + NoDefaultCursor, + #[error("Error opening xcursor file: {0}")] + File(#[from] std::io::Error), + #[error("Failed to parse XCursor file")] + Parse, +} + +fn load_icon(theme: &CursorTheme) -> Result, Error> { + let icon_path = theme.load_icon("default").ok_or(Error::NoDefaultCursor)?; + let mut cursor_file = std::fs::File::open(icon_path)?; + let mut cursor_data = Vec::new(); + cursor_file.read_to_end(&mut cursor_data)?; + parse_xcursor(&cursor_data).ok_or(Error::Parse) +} diff --git a/vendor/smithay/anvil/src/drawing.rs b/vendor/smithay/anvil/src/drawing.rs new file mode 100644 index 0000000..1661113 --- /dev/null +++ b/vendor/smithay/anvil/src/drawing.rs @@ -0,0 +1,259 @@ +#![allow(clippy::too_many_arguments)] + +use smithay::{ + backend::renderer::{ + Color32F, ImportAll, ImportMem, Renderer, Texture, + element::{ + AsRenderElements, Kind, + memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, + surface::WaylandSurfaceRenderElement, + }, + }, + input::pointer::CursorImageStatus, + render_elements, + utils::{Physical, Point, Scale}, +}; +#[cfg(feature = "debug")] +use smithay::{ + backend::renderer::{ + Frame, + element::{Element, Id, RenderElement}, + utils::CommitCounter, + }, + utils::{Buffer, Logical, Rectangle, Size, Transform, user_data::UserDataMap}, +}; + +pub static CLEAR_COLOR: Color32F = Color32F::new(0.8, 0.8, 0.9, 1.0); +pub static CLEAR_COLOR_FULLSCREEN: Color32F = Color32F::new(0.0, 0.0, 0.0, 0.0); + +pub struct PointerElement { + buffer: Option, + status: CursorImageStatus, +} + +impl Default for PointerElement { + fn default() -> Self { + Self { + buffer: Default::default(), + status: CursorImageStatus::default_named(), + } + } +} + +impl PointerElement { + pub fn set_status(&mut self, status: CursorImageStatus) { + self.status = status; + } + + pub fn set_buffer(&mut self, buffer: MemoryRenderBuffer) { + self.buffer = Some(buffer); + } +} + +render_elements! { + pub PointerRenderElement where R: ImportAll + ImportMem; + Surface=WaylandSurfaceRenderElement, + Memory=MemoryRenderBufferRenderElement, +} + +impl std::fmt::Debug for PointerRenderElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(), + Self::Memory(arg0) => f.debug_tuple("Memory").field(arg0).finish(), + Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(), + } + } +} + +impl AsRenderElements for PointerElement +where + R: Renderer + ImportAll + ImportMem, +{ + type RenderElement = PointerRenderElement; + fn render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec + where + E: From>, + { + match &self.status { + CursorImageStatus::Hidden => vec![], + // Always render `Default` for a named shape. + CursorImageStatus::Named(_) => { + if let Some(buffer) = self.buffer.as_ref() { + vec![ + PointerRenderElement::::from( + MemoryRenderBufferRenderElement::from_buffer( + renderer, + location.to_f64(), + buffer, + None, + None, + None, + Kind::Cursor, + ) + .expect("Lost system pointer buffer"), + ) + .into(), + ] + } else { + vec![] + } + } + CursorImageStatus::Surface(surface) => { + let elements: Vec> = + smithay::backend::renderer::element::surface::render_elements_from_surface_tree( + renderer, + surface, + location, + scale, + alpha, + Kind::Cursor, + ); + elements.into_iter().map(E::from).collect() + } + } + } +} + +#[cfg(feature = "debug")] +pub static FPS_NUMBERS_PNG: &[u8] = include_bytes!("../resources/numbers.png"); + +#[cfg(feature = "debug")] +#[derive(Debug, Clone)] +pub struct FpsElement { + id: Id, + value: u32, + texture: T, + commit_counter: CommitCounter, +} + +#[cfg(feature = "debug")] +impl FpsElement { + pub fn new(texture: T) -> Self { + FpsElement { + id: Id::new(), + texture, + value: 0, + commit_counter: CommitCounter::default(), + } + } + + pub fn update_fps(&mut self, fps: u32) { + if self.value != fps { + self.value = fps; + self.commit_counter.increment(); + } + } +} + +#[cfg(feature = "debug")] +impl Element for FpsElement +where + T: Texture + 'static, +{ + fn id(&self) -> &Id { + &self.id + } + + fn location(&self, _scale: Scale) -> Point { + (0, 0).into() + } + + fn src(&self) -> Rectangle { + let digits = if self.value < 10 { + 1 + } else if self.value < 100 { + 2 + } else { + 3 + }; + Rectangle::from_size((24 * digits, 35).into()).to_f64() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + let digits = if self.value < 10 { + 1 + } else if self.value < 100 { + 2 + } else { + 3 + }; + Rectangle::from_size((24 * digits, 35).into()).to_physical_precise_round(scale) + } + + fn current_commit(&self) -> CommitCounter { + self.commit_counter + } +} + +#[cfg(feature = "debug")] +impl RenderElement for FpsElement +where + R: Renderer + ImportAll, + R::TextureId: 'static, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + // FIXME: respect the src for cropping + let scale = dst.size.to_f64() / self.src().size; + let value_str = std::cmp::min(self.value, 999).to_string(); + let mut offset: Point = Point::from((0.0, 0.0)); + for digit in value_str.chars().map(|d| d.to_digit(10).unwrap()) { + let digit_location = dst.loc.to_f64() + offset; + let digit_size = Size::::from((22, 35)).to_f64().to_physical(scale); + let dst = Rectangle::new( + digit_location.to_i32_round(), + ((digit_size.to_point() + digit_location).to_i32_round() - digit_location.to_i32_round()) + .to_size(), + ); + let damage = damage + .iter() + .cloned() + .flat_map(|x| x.intersection(dst)) + .map(|mut x| { + x.loc -= dst.loc; + x + }) + .collect::>(); + let texture_src: Rectangle = match digit { + 9 => Rectangle::from_size((22, 35).into()), + 6 => Rectangle::new((22, 0).into(), (22, 35).into()), + 3 => Rectangle::new((44, 0).into(), (22, 35).into()), + 1 => Rectangle::new((66, 0).into(), (22, 35).into()), + 8 => Rectangle::new((0, 35).into(), (22, 35).into()), + 0 => Rectangle::new((22, 35).into(), (22, 35).into()), + 2 => Rectangle::new((44, 35).into(), (22, 35).into()), + 7 => Rectangle::new((0, 70).into(), (22, 35).into()), + 4 => Rectangle::new((22, 70).into(), (22, 35).into()), + 5 => Rectangle::new((44, 70).into(), (22, 35).into()), + _ => unreachable!(), + }; + + frame.render_texture_from_to( + &self.texture, + texture_src.to_f64(), + dst, + &damage, + &[], + Transform::Normal, + 1.0, + )?; + offset += Point::from((24.0, 0.0)).to_physical(scale); + } + + Ok(()) + } +} diff --git a/vendor/smithay/anvil/src/focus.rs b/vendor/smithay/anvil/src/focus.rs new file mode 100644 index 0000000..903e135 --- /dev/null +++ b/vendor/smithay/anvil/src/focus.rs @@ -0,0 +1,712 @@ +use std::{borrow::Cow, sync::Arc}; + +#[cfg(feature = "xwayland")] +use smithay::xwayland::X11Surface; +#[cfg(feature = "xwayland")] +use smithay::xwayland::xwm::XwmOfferData; +pub use smithay::{ + backend::input::{InputTime, KeyState}, + desktop::{LayerSurface, PopupKind}, + input::{ + Seat, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + pointer::{AxisFrame, ButtonEvent, MotionEvent, PointerTarget, RelativeMotionEvent}, + }, + reexports::wayland_server::{Resource, backend::ObjectId, protocol::wl_surface::WlSurface}, + utils::{IsAlive, Serial}, + wayland::seat::WaylandFocus, +}; +use smithay::{ + desktop::{Window, WindowSurface}, + input::{ + dnd::{DndFocus, OfferData, Source}, + pointer::{ + GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, + GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + }, + tablet::tool::TabletToolTarget, + touch::{FrameMarker, TouchTarget}, + }, + reexports::wayland_server::DisplayHandle, + utils::{Logical, Point}, + wayland::selection::data_device::WlOfferData, +}; + +use crate::{ + shell::{SSD, WindowElement}, + state::{AnvilState, Backend}, +}; + +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum KeyboardFocusTarget { + Window(Window), + LayerSurface(LayerSurface), + Popup(PopupKind), +} + +impl IsAlive for KeyboardFocusTarget { + #[inline] + fn alive(&self) -> bool { + match self { + KeyboardFocusTarget::Window(w) => w.alive(), + KeyboardFocusTarget::LayerSurface(l) => l.alive(), + KeyboardFocusTarget::Popup(p) => p.alive(), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum PointerFocusTarget { + WlSurface(WlSurface), + #[cfg(feature = "xwayland")] + X11Surface(X11Surface), + SSD(SSD), +} + +impl IsAlive for PointerFocusTarget { + #[inline] + fn alive(&self) -> bool { + match self { + PointerFocusTarget::WlSurface(w) => w.alive(), + #[cfg(feature = "xwayland")] + PointerFocusTarget::X11Surface(w) => w.alive(), + PointerFocusTarget::SSD(x) => x.alive(), + } + } +} + +impl From for WlSurface { + #[inline] + fn from(target: PointerFocusTarget) -> Self { + target.wl_surface().unwrap().into_owned() + } +} + +impl KeyboardFocusTarget { + fn inner_keyboard_target(&self) -> &dyn KeyboardTarget> { + match self { + Self::Window(w) => match w.underlying_surface() { + WindowSurface::Wayland(w) => w.wl_surface(), + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => s, + }, + Self::LayerSurface(l) => l.wl_surface(), + Self::Popup(p) => p.wl_surface(), + } + } +} + +impl PointerFocusTarget { + fn inner_pointer_target(&self) -> &dyn PointerTarget> { + match self { + Self::WlSurface(w) => w, + #[cfg(feature = "xwayland")] + Self::X11Surface(w) => w, + Self::SSD(w) => w, + } + } + + fn inner_touch_target(&self) -> &dyn TouchTarget> { + match self { + Self::WlSurface(w) => w, + #[cfg(feature = "xwayland")] + Self::X11Surface(w) => w, + Self::SSD(w) => w, + } + } + + fn inner_tablet_tool_target( + &self, + ) -> &dyn TabletToolTarget> { + match self { + Self::WlSurface(w) => w, + #[cfg(feature = "xwayland")] + Self::X11Surface(w) => w, + Self::SSD(w) => w, + } + } +} + +impl PointerTarget> for PointerFocusTarget { + fn enter( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &MotionEvent, + ) { + self.inner_pointer_target().enter(seat, data, event) + } + fn motion( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &MotionEvent, + ) { + self.inner_pointer_target().motion(seat, data, event) + } + fn relative_motion( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &RelativeMotionEvent, + ) { + self.inner_pointer_target().relative_motion(seat, data, event) + } + fn button( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &ButtonEvent, + ) { + self.inner_pointer_target().button(seat, data, event) + } + fn axis( + &self, + seat: &Seat>, + data: &mut AnvilState, + frame: AxisFrame, + ) { + self.inner_pointer_target().axis(seat, data, frame) + } + fn frame(&self, seat: &Seat>, data: &mut AnvilState) { + self.inner_pointer_target().frame(seat, data) + } + fn leave( + &self, + seat: &Seat>, + data: &mut AnvilState, + serial: Serial, + time: InputTime, + ) { + self.inner_pointer_target().leave(seat, data, serial, time) + } + fn gesture_swipe_begin( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GestureSwipeBeginEvent, + ) { + self.inner_pointer_target().gesture_swipe_begin(seat, data, event) + } + fn gesture_swipe_update( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GestureSwipeUpdateEvent, + ) { + self.inner_pointer_target() + .gesture_swipe_update(seat, data, event) + } + fn gesture_swipe_end( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GestureSwipeEndEvent, + ) { + self.inner_pointer_target().gesture_swipe_end(seat, data, event) + } + fn gesture_pinch_begin( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GesturePinchBeginEvent, + ) { + self.inner_pointer_target().gesture_pinch_begin(seat, data, event) + } + fn gesture_pinch_update( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GesturePinchUpdateEvent, + ) { + self.inner_pointer_target() + .gesture_pinch_update(seat, data, event) + } + fn gesture_pinch_end( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GesturePinchEndEvent, + ) { + self.inner_pointer_target().gesture_pinch_end(seat, data, event) + } + fn gesture_hold_begin( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GestureHoldBeginEvent, + ) { + self.inner_pointer_target().gesture_hold_begin(seat, data, event) + } + fn gesture_hold_end( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &GestureHoldEndEvent, + ) { + self.inner_pointer_target().gesture_hold_end(seat, data, event) + } +} + +impl KeyboardTarget> for KeyboardFocusTarget { + fn enter( + &self, + seat: &Seat>, + data: &mut AnvilState, + keys: Vec>, + serial: Serial, + ) { + self.inner_keyboard_target().enter(seat, data, keys, serial) + } + fn leave( + &self, + seat: &Seat>, + data: &mut AnvilState, + serial: Serial, + ) { + self.inner_keyboard_target().leave(seat, data, serial) + } + fn key( + &self, + seat: &Seat>, + data: &mut AnvilState, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: InputTime, + ) { + self.inner_keyboard_target() + .key(seat, data, key, state, serial, time) + } + fn modifiers( + &self, + seat: &Seat>, + data: &mut AnvilState, + modifiers: ModifiersState, + serial: Serial, + ) { + self.inner_keyboard_target() + .modifiers(seat, data, modifiers, serial) + } +} + +impl TouchTarget> for PointerFocusTarget { + fn down( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &smithay::input::touch::DownEvent, + ) { + self.inner_touch_target().down(seat, data, event) + } + + fn up( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &smithay::input::touch::UpEvent, + ) { + self.inner_touch_target().up(seat, data, event) + } + + fn motion( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &smithay::input::touch::MotionEvent, + ) { + self.inner_touch_target().motion(seat, data, event) + } + + fn frame( + &self, + seat: &Seat>, + data: &mut AnvilState, + marker: FrameMarker, + ) { + self.inner_touch_target().frame(seat, data, marker) + } + + fn cancel( + &self, + seat: &Seat>, + data: &mut AnvilState, + marker: FrameMarker, + ) { + self.inner_touch_target().cancel(seat, data, marker) + } + + fn shape( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &smithay::input::touch::ShapeEvent, + ) { + self.inner_touch_target().shape(seat, data, event) + } + + fn orientation( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &smithay::input::touch::OrientationEvent, + ) { + self.inner_touch_target().orientation(seat, data, event) + } + + fn last_frame( + &self, + seat: &Seat>, + data: &mut AnvilState, + ) -> Option { + self.inner_touch_target().last_frame(seat, data) + } +} + +impl TabletToolTarget> for PointerFocusTarget { + fn proximity_in( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + tablet: &smithay::input::tablet::Tablet, + serial: Serial, + ) { + self.inner_tablet_tool_target() + .proximity_in(seat, data, tool_descriptor, tablet, serial); + } + + fn proximity_out( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + ) { + self.inner_tablet_tool_target() + .proximity_out(seat, data, tool_descriptor); + } + + fn down( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + event: &smithay::input::tablet::tool::DownEvent, + ) { + self.inner_tablet_tool_target() + .down(seat, data, tool_descriptor, event); + } + + fn up( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + event: &smithay::input::tablet::tool::UpEvent, + ) { + self.inner_tablet_tool_target() + .up(seat, data, tool_descriptor, event); + } + + fn motion( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + event: &smithay::input::tablet::tool::MotionEvent, + ) { + self.inner_tablet_tool_target() + .motion(seat, data, tool_descriptor, event); + } + + fn button( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + event: &smithay::input::tablet::tool::ButtonEvent, + ) { + self.inner_tablet_tool_target() + .button(seat, data, tool_descriptor, event); + } + + fn axis( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + frame: smithay::input::tablet::tool::AxisFrame, + ) { + self.inner_tablet_tool_target() + .axis(seat, data, tool_descriptor, frame); + } + + fn frame( + &self, + seat: &Seat>, + data: &mut AnvilState, + tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + time: InputTime, + ) { + self.inner_tablet_tool_target() + .frame(seat, data, tool_descriptor, time); + } +} + +impl WaylandFocus for PointerFocusTarget { + #[inline] + fn wl_surface(&self) -> Option> { + match self { + PointerFocusTarget::WlSurface(w) => w.wl_surface(), + #[cfg(feature = "xwayland")] + PointerFocusTarget::X11Surface(w) => w.wl_surface().map(Cow::Owned), + PointerFocusTarget::SSD(_) => None, + } + } + #[inline] + fn same_client_as(&self, object_id: &ObjectId) -> bool { + match self { + PointerFocusTarget::WlSurface(w) => w.same_client_as(object_id), + #[cfg(feature = "xwayland")] + PointerFocusTarget::X11Surface(w) => w.same_client_as(object_id), + PointerFocusTarget::SSD(w) => w + .wl_surface() + .map(|surface| surface.same_client_as(object_id)) + .unwrap_or(false), + } + } +} + +impl WaylandFocus for KeyboardFocusTarget { + #[inline] + fn wl_surface(&self) -> Option> { + match self { + KeyboardFocusTarget::Window(w) => w.wl_surface(), + KeyboardFocusTarget::LayerSurface(l) => Some(Cow::Borrowed(l.wl_surface())), + KeyboardFocusTarget::Popup(p) => Some(Cow::Borrowed(p.wl_surface())), + } + } +} + +pub enum AnvilOfferData { + Wayland(WlOfferData), + #[cfg(feature = "xwayland")] + X11(XwmOfferData), +} + +impl OfferData for AnvilOfferData { + fn disable(&self) { + match self { + AnvilOfferData::Wayland(data) => data.disable(), + #[cfg(feature = "xwayland")] + AnvilOfferData::X11(data) => data.disable(), + } + } + + fn drop(&self) { + match self { + AnvilOfferData::Wayland(data) => data.drop(), + #[cfg(feature = "xwayland")] + AnvilOfferData::X11(data) => data.drop(), + } + } + + fn validated(&self) -> bool { + match self { + AnvilOfferData::Wayland(data) => data.validated(), + #[cfg(feature = "xwayland")] + AnvilOfferData::X11(data) => data.validated(), + } + } +} + +#[allow(unreachable_patterns)] +impl DndFocus> for PointerFocusTarget { + type OfferData + = AnvilOfferData + where + S: Source; + + fn enter( + &self, + data: &mut AnvilState, + dh: &DisplayHandle, + source: Arc, + seat: &Seat>, + location: Point, + serial: &Serial, + ) -> Option> { + match self { + PointerFocusTarget::WlSurface(surface) => { + DndFocus::enter(surface, data, dh, source, seat, location, serial) + .map(AnvilOfferData::Wayland) + } + #[cfg(feature = "xwayland")] + PointerFocusTarget::X11Surface(surface) => { + DndFocus::enter(surface, data, dh, source, seat, location, serial).map(AnvilOfferData::X11) + } + _ => None, + } + } + + fn motion( + &self, + data: &mut AnvilState, + offer: Option<&mut AnvilOfferData>, + seat: &Seat>, + location: Point, + time: InputTime, + ) { + match self { + PointerFocusTarget::WlSurface(surface) => { + let offer = match offer { + Some(AnvilOfferData::Wayland(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::motion(surface, data, offer, seat, location, time) + } + #[cfg(feature = "xwayland")] + PointerFocusTarget::X11Surface(surface) => { + let offer = match offer { + Some(AnvilOfferData::X11(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::motion(surface, data, offer, seat, location, time) + } + _ => {} + } + } + + fn leave( + &self, + data: &mut AnvilState, + offer: Option<&mut AnvilOfferData>, + seat: &Seat>, + ) { + match self { + PointerFocusTarget::WlSurface(surface) => { + let offer = match offer { + Some(AnvilOfferData::Wayland(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::leave(surface, data, offer, seat) + } + #[cfg(feature = "xwayland")] + PointerFocusTarget::X11Surface(surface) => { + let offer = match offer { + Some(AnvilOfferData::X11(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::leave(surface, data, offer, seat) + } + _ => {} + } + } + + fn drop( + &self, + data: &mut AnvilState, + offer: Option<&mut AnvilOfferData>, + seat: &Seat>, + ) { + match self { + PointerFocusTarget::WlSurface(surface) => { + let offer = match offer { + Some(AnvilOfferData::Wayland(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::drop(surface, data, offer, seat) + } + #[cfg(feature = "xwayland")] + PointerFocusTarget::X11Surface(surface) => { + let offer = match offer { + Some(AnvilOfferData::X11(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::drop(surface, data, offer, seat) + } + _ => {} + } + } +} + +impl From for PointerFocusTarget { + #[inline] + fn from(value: WlSurface) -> Self { + PointerFocusTarget::WlSurface(value) + } +} + +impl From<&WlSurface> for PointerFocusTarget { + #[inline] + fn from(value: &WlSurface) -> Self { + PointerFocusTarget::from(value.clone()) + } +} + +impl From for PointerFocusTarget { + #[inline] + fn from(value: PopupKind) -> Self { + PointerFocusTarget::from(value.wl_surface()) + } +} + +#[cfg(feature = "xwayland")] +impl From for PointerFocusTarget { + #[inline] + fn from(value: X11Surface) -> Self { + PointerFocusTarget::X11Surface(value) + } +} + +#[cfg(feature = "xwayland")] +impl From<&X11Surface> for PointerFocusTarget { + #[inline] + fn from(value: &X11Surface) -> Self { + PointerFocusTarget::from(value.clone()) + } +} + +impl From for KeyboardFocusTarget { + #[inline] + fn from(w: WindowElement) -> Self { + KeyboardFocusTarget::Window(w.0.clone()) + } +} + +impl From for KeyboardFocusTarget { + #[inline] + fn from(l: LayerSurface) -> Self { + KeyboardFocusTarget::LayerSurface(l) + } +} + +impl From for KeyboardFocusTarget { + #[inline] + fn from(p: PopupKind) -> Self { + KeyboardFocusTarget::Popup(p) + } +} + +impl From for PointerFocusTarget { + #[inline] + fn from(value: KeyboardFocusTarget) -> Self { + match value { + KeyboardFocusTarget::Window(w) => match w.underlying_surface() { + WindowSurface::Wayland(w) => PointerFocusTarget::from(w.wl_surface()), + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => PointerFocusTarget::from(s), + }, + KeyboardFocusTarget::LayerSurface(surface) => PointerFocusTarget::from(surface.wl_surface()), + KeyboardFocusTarget::Popup(popup) => PointerFocusTarget::from(popup.wl_surface()), + } + } +} diff --git a/vendor/smithay/anvil/src/input_handler.rs b/vendor/smithay/anvil/src/input_handler.rs new file mode 100644 index 0000000..59c2fb7 --- /dev/null +++ b/vendor/smithay/anvil/src/input_handler.rs @@ -0,0 +1,1457 @@ +use std::{convert::TryInto, process::Command, sync::atomic::Ordering}; + +use crate::{AnvilState, focus::PointerFocusTarget, shell::FullscreenSurface}; + +#[cfg(feature = "udev")] +use crate::udev::UdevData; +#[cfg(feature = "udev")] +use smithay::{backend::renderer::DebugFlags, input::tablet}; + +use smithay::{ + backend::input::{ + self, Axis, AxisSource, Device, DeviceCapability, Event, GestureBeginEvent, GestureEndEvent, + GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, InputTime, + KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, TouchEvent, + }, + desktop::{WindowSurfaceType, layer_map_for_output}, + input::{ + keyboard::{FilterResult, Keysym, ModifiersState, keysyms as xkb}, + pointer::{AxisFrame, ButtonEvent, MotionEvent}, + pointer::{ + GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, + GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + }, + tablet::{TabletDescriptor, TabletSeatTrait}, + touch::{DownEvent, UpEvent}, + }, + output::Scale, + reexports::{ + wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1, + wayland_server::protocol::wl_pointer, + }, + utils::{Logical, Point, SERIAL_COUNTER as SCOUNTER, Serial, Transform}, + wayland::{ + input_method::InputMethodSeat, + keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat, + shell::wlr_layer::{KeyboardInteractivity, Layer as WlrLayer}, + virtual_keyboard::{VirtualKeyboardBackend, VirtualKeyboardHandler, VirtualKeyboardSpecialEvent}, + }, +}; + +use smithay::backend::input::AbsolutePositionEvent; + +#[cfg(any(feature = "winit", feature = "x11"))] +use smithay::output::Output; +use tracing::{debug, error, info, warn}; + +use crate::state::Backend; +#[cfg(feature = "udev")] +use smithay::{ + backend::{ + input::{ + PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, + TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, + }, + session::Session, + }, + input::pointer::RelativeMotionEvent, + reexports::wayland_server::DisplayHandle, + wayland::{ + pointer_constraints::{PointerConstraint, with_pointer_constraint}, + seat::WaylandFocus, + }, +}; + +impl AnvilState { + // Allow in this method because of existing usage + #[allow(clippy::uninlined_format_args)] + fn process_common_key_action(&mut self, action: KeyAction) { + match action { + KeyAction::None => (), + + KeyAction::Quit => { + info!("Quitting."); + self.running.store(false, Ordering::SeqCst); + } + + KeyAction::Run(cmd) => { + info!(cmd, "Starting program"); + + if let Err(e) = Command::new(&cmd) + .envs( + self.socket_name + .clone() + .map(|v| ("WAYLAND_DISPLAY", v)) + .into_iter() + .chain( + #[cfg(feature = "xwayland")] + self.xdisplay.map(|v| ("DISPLAY", format!(":{v}"))), + #[cfg(not(feature = "xwayland"))] + None, + ), + ) + .spawn() + { + error!(cmd, err = %e, "Failed to start program"); + } + } + + KeyAction::TogglePreview => { + self.show_window_preview = !self.show_window_preview; + } + + KeyAction::ToggleDecorations => { + for element in self.space.elements() { + #[allow(irrefutable_let_patterns)] + if let Some(toplevel) = element.0.toplevel() { + let mode_changed = toplevel.with_pending_state(|state| { + if let Some(current_mode) = state.decoration_mode { + let new_mode = + if current_mode == zxdg_toplevel_decoration_v1::Mode::ClientSide { + zxdg_toplevel_decoration_v1::Mode::ServerSide + } else { + zxdg_toplevel_decoration_v1::Mode::ClientSide + }; + state.decoration_mode = Some(new_mode); + true + } else { + false + } + }); + + if mode_changed && toplevel.is_initial_configure_sent() { + toplevel.send_pending_configure(); + } + } + } + } + + _ => unreachable!( + "Common key action handler encountered backend specific action {:?}", + action + ), + } + } + + fn keyboard_key_to_action(&mut self, evt: B::KeyboardKeyEvent) -> KeyAction { + let keycode = evt.key_code(); + let state = evt.state(); + debug!(?keycode, ?state, "key"); + let serial = SCOUNTER.next_serial(); + let time = Event::time(&evt); + let mut suppressed_keys = self.suppressed_keys.clone(); + let keyboard = self.seat.get_keyboard().unwrap(); + + for layer in self.layer_shell_state.layer_surfaces().rev() { + let exclusive = layer.with_cached_state(|data| { + data.keyboard_interactivity == KeyboardInteractivity::Exclusive + && (data.layer == WlrLayer::Top || data.layer == WlrLayer::Overlay) + }); + if exclusive { + let surface = self.space.outputs().find_map(|o| { + let map = layer_map_for_output(o); + map.layers().find(|l| l.layer_surface() == &layer).cloned() + }); + if let Some(surface) = surface { + keyboard.set_focus(self, Some(surface.into()), serial); + keyboard.input::<(), _>(self, keycode, state, serial, time, |_, _, _| { + FilterResult::Forward + }); + return KeyAction::None; + }; + } + } + + let inhibited = self + .space + .element_under(self.pointer.current_location()) + .and_then(|(window, _)| { + let surface = window.wl_surface()?; + self.seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + }) + .map(|inhibitor| inhibitor.is_active()) + .unwrap_or(false); + + let action = keyboard + .input(self, keycode, state, serial, time, |_, modifiers, handle| { + let keysym = handle.modified_sym(); + + debug!( + ?state, + mods = ?modifiers, + keysym = ::xkbcommon::xkb::keysym_get_name(keysym), + "keysym" + ); + + // If the key is pressed and triggered a action + // we will not forward the key to the client. + // Additionally add the key to the suppressed keys + // so that we can decide on a release if the key + // should be forwarded to the client or not. + if let KeyState::Pressed = state { + if !inhibited { + let action = process_keyboard_shortcut(*modifiers, keysym); + + if action.is_some() { + suppressed_keys.push(keysym); + } + + action + .map(FilterResult::Intercept) + .unwrap_or(FilterResult::Forward) + } else { + FilterResult::Forward + } + } else { + let suppressed = suppressed_keys.contains(&keysym); + if suppressed { + suppressed_keys.retain(|k| *k != keysym); + FilterResult::Intercept(KeyAction::None) + } else { + FilterResult::Forward + } + } + }) + .unwrap_or(KeyAction::None); + + self.suppressed_keys = suppressed_keys; + action + } + + fn on_pointer_button(&mut self, evt: B::PointerButtonEvent) { + let serial = SCOUNTER.next_serial(); + let button = evt.button_code(); + + let state = wl_pointer::ButtonState::from(evt.state()); + + if wl_pointer::ButtonState::Pressed == state { + self.update_keyboard_focus(self.pointer.current_location(), serial); + }; + let pointer = self.pointer.clone(); + pointer.button( + self, + &ButtonEvent { + button, + state: state.try_into().unwrap(), + serial, + time: evt.time(), + }, + ); + pointer.frame(self); + } + + fn on_gesture_swipe_begin(&mut self, evt: B::GestureSwipeBeginEvent) { + let serial = SCOUNTER.next_serial(); + let pointer = self.pointer.clone(); + pointer.gesture_swipe_begin( + self, + &GestureSwipeBeginEvent { + serial, + time: evt.time(), + fingers: evt.fingers(), + }, + ); + } + + fn on_gesture_swipe_update(&mut self, evt: B::GestureSwipeUpdateEvent) { + let pointer = self.pointer.clone(); + pointer.gesture_swipe_update( + self, + &GestureSwipeUpdateEvent { + time: evt.time(), + delta: evt.delta(), + }, + ); + } + + fn on_gesture_swipe_end(&mut self, evt: B::GestureSwipeEndEvent) { + let serial = SCOUNTER.next_serial(); + let pointer = self.pointer.clone(); + pointer.gesture_swipe_end( + self, + &GestureSwipeEndEvent { + serial, + time: evt.time(), + cancelled: evt.cancelled(), + }, + ); + } + + fn on_gesture_pinch_begin(&mut self, evt: B::GesturePinchBeginEvent) { + let serial = SCOUNTER.next_serial(); + let pointer = self.pointer.clone(); + pointer.gesture_pinch_begin( + self, + &GesturePinchBeginEvent { + serial, + time: evt.time(), + fingers: evt.fingers(), + }, + ); + } + + fn on_gesture_pinch_update(&mut self, evt: B::GesturePinchUpdateEvent) { + let pointer = self.pointer.clone(); + pointer.gesture_pinch_update( + self, + &GesturePinchUpdateEvent { + time: evt.time(), + delta: evt.delta(), + scale: evt.scale(), + rotation: evt.rotation(), + }, + ); + } + + fn on_gesture_pinch_end(&mut self, evt: B::GesturePinchEndEvent) { + let serial = SCOUNTER.next_serial(); + let pointer = self.pointer.clone(); + pointer.gesture_pinch_end( + self, + &GesturePinchEndEvent { + serial, + time: evt.time(), + cancelled: evt.cancelled(), + }, + ); + } + + fn on_gesture_hold_begin(&mut self, evt: B::GestureHoldBeginEvent) { + let serial = SCOUNTER.next_serial(); + let pointer = self.pointer.clone(); + pointer.gesture_hold_begin( + self, + &GestureHoldBeginEvent { + serial, + time: evt.time(), + fingers: evt.fingers(), + }, + ); + } + + fn on_gesture_hold_end(&mut self, evt: B::GestureHoldEndEvent) { + let serial = SCOUNTER.next_serial(); + let pointer = self.pointer.clone(); + pointer.gesture_hold_end( + self, + &GestureHoldEndEvent { + serial, + time: evt.time(), + cancelled: evt.cancelled(), + }, + ); + } + + fn update_keyboard_focus(&mut self, location: Point, serial: Serial) { + let keyboard = self.seat.get_keyboard().unwrap(); + let touch = self.seat.get_touch(); + let input_method = self.seat.input_method(); + // change the keyboard focus unless the pointer or keyboard is grabbed + // We test for any matching surface type here but always use the root + // (in case of a window the toplevel) surface for the focus. + // So for example if a user clicks on a subsurface or popup the toplevel + // will receive the keyboard focus. Directly assigning the focus to the + // matching surface leads to issues with clients dismissing popups and + // subsurface menus (for example firefox-wayland). + // see here for a discussion about that issue: + // https://gitlab.freedesktop.org/wayland/wayland/-/issues/294 + if !self.pointer.is_grabbed() + && (!keyboard.is_grabbed() || input_method.keyboard_grabbed()) + && !touch.map(|touch| touch.is_grabbed()).unwrap_or(false) + { + let output = self.space.output_under(location).next().cloned(); + if let Some(output) = output.as_ref() { + let output_geo = self.space.output_geometry(output).unwrap(); + if let Some(window) = output + .user_data() + .get::() + .and_then(|f| f.get()) + { + if let Some((_, _)) = + window.surface_under(location - output_geo.loc.to_f64(), WindowSurfaceType::ALL) + { + #[cfg(feature = "xwayland")] + if let Some(surface) = window.0.x11_surface() { + self.xwm.as_mut().unwrap().raise_window(surface).unwrap(); + } + keyboard.set_focus(self, Some(window.into()), serial); + return; + } + } + + let layers = layer_map_for_output(output); + if let Some(layer) = layers + .layer_under(WlrLayer::Overlay, location - output_geo.loc.to_f64()) + .or_else(|| layers.layer_under(WlrLayer::Top, location - output_geo.loc.to_f64())) + { + if layer.can_receive_keyboard_focus() { + if let Some((_, _)) = layer.surface_under( + location + - output_geo.loc.to_f64() + - layers.layer_geometry(layer).unwrap().loc.to_f64(), + WindowSurfaceType::ALL, + ) { + keyboard.set_focus(self, Some(layer.clone().into()), serial); + return; + } + } + } + } + + if let Some((window, _)) = self.space.element_under(location).map(|(w, p)| (w.clone(), p)) { + self.space.raise_element(&window, true); + #[cfg(feature = "xwayland")] + if let Some(surface) = window.0.x11_surface() { + self.xwm.as_mut().unwrap().raise_window(surface).unwrap(); + } + keyboard.set_focus(self, Some(window.into()), serial); + return; + } + + if let Some(output) = output.as_ref() { + let output_geo = self.space.output_geometry(output).unwrap(); + let layers = layer_map_for_output(output); + if let Some(layer) = layers + .layer_under(WlrLayer::Bottom, location - output_geo.loc.to_f64()) + .or_else(|| layers.layer_under(WlrLayer::Background, location - output_geo.loc.to_f64())) + { + if layer.can_receive_keyboard_focus() { + if let Some((_, _)) = layer.surface_under( + location + - output_geo.loc.to_f64() + - layers.layer_geometry(layer).unwrap().loc.to_f64(), + WindowSurfaceType::ALL, + ) { + keyboard.set_focus(self, Some(layer.clone().into()), serial); + } + } + } + }; + } + } + + pub fn surface_under( + &self, + pos: Point, + ) -> Option<(PointerFocusTarget, Point)> { + let output = self.space.outputs().find(|o| { + let geometry = self.space.output_geometry(o).unwrap(); + geometry.contains(pos.to_i32_round()) + })?; + let output_geo = self.space.output_geometry(output).unwrap(); + let layers = layer_map_for_output(output); + + let mut under = None; + if let Some((surface, loc)) = output + .user_data() + .get::() + .and_then(|f| f.get()) + .and_then(|w| w.surface_under(pos - output_geo.loc.to_f64(), WindowSurfaceType::ALL)) + { + under = Some((surface, loc + output_geo.loc)); + } else if let Some(focus) = layers + .layer_under(WlrLayer::Overlay, pos - output_geo.loc.to_f64()) + .or_else(|| layers.layer_under(WlrLayer::Top, pos - output_geo.loc.to_f64())) + .and_then(|layer| { + let layer_loc = layers.layer_geometry(layer).unwrap().loc; + layer + .surface_under( + pos - output_geo.loc.to_f64() - layer_loc.to_f64(), + WindowSurfaceType::ALL, + ) + .map(|(surface, loc)| { + ( + PointerFocusTarget::from(surface), + loc + layer_loc + output_geo.loc, + ) + }) + }) + { + under = Some(focus) + } else if let Some(focus) = self.space.element_under(pos).and_then(|(window, loc)| { + window + .surface_under(pos - loc.to_f64(), WindowSurfaceType::ALL) + .map(|(surface, surf_loc)| (surface, surf_loc + loc)) + }) { + under = Some(focus); + } else if let Some(focus) = layers + .layer_under(WlrLayer::Bottom, pos - output_geo.loc.to_f64()) + .or_else(|| layers.layer_under(WlrLayer::Background, pos - output_geo.loc.to_f64())) + .and_then(|layer| { + let layer_loc = layers.layer_geometry(layer).unwrap().loc; + layer + .surface_under( + pos - output_geo.loc.to_f64() - layer_loc.to_f64(), + WindowSurfaceType::ALL, + ) + .map(|(surface, loc)| { + ( + PointerFocusTarget::from(surface), + loc + layer_loc + output_geo.loc, + ) + }) + }) + { + under = Some(focus) + }; + under.map(|(s, l)| (s, l.to_f64())) + } + + fn on_pointer_axis(&mut self, evt: B::PointerAxisEvent) { + let horizontal_amount = evt + .amount(input::Axis::Horizontal) + .unwrap_or_else(|| evt.amount_v120(input::Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.); + let vertical_amount = evt + .amount(input::Axis::Vertical) + .unwrap_or_else(|| evt.amount_v120(input::Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.); + let horizontal_amount_discrete = evt.amount_v120(input::Axis::Horizontal); + let vertical_amount_discrete = evt.amount_v120(input::Axis::Vertical); + + { + let mut frame = AxisFrame::new(evt.time()).source(evt.source()); + if horizontal_amount != 0.0 { + frame = frame.relative_direction(Axis::Horizontal, evt.relative_direction(Axis::Horizontal)); + frame = frame.value(Axis::Horizontal, horizontal_amount); + if let Some(discrete) = horizontal_amount_discrete { + frame = frame.v120(Axis::Horizontal, discrete as i32); + } + } + if vertical_amount != 0.0 { + frame = frame.relative_direction(Axis::Vertical, evt.relative_direction(Axis::Vertical)); + frame = frame.value(Axis::Vertical, vertical_amount); + if let Some(discrete) = vertical_amount_discrete { + frame = frame.v120(Axis::Vertical, discrete as i32); + } + } + if evt.source() == AxisSource::Finger { + if evt.amount(Axis::Horizontal) == Some(0.0) { + frame = frame.stop(Axis::Horizontal); + } + if evt.amount(Axis::Vertical) == Some(0.0) { + frame = frame.stop(Axis::Vertical); + } + } + let pointer = self.pointer.clone(); + pointer.axis(self, frame); + pointer.frame(self); + } + } + + fn touch_location_transformed>( + &self, + evt: &E, + ) -> Option> { + let output = self + .space + .outputs() + .find(|output| output.name().starts_with("eDP")) + .or_else(|| self.space.outputs().next()); + + let output = output?; + let output_geometry = self.space.output_geometry(output)?; + + let transform = output.current_transform(); + let size = transform.invert().transform_size(output_geometry.size); + Some( + transform.transform_point_in(evt.position_transformed(size), &size.to_f64()) + + output_geometry.loc.to_f64(), + ) + } + + fn on_touch_down(&mut self, evt: B::TouchDownEvent) { + let Some(handle) = self.seat.get_touch() else { + return; + }; + + let Some(touch_location) = self.touch_location_transformed(&evt) else { + return; + }; + + let serial = SCOUNTER.next_serial(); + self.update_keyboard_focus(touch_location, serial); + + let under = self.surface_under(touch_location); + handle.down( + self, + under, + &DownEvent { + slot: evt.slot(), + location: touch_location, + serial, + time: evt.time(), + }, + ); + } + + fn on_touch_up(&mut self, evt: B::TouchUpEvent) { + let Some(handle) = self.seat.get_touch() else { + return; + }; + let serial = SCOUNTER.next_serial(); + handle.up( + self, + &UpEvent { + slot: evt.slot(), + serial, + time: evt.time(), + }, + ) + } + + fn on_touch_motion(&mut self, evt: B::TouchMotionEvent) { + let Some(handle) = self.seat.get_touch() else { + return; + }; + let Some(touch_location) = self.touch_location_transformed(&evt) else { + return; + }; + + let under = self.surface_under(touch_location); + handle.motion( + self, + under, + &smithay::input::touch::MotionEvent { + slot: evt.slot(), + location: touch_location, + time: evt.time(), + }, + ); + } + + fn on_touch_frame(&mut self, _evt: B::TouchFrameEvent) { + let Some(handle) = self.seat.get_touch() else { + return; + }; + handle.frame(self); + } + + fn on_touch_cancel(&mut self, _evt: B::TouchCancelEvent) { + let Some(handle) = self.seat.get_touch() else { + return; + }; + handle.cancel(self); + } + + fn on_device_added(&mut self, device: B::Device) { + let dh = &self.display_handle; + if device.has_capability(DeviceCapability::TabletTool) { + self.seat + .tablet_seat() + .add_wp_tablet(dh, &TabletDescriptor::from(&device)); + } + if device.has_capability(DeviceCapability::Touch) && self.seat.get_touch().is_none() { + self.seat.add_touch(); + } + } + + fn on_device_removed(&mut self, device: B::Device) { + if device.has_capability(DeviceCapability::TabletTool) { + let tablet_seat = self.seat.tablet_seat(); + + tablet_seat.remove_tablet(&TabletDescriptor::from(&device)); + + // If there are no tablets in seat we can remove all tools + if tablet_seat.count_tablets() == 0 { + tablet_seat.clear_tools(); + } + } + } +} + +#[cfg(any(feature = "winit", feature = "x11"))] +impl AnvilState { + pub fn process_input_event_windowed(&mut self, event: InputEvent, output_name: &str) { + match event { + InputEvent::Keyboard { event } => match self.keyboard_key_to_action::(event) { + KeyAction::ScaleUp => { + let output = self + .space + .outputs() + .find(|o| o.name() == output_name) + .unwrap() + .clone(); + + let current_scale = output.current_scale().fractional_scale(); + let new_scale = current_scale + 0.25; + output.change_current_state(None, None, Some(Scale::Fractional(new_scale)), None); + + crate::shell::fixup_positions(&mut self.space, self.pointer.current_location()); + self.backend_data.reset_buffers(&output); + } + + KeyAction::ScaleDown => { + let output = self + .space + .outputs() + .find(|o| o.name() == output_name) + .unwrap() + .clone(); + + let current_scale = output.current_scale().fractional_scale(); + let new_scale = f64::max(1.0, current_scale - 0.25); + output.change_current_state(None, None, Some(Scale::Fractional(new_scale)), None); + + crate::shell::fixup_positions(&mut self.space, self.pointer.current_location()); + self.backend_data.reset_buffers(&output); + } + + KeyAction::RotateOutput => { + let output = self + .space + .outputs() + .find(|o| o.name() == output_name) + .unwrap() + .clone(); + + let current_transform = output.current_transform(); + let new_transform = match current_transform { + Transform::Normal => Transform::_90, + Transform::_90 => Transform::_180, + Transform::_180 => Transform::_270, + Transform::_270 => Transform::Flipped, + Transform::Flipped => Transform::Flipped90, + Transform::Flipped90 => Transform::Flipped180, + Transform::Flipped180 => Transform::Flipped270, + Transform::Flipped270 => Transform::Normal, + }; + tracing::info!(?current_transform, ?new_transform, output = ?output.name(), "changing output transform"); + output.change_current_state(None, Some(new_transform), None, None); + crate::shell::fixup_positions(&mut self.space, self.pointer.current_location()); + self.backend_data.reset_buffers(&output); + } + + action => match action { + KeyAction::None + | KeyAction::Quit + | KeyAction::Run(_) + | KeyAction::TogglePreview + | KeyAction::ToggleDecorations => self.process_common_key_action(action), + + _ => tracing::warn!( + ?action, + output_name, + "Key action unsupported on on output backend.", + ), + }, + }, + + InputEvent::PointerMotionAbsolute { event } => { + let output = self + .space + .outputs() + .find(|o| o.name() == output_name) + .unwrap() + .clone(); + self.on_pointer_move_absolute_windowed::(event, &output) + } + InputEvent::PointerButton { event } => self.on_pointer_button::(event), + InputEvent::PointerAxis { event } => self.on_pointer_axis::(event), + + InputEvent::GestureSwipeBegin { event, .. } => self.on_gesture_swipe_begin::(event), + InputEvent::GestureSwipeUpdate { event, .. } => self.on_gesture_swipe_update::(event), + InputEvent::GestureSwipeEnd { event, .. } => self.on_gesture_swipe_end::(event), + InputEvent::GesturePinchBegin { event, .. } => self.on_gesture_pinch_begin::(event), + InputEvent::GesturePinchUpdate { event, .. } => self.on_gesture_pinch_update::(event), + InputEvent::GesturePinchEnd { event, .. } => self.on_gesture_pinch_end::(event), + InputEvent::GestureHoldBegin { event, .. } => self.on_gesture_hold_begin::(event), + InputEvent::GestureHoldEnd { event, .. } => self.on_gesture_hold_end::(event), + + InputEvent::TouchDown { event } => self.on_touch_down::(event), + InputEvent::TouchUp { event } => self.on_touch_up::(event), + InputEvent::TouchMotion { event } => self.on_touch_motion::(event), + InputEvent::TouchFrame { event } => self.on_touch_frame::(event), + InputEvent::TouchCancel { event } => self.on_touch_cancel::(event), + InputEvent::DeviceAdded { device } => self.on_device_added::(device), + InputEvent::DeviceRemoved { device } => self.on_device_removed::(device), + _ => (), // other events are not handled in anvil (yet) + } + } + + fn on_pointer_move_absolute_windowed( + &mut self, + evt: B::PointerMotionAbsoluteEvent, + output: &Output, + ) { + let output_geo = self.space.output_geometry(output).unwrap(); + + let pos = evt.position_transformed(output_geo.size) + output_geo.loc.to_f64(); + let serial = SCOUNTER.next_serial(); + + let pointer = self.pointer.clone(); + let under = self.surface_under(pos); + pointer.motion( + self, + under, + &MotionEvent { + location: pos, + serial, + time: evt.time(), + }, + ); + pointer.frame(self); + } + + pub fn release_all_keys(&mut self) { + let keyboard = self.seat.get_keyboard().unwrap(); + for keycode in keyboard.pressed_keys() { + keyboard.input( + self, + keycode, + KeyState::Released, + SCOUNTER.next_serial(), + InputTime::now(), + |_, _, _| FilterResult::Forward::, + ); + } + } +} + +#[cfg(feature = "udev")] +impl AnvilState { + pub fn process_input_event(&mut self, dh: &DisplayHandle, event: InputEvent) { + match event { + InputEvent::Keyboard { event, .. } => match self.keyboard_key_to_action::(event) { + #[cfg(feature = "udev")] + KeyAction::VtSwitch(vt) => { + info!(to = vt, "Trying to switch vt"); + if let Err(err) = self.backend_data.session.change_vt(vt) { + error!(vt, "Error switching vt: {}", err); + } + } + KeyAction::Screen(num) => { + let geometry = self + .space + .outputs() + .nth(num) + .map(|o| self.space.output_geometry(o).unwrap()); + + if let Some(geometry) = geometry { + let x = geometry.loc.x as f64 + geometry.size.w as f64 / 2.0; + let y = geometry.size.h as f64 / 2.0; + let location = (x, y).into(); + let pointer = self.pointer.clone(); + let under = self.surface_under(location); + pointer.motion( + self, + under, + &MotionEvent { + location, + serial: SCOUNTER.next_serial(), + time: InputTime::now(), + }, + ); + pointer.frame(self); + } + } + KeyAction::ScaleUp => { + let pos = self.pointer.current_location().to_i32_round(); + let output = self + .space + .outputs() + .find(|o| self.space.output_geometry(o).unwrap().contains(pos)) + .cloned(); + + if let Some(output) = output { + let (output_location, scale) = ( + self.space.output_geometry(&output).unwrap().loc, + output.current_scale().fractional_scale(), + ); + let new_scale = scale + 0.25; + output.change_current_state(None, None, Some(Scale::Fractional(new_scale)), None); + + let rescale = scale / new_scale; + let output_location = output_location.to_f64(); + let mut pointer_output_location = self.pointer.current_location() - output_location; + pointer_output_location.x *= rescale; + pointer_output_location.y *= rescale; + let pointer_location = output_location + pointer_output_location; + + crate::shell::fixup_positions(&mut self.space, pointer_location); + let pointer = self.pointer.clone(); + let under = self.surface_under(pointer_location); + pointer.motion( + self, + under, + &MotionEvent { + location: pointer_location, + serial: SCOUNTER.next_serial(), + time: InputTime::now(), + }, + ); + pointer.frame(self); + self.backend_data.reset_buffers(&output); + } + } + KeyAction::ScaleDown => { + let pos = self.pointer.current_location().to_i32_round(); + let output = self + .space + .outputs() + .find(|o| self.space.output_geometry(o).unwrap().contains(pos)) + .cloned(); + + if let Some(output) = output { + let (output_location, scale) = ( + self.space.output_geometry(&output).unwrap().loc, + output.current_scale().fractional_scale(), + ); + let new_scale = f64::max(1.0, scale - 0.25); + output.change_current_state(None, None, Some(Scale::Fractional(new_scale)), None); + + let rescale = scale / new_scale; + let output_location = output_location.to_f64(); + let mut pointer_output_location = self.pointer.current_location() - output_location; + pointer_output_location.x *= rescale; + pointer_output_location.y *= rescale; + let pointer_location = output_location + pointer_output_location; + + crate::shell::fixup_positions(&mut self.space, pointer_location); + let pointer = self.pointer.clone(); + let under = self.surface_under(pointer_location); + pointer.motion( + self, + under, + &MotionEvent { + location: pointer_location, + serial: SCOUNTER.next_serial(), + time: InputTime::now(), + }, + ); + pointer.frame(self); + self.backend_data.reset_buffers(&output); + } + } + KeyAction::RotateOutput => { + let pos = self.pointer.current_location().to_i32_round(); + let output = self + .space + .outputs() + .find(|o| self.space.output_geometry(o).unwrap().contains(pos)) + .cloned(); + + if let Some(output) = output { + let current_transform = output.current_transform(); + let new_transform = match current_transform { + Transform::Normal => Transform::_90, + Transform::_90 => Transform::_180, + Transform::_180 => Transform::_270, + Transform::_270 => Transform::Flipped, + Transform::Flipped => Transform::Flipped90, + Transform::Flipped90 => Transform::Flipped180, + Transform::Flipped180 => Transform::Flipped270, + Transform::Flipped270 => Transform::Normal, + }; + output.change_current_state(None, Some(new_transform), None, None); + crate::shell::fixup_positions(&mut self.space, self.pointer.current_location()); + self.backend_data.reset_buffers(&output); + } + } + KeyAction::ToggleTint => { + let mut debug_flags = self.backend_data.debug_flags(); + debug_flags.toggle(DebugFlags::TINT); + self.backend_data.set_debug_flags(debug_flags); + } + + action => match action { + KeyAction::None + | KeyAction::Quit + | KeyAction::Run(_) + | KeyAction::TogglePreview + | KeyAction::ToggleDecorations => self.process_common_key_action(action), + + _ => unreachable!(), + }, + }, + InputEvent::PointerMotion { event, .. } => self.on_pointer_move::(dh, event), + InputEvent::PointerMotionAbsolute { event, .. } => self.on_pointer_move_absolute::(dh, event), + InputEvent::PointerButton { event, .. } => self.on_pointer_button::(event), + InputEvent::PointerAxis { event, .. } => self.on_pointer_axis::(event), + InputEvent::TabletToolAxis { event, .. } => self.on_tablet_tool_axis::(event), + InputEvent::TabletToolProximity { event, .. } => self.on_tablet_tool_proximity::(dh, event), + InputEvent::TabletToolTip { event, .. } => self.on_tablet_tool_tip::(event), + InputEvent::TabletToolButton { event, .. } => self.on_tablet_button::(event), + InputEvent::GestureSwipeBegin { event, .. } => self.on_gesture_swipe_begin::(event), + InputEvent::GestureSwipeUpdate { event, .. } => self.on_gesture_swipe_update::(event), + InputEvent::GestureSwipeEnd { event, .. } => self.on_gesture_swipe_end::(event), + InputEvent::GesturePinchBegin { event, .. } => self.on_gesture_pinch_begin::(event), + InputEvent::GesturePinchUpdate { event, .. } => self.on_gesture_pinch_update::(event), + InputEvent::GesturePinchEnd { event, .. } => self.on_gesture_pinch_end::(event), + InputEvent::GestureHoldBegin { event, .. } => self.on_gesture_hold_begin::(event), + InputEvent::GestureHoldEnd { event, .. } => self.on_gesture_hold_end::(event), + + InputEvent::TouchDown { event } => self.on_touch_down::(event), + InputEvent::TouchUp { event } => self.on_touch_up::(event), + InputEvent::TouchMotion { event } => self.on_touch_motion::(event), + InputEvent::TouchFrame { event } => self.on_touch_frame::(event), + InputEvent::TouchCancel { event } => self.on_touch_cancel::(event), + + InputEvent::DeviceAdded { device } => self.on_device_added::(device), + InputEvent::DeviceRemoved { device } => self.on_device_removed::(device), + _ => { + // other events are not handled in anvil (yet) + } + } + } + + fn on_pointer_move(&mut self, _dh: &DisplayHandle, evt: B::PointerMotionEvent) { + let mut pointer_location = self.pointer.current_location(); + let serial = SCOUNTER.next_serial(); + + let pointer = self.pointer.clone(); + let under = self.surface_under(pointer_location); + + let mut pointer_locked = false; + let mut pointer_confined = false; + let mut confine_region = None; + if let Some((surface, surface_loc)) = under + .as_ref() + .and_then(|(target, l)| Some((target.wl_surface()?, l))) + { + with_pointer_constraint(&surface, &pointer, |constraint| match constraint { + Some(constraint) if constraint.is_active() => { + // Constraint does not apply if not within region + if !constraint + .region() + .is_none_or(|x| x.contains((pointer_location - *surface_loc).to_i32_round())) + { + return; + } + match &*constraint { + PointerConstraint::Locked(_locked) => { + pointer_locked = true; + } + PointerConstraint::Confined(confine) => { + pointer_confined = true; + confine_region = confine.region().cloned(); + } + } + } + _ => {} + }); + } + + pointer.relative_motion( + self, + under.clone(), + &RelativeMotionEvent { + delta: evt.delta(), + delta_unaccel: evt.delta_unaccel(), + time: evt.time(), + }, + ); + + // If pointer is locked, only emit relative motion + if pointer_locked { + pointer.frame(self); + return; + } + + // Clamp delta, such that the pointer never moves out of bounds + let mut delta = self.clamp_coords(pointer_location + evt.delta()) - pointer_location; + + // Clamp the individual x and y components of delta to respect confinement regions + if pointer_confined { + if let Some((_, surface_loc)) = &under { + if let Some(region) = &confine_region { + // Clamp delta.x + if !region.contains( + (pointer_location + Point::new(delta.x, 0f64) - *surface_loc).to_i32_round(), + ) { + delta.x = 0f64; + } + + // Clamp delta.y + if !region.contains( + (pointer_location + Point::new(0f64, delta.y) - *surface_loc).to_i32_round(), + ) { + delta.y = 0f64; + } + } + } + } + + pointer_location += delta; + + let new_under = self.surface_under(pointer_location); + + // If confined, don't move pointer if it would go outside surface + if pointer_confined { + if let Some((surface, _)) = &under { + if new_under.as_ref().and_then(|(under, _)| under.wl_surface()) != surface.wl_surface() { + pointer.frame(self); + return; + } + } + } + + pointer.motion( + self, + under, + &MotionEvent { + location: pointer_location, + serial, + time: evt.time(), + }, + ); + pointer.frame(self); + + // If pointer is now in a constraint region, activate it + // TODO Anywhere else pointer is moved needs to do this + if let Some((under, surface_location)) = + new_under.and_then(|(target, loc)| Some((target.wl_surface()?.into_owned(), loc))) + { + with_pointer_constraint(&under, &pointer, |constraint| match constraint { + Some(constraint) if !constraint.is_active() => { + let point = (pointer_location - surface_location).to_i32_round(); + if constraint.region().is_none_or(|region| region.contains(point)) { + constraint.activate(); + } + } + _ => {} + }); + } + } + + fn on_pointer_move_absolute( + &mut self, + _dh: &DisplayHandle, + evt: B::PointerMotionAbsoluteEvent, + ) { + let serial = SCOUNTER.next_serial(); + + let max_x = self + .space + .outputs() + .fold(0, |acc, o| acc + self.space.output_geometry(o).unwrap().size.w); + + let max_h_output = self + .space + .outputs() + .max_by_key(|o| self.space.output_geometry(o).unwrap().size.h) + .unwrap(); + + let max_y = self.space.output_geometry(max_h_output).unwrap().size.h; + + let mut pointer_location = (evt.x_transformed(max_x), evt.y_transformed(max_y)).into(); + + // clamp to screen limits + pointer_location = self.clamp_coords(pointer_location); + + let pointer = self.pointer.clone(); + let under = self.surface_under(pointer_location); + + pointer.motion( + self, + under, + &MotionEvent { + location: pointer_location, + serial, + time: evt.time(), + }, + ); + pointer.frame(self); + } + + fn on_tablet_tool_axis(&mut self, evt: B::TabletToolAxisEvent) { + let tablet_seat = self.seat.tablet_seat(); + + if let Some(pointer_location) = self.touch_location_transformed(&evt) { + let pointer = self.pointer.clone(); + let under = self.surface_under(pointer_location); + let tool = tablet_seat.get_tool(&evt.tool()); + let time = InputTime::now(); + + pointer.motion( + self, + under.clone(), + &MotionEvent { + location: pointer_location, + serial: SCOUNTER.next_serial(), + time, + }, + ); + + if let Some(tool) = tool { + let frame = tablet::tool::AxisFrame { + pressure: evt.pressure_has_changed().then(|| evt.pressure()), + distance: evt.distance_has_changed().then(|| evt.distance()), + tilt: evt.tilt_has_changed().then(|| evt.tilt()), + rotation: evt.rotation_has_changed().then(|| evt.rotation()), + slider: evt.slider_has_changed().then(|| evt.slider_position()), + wheel: evt + .wheel_has_changed() + .then(|| (evt.wheel_delta(), evt.wheel_delta_discrete())), + }; + + tool.axis(self, frame); + + tool.motion( + self, + under, + &tablet::tool::MotionEvent { + location: pointer_location, + serial: SCOUNTER.next_serial(), + time, + }, + ); + + tool.frame(self, time); + } + + pointer.frame(self); + } + } + + fn on_tablet_tool_proximity( + &mut self, + dh: &DisplayHandle, + evt: B::TabletToolProximityEvent, + ) { + let tablet_seat = self.seat.tablet_seat(); + + if let Some(pointer_location) = self.touch_location_transformed(&evt) { + let tool = evt.tool(); + + let pointer = self.pointer.clone(); + let under = self.surface_under(pointer_location); + let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&evt.device())); + let tool = tablet_seat + .get_tool(&tool) + .unwrap_or_else(|| tablet_seat.add_wp_tool(self, dh, &tool)); + + pointer.motion( + self, + under.clone(), + &MotionEvent { + location: pointer_location, + serial: SCOUNTER.next_serial(), + time: evt.time(), + }, + ); + pointer.frame(self); + + if let Some(tablet) = tablet { + let frame = tablet::tool::AxisFrame { + pressure: evt.pressure_has_changed().then(|| evt.pressure()), + distance: evt.distance_has_changed().then(|| evt.distance()), + tilt: evt.tilt_has_changed().then(|| evt.tilt()), + rotation: evt.rotation_has_changed().then(|| evt.rotation()), + slider: evt.slider_has_changed().then(|| evt.slider_position()), + wheel: evt + .wheel_has_changed() + .then(|| (evt.wheel_delta(), evt.wheel_delta_discrete())), + }; + + match evt.state() { + ProximityState::In => { + tool.proximity_in( + self, + under, + tablet, + &tablet::tool::ProximityInEvent { + location: pointer_location, + axis: Some(frame), + serial: SCOUNTER.next_serial(), + time: evt.time(), + }, + ); + } + ProximityState::Out => { + tool.proximity_out( + self, + &tablet::tool::ProximityOutEvent { + serial: SCOUNTER.next_serial(), + time: evt.time(), + }, + ); + } + } + + // Doing this in an idle handler would allow other events (e.g. buttons) to be + // sent as part of the same frame, which is closer to what the protocol + // expect, and let well behaved clients accumulate events. + tool.frame(self, evt.time()); + } + } + } + + fn on_tablet_tool_tip(&mut self, evt: B::TabletToolTipEvent) { + let tool = self.seat.tablet_seat().get_tool(&evt.tool()); + + if let Some(tool) = tool { + let serial = SCOUNTER.next_serial(); + + match evt.tip_state() { + TabletToolTipState::Down => { + tool.down( + self, + &tablet::tool::DownEvent { + serial, + time: evt.time(), + }, + ); + + // change the keyboard focus + self.update_keyboard_focus(self.pointer.current_location(), serial); + } + TabletToolTipState::Up => { + tool.up( + self, + &tablet::tool::UpEvent { + serial, + time: evt.time(), + }, + ); + } + } + + tool.frame(self, evt.time()); + } + } + + fn on_tablet_button(&mut self, evt: B::TabletToolButtonEvent) { + let tool = self.seat.tablet_seat().get_tool(&evt.tool()); + + if let Some(tool) = tool { + tool.button( + self, + &tablet::tool::ButtonEvent { + serial: SCOUNTER.next_serial(), + button: evt.button(), + state: evt.button_state(), + time: evt.time(), + }, + ); + + tool.frame(self, evt.time()); + } + } + + fn clamp_coords(&self, pos: Point) -> Point { + if self.space.outputs().next().is_none() { + return pos; + } + + let (pos_x, pos_y) = pos.into(); + let max_x = self + .space + .outputs() + .fold(0, |acc, o| acc + self.space.output_geometry(o).unwrap().size.w); + let clamped_x = pos_x.clamp(0.0, max_x as f64); + let max_y = self + .space + .outputs() + .find(|o| { + let geo = self.space.output_geometry(o).unwrap(); + geo.contains((clamped_x as i32, 0)) + }) + .map(|o| self.space.output_geometry(o).unwrap().size.h); + + if let Some(max_y) = max_y { + let clamped_y = pos_y.clamp(0.0, max_y as f64); + (clamped_x, clamped_y).into() + } else { + (clamped_x, pos_y).into() + } + } +} + +/// Possible results of a keyboard action +#[allow(dead_code)] // some of these are only read if udev is enabled +#[derive(Debug)] +enum KeyAction { + /// Quit the compositor + Quit, + /// Trigger a vt-switch + VtSwitch(i32), + /// run a command + Run(String), + /// Switch the current screen + Screen(usize), + ScaleUp, + ScaleDown, + TogglePreview, + RotateOutput, + ToggleTint, + ToggleDecorations, + /// Do nothing more + None, +} + +fn process_keyboard_shortcut(modifiers: ModifiersState, keysym: Keysym) -> Option { + if modifiers.ctrl && modifiers.alt && keysym == Keysym::BackSpace || modifiers.logo && keysym == Keysym::q + { + // ctrl+alt+backspace = quit + // logo + q = quit + Some(KeyAction::Quit) + } else if (xkb::KEY_XF86Switch_VT_1..=xkb::KEY_XF86Switch_VT_12).contains(&keysym.raw()) { + // VTSwitch + Some(KeyAction::VtSwitch( + (keysym.raw() - xkb::KEY_XF86Switch_VT_1 + 1) as i32, + )) + } else if modifiers.logo && keysym == Keysym::Return { + // run terminal + Some(KeyAction::Run("weston-terminal".into())) + } else if modifiers.logo && (xkb::KEY_1..=xkb::KEY_9).contains(&keysym.raw()) { + Some(KeyAction::Screen((keysym.raw() - xkb::KEY_1) as usize)) + } else if modifiers.logo && modifiers.shift && keysym == Keysym::M { + Some(KeyAction::ScaleDown) + } else if modifiers.logo && modifiers.shift && keysym == Keysym::P { + Some(KeyAction::ScaleUp) + } else if modifiers.logo && modifiers.shift && keysym == Keysym::W { + Some(KeyAction::TogglePreview) + } else if modifiers.logo && modifiers.shift && keysym == Keysym::R { + Some(KeyAction::RotateOutput) + } else if modifiers.logo && modifiers.shift && keysym == Keysym::T { + Some(KeyAction::ToggleTint) + } else if modifiers.logo && modifiers.shift && keysym == Keysym::D { + Some(KeyAction::ToggleDecorations) + } else { + None + } +} + +impl VirtualKeyboardHandler for AnvilState { + fn process_virtual_keyboard_event(&mut self, event: InputEvent) { + let keyboard = self.seat.get_keyboard().unwrap(); + + match event { + // The device's keycodes only mean anything under its own keymap, so + // activate it before processing events. Anvil never switches back; + // a real compositor should track which device the seat keymap came + // from and restore its configured one when a physical key arrives. + InputEvent::DeviceAdded { device } + | InputEvent::Special(VirtualKeyboardSpecialEvent::KeymapChanged { device }) => { + let Some(keymap) = device.keymap() else { + return; + }; + if let Err(err) = keyboard.set_keymap_from_string(self, keymap.to_string()) { + warn!(?err, "Failed to apply virtual keyboard keymap"); + } + } + InputEvent::Keyboard { event } => { + match self.keyboard_key_to_action::(event) { + action @ (KeyAction::None + | KeyAction::Quit + | KeyAction::Run(_) + | KeyAction::TogglePreview + | KeyAction::ToggleDecorations) => self.process_common_key_action(action), + // VT switching and output changes are out-of-scope for virtual keyboards. + action => debug!(?action, "ignoring key action from a virtual keyboard"), + } + } + InputEvent::Special(VirtualKeyboardSpecialEvent::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + .. + }) => { + keyboard.with_xkb_state(self, |mut context| { + context.set_modifier_mask(mods_depressed, mods_latched, mods_locked, group) + }); + } + _ => (), + } + } +} diff --git a/vendor/smithay/anvil/src/lib.rs b/vendor/smithay/anvil/src/lib.rs new file mode 100644 index 0000000..a4ff6cb --- /dev/null +++ b/vendor/smithay/anvil/src/lib.rs @@ -0,0 +1,27 @@ +#![warn(rust_2018_idioms)] +#![allow(clippy::collapsible_match)] +// If no backend is enabled, a large portion of the codebase is unused. +// So silence this useless warning for the CI. +#![cfg_attr( + not(any(feature = "winit", feature = "x11", feature = "udev")), + allow(dead_code, unused_imports) +)] + +#[cfg(any(feature = "udev", feature = "xwayland"))] +pub mod cursor; +pub mod drawing; +pub mod focus; +pub mod input_handler; +#[cfg(feature = "libei")] +pub mod libei; +pub mod render; +pub mod shell; +pub mod state; +#[cfg(feature = "udev")] +pub mod udev; +#[cfg(feature = "winit")] +pub mod winit; +#[cfg(feature = "x11")] +pub mod x11; + +pub use state::{AnvilState, ClientState}; diff --git a/vendor/smithay/anvil/src/libei.rs b/vendor/smithay/anvil/src/libei.rs new file mode 100644 index 0000000..0890c1f --- /dev/null +++ b/vendor/smithay/anvil/src/libei.rs @@ -0,0 +1,50 @@ +use smithay::{ + backend::libei::{EiInput, EiInputEvent}, + input::keyboard::XkbConfig, + reexports::{ + calloop, + reis::{calloop::EisListenerSource, eis}, + }, +}; + +use crate::{state::AnvilState, udev::UdevData}; + +pub fn listen_eis(handle: &calloop::LoopHandle<'static, AnvilState>) { + let listener = match eis::Listener::bind_auto() { + Ok(listener) => listener, + Err(err) => { + tracing::error!("Failed to bind EI listener socket: {}", err); + return; + } + }; + + unsafe { std::env::set_var("LIBEI_SOCKET", listener.path()) }; + + let listener_source = EisListenerSource::new(listener); + let handle_clone = handle.clone(); + handle + .insert_source(listener_source, move |context, _, _| { + let source = EiInput::new(context); + handle_clone + .insert_source(source, |event, connection, data| match event { + EiInputEvent::Connected => { + let seat = connection.add_seat("default"); + let _ = seat.add_keyboard("virtual keyboard", XkbConfig::default()); + seat.add_pointer("virtual pointer"); + seat.add_pointer_absolute("virtual absolute pointer", &[]); + seat.add_touch("virtual touch", &[]); + } + EiInputEvent::Disconnected => {} + EiInputEvent::Event(event) => { + let dh = data.display_handle.clone(); + data.process_input_event(&dh, event); + } + EiInputEvent::TextKeysym { .. } | EiInputEvent::TextUtf8 { .. } => { + // Anvil doesn't add a text device to the libei seat + } + }) + .unwrap(); + Ok(calloop::PostAction::Continue) + }) + .unwrap(); +} diff --git a/vendor/smithay/anvil/src/main.rs b/vendor/smithay/anvil/src/main.rs new file mode 100644 index 0000000..941496a --- /dev/null +++ b/vendor/smithay/anvil/src/main.rs @@ -0,0 +1,69 @@ +static POSSIBLE_BACKENDS: &[&str] = &[ + #[cfg(feature = "winit")] + "--winit : Run anvil as a X11 or Wayland client using winit.", + #[cfg(feature = "udev")] + "--tty-udev : Run anvil as a tty udev client (requires root if without logind).", + #[cfg(feature = "x11")] + "--x11 : Run anvil as an X11 client.", +]; + +#[cfg(feature = "profile-with-tracy-mem")] +#[global_allocator] +static GLOBAL: profiling::tracy_client::ProfiledAllocator = + profiling::tracy_client::ProfiledAllocator::new(std::alloc::System, 10); + +// Allow in this function because of existing usage +#[allow(clippy::uninlined_format_args)] +fn main() { + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt() + .compact() + .with_env_filter(env_filter) + .init(); + } else { + tracing_subscriber::fmt().compact().init(); + } + + #[cfg(feature = "profile-with-tracy")] + profiling::tracy_client::Client::start(); + + profiling::register_thread!("Main Thread"); + + #[cfg(feature = "profile-with-puffin")] + let _server = puffin_http::Server::new(&format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT)).unwrap(); + #[cfg(feature = "profile-with-puffin")] + profiling::puffin::set_scopes_on(true); + + let arg = ::std::env::args().nth(1); + match arg.as_ref().map(|s| &s[..]) { + #[cfg(feature = "winit")] + Some("--winit") => { + tracing::info!("Starting anvil with winit backend"); + anvil::winit::run_winit(); + } + #[cfg(feature = "udev")] + Some("--tty-udev") => { + tracing::info!("Starting anvil on a tty using udev"); + anvil::udev::run_udev(); + } + #[cfg(feature = "x11")] + Some("--x11") => { + tracing::info!("Starting anvil with x11 backend"); + anvil::x11::run_x11(); + } + Some(other) => { + tracing::error!("Unknown backend: {}", other); + } + None => { + #[allow(clippy::disallowed_macros)] + { + println!("USAGE: anvil --backend"); + println!(); + println!("Possible backends are:"); + for b in POSSIBLE_BACKENDS { + println!("\t{b}"); + } + } + } + } +} diff --git a/vendor/smithay/anvil/src/render.rs b/vendor/smithay/anvil/src/render.rs new file mode 100644 index 0000000..2aba9e6 --- /dev/null +++ b/vendor/smithay/anvil/src/render.rs @@ -0,0 +1,210 @@ +use smithay::{ + backend::renderer::{ + Color32F, ImportAll, ImportMem, Renderer, + damage::{Error as OutputDamageTrackerError, OutputDamageTracker, RenderOutputResult}, + element::{ + AsRenderElements, RenderElement, Wrap, + surface::WaylandSurfaceRenderElement, + utils::{ + ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, RelocateRenderElement, + RescaleRenderElement, + }, + }, + }, + desktop::space::{ + ConstrainBehavior, ConstrainReference, Space, SpaceRenderElements, constrain_space_element, + }, + output::Output, + utils::{Point, Rectangle, Size}, +}; + +#[cfg(feature = "debug")] +use crate::drawing::FpsElement; +use crate::{ + drawing::{CLEAR_COLOR, CLEAR_COLOR_FULLSCREEN, PointerRenderElement}, + shell::{FullscreenSurface, WindowElement, WindowRenderElement}, +}; + +smithay::backend::renderer::element::render_elements! { + pub CustomRenderElements where + R: ImportAll + ImportMem; + Pointer=PointerRenderElement, + Surface=WaylandSurfaceRenderElement, + #[cfg(feature = "debug")] + // Note: We would like to borrow this element instead, but that would introduce + // a feature-dependent lifetime, which introduces a lot more feature bounds + // as the whole type changes and we can't have an unused lifetime (for when "debug" is disabled) + // in the declaration. + Fps=FpsElement, +} + +impl std::fmt::Debug for CustomRenderElements { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pointer(arg0) => f.debug_tuple("Pointer").field(arg0).finish(), + Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(), + #[cfg(feature = "debug")] + Self::Fps(arg0) => f.debug_tuple("Fps").field(arg0).finish(), + Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(), + } + } +} + +smithay::backend::renderer::element::render_elements! { + pub OutputRenderElements where R: ImportAll + ImportMem; + Space=SpaceRenderElements, + Window=Wrap, + Custom=CustomRenderElements, + Preview=CropRenderElement>>>, +} + +impl + std::fmt::Debug> std::fmt::Debug + for OutputRenderElements +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Space(arg0) => f.debug_tuple("Space").field(arg0).finish(), + Self::Window(arg0) => f.debug_tuple("Window").field(arg0).finish(), + Self::Custom(arg0) => f.debug_tuple("Custom").field(arg0).finish(), + Self::Preview(arg0) => f.debug_tuple("Preview").field(arg0).finish(), + Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(), + } + } +} + +pub fn space_preview_elements<'a, R, C>( + renderer: &'a mut R, + space: &'a Space, + output: &'a Output, +) -> impl Iterator + 'a +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Clone + 'static, + C: From>>>> + 'a, +{ + let constrain_behavior = ConstrainBehavior { + reference: ConstrainReference::BoundingBox, + behavior: ConstrainScaleBehavior::Fit, + align: ConstrainAlign::CENTER, + }; + + let preview_padding = 10; + + let elements_on_space = space.elements_for_output(output).count(); + let output_scale = output.current_scale().fractional_scale(); + let output_transform = output.current_transform(); + let output_size = output + .current_mode() + .map(|mode| { + output_transform + .transform_size(mode.size) + .to_f64() + .to_logical(output_scale) + }) + .unwrap_or_default(); + + let max_elements_per_row = 4; + let elements_per_row = usize::min(elements_on_space, max_elements_per_row); + let rows = f64::ceil(elements_on_space as f64 / elements_per_row as f64); + + let preview_size = Size::from(( + f64::round(output_size.w / elements_per_row as f64) as i32 - preview_padding * 2, + f64::round(output_size.h / rows) as i32 - preview_padding * 2, + )); + + space + .elements_for_output(output) + .enumerate() + .flat_map(move |(element_index, window)| { + let column = element_index % elements_per_row; + let row = element_index / elements_per_row; + let preview_location = Point::from(( + preview_padding + (preview_padding + preview_size.w) * column as i32, + preview_padding + (preview_padding + preview_size.h) * row as i32, + )); + let constrain = Rectangle::new(preview_location, preview_size); + constrain_space_element( + renderer, + window, + preview_location, + 1.0, + output_scale, + constrain, + constrain_behavior, + ) + }) +} + +#[profiling::function] +pub fn output_elements( + output: &Output, + space: &Space, + custom_elements: impl IntoIterator>, + renderer: &mut R, + show_window_preview: bool, +) -> (Vec>>, Color32F) +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Clone + 'static, +{ + if let Some(window) = output + .user_data() + .get::() + .and_then(|f| f.get()) + { + let scale = output.current_scale().fractional_scale().into(); + let window_render_elements: Vec> = + AsRenderElements::::render_elements(&window, renderer, (0, 0).into(), scale, 1.0); + + let elements = custom_elements + .into_iter() + .map(OutputRenderElements::from) + .chain( + window_render_elements + .into_iter() + .map(|e| OutputRenderElements::Window(Wrap::from(e))), + ) + .collect::>(); + (elements, CLEAR_COLOR_FULLSCREEN) + } else { + let mut output_render_elements = custom_elements + .into_iter() + .map(OutputRenderElements::from) + .collect::>(); + + if show_window_preview && space.elements_for_output(output).count() > 0 { + output_render_elements.extend(space_preview_elements(renderer, space, output)); + } + + let space_elements = smithay::desktop::space::space_render_elements::<_, WindowElement, _>( + renderer, + [space], + output, + 1.0, + ) + .expect("output without mode?"); + output_render_elements.extend(space_elements.into_iter().map(OutputRenderElements::Space)); + + (output_render_elements, CLEAR_COLOR) + } +} + +#[allow(clippy::too_many_arguments)] +pub fn render_output<'a, 'd, R>( + output: &'a Output, + space: &'a Space, + custom_elements: impl IntoIterator>, + renderer: &'a mut R, + framebuffer: &'a mut R::Framebuffer<'_>, + damage_tracker: &'d mut OutputDamageTracker, + age: usize, + show_window_preview: bool, +) -> Result, OutputDamageTrackerError> +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Clone + 'static, +{ + let (elements, clear_color) = + output_elements(output, space, custom_elements, renderer, show_window_preview); + damage_tracker.render_output(renderer, framebuffer, age, &elements, clear_color) +} diff --git a/vendor/smithay/anvil/src/shell/element.rs b/vendor/smithay/anvil/src/shell/element.rs new file mode 100644 index 0000000..8740b3a --- /dev/null +++ b/vendor/smithay/anvil/src/shell/element.rs @@ -0,0 +1,564 @@ +use std::{borrow::Cow, time::Duration}; + +use smithay::{ + backend::{ + input::InputTime, + renderer::{ + ImportAll, ImportMem, Renderer, Texture, + element::{ + AsRenderElements, solid::SolidColorRenderElement, surface::WaylandSurfaceRenderElement, + }, + }, + }, + desktop::{ + Window, WindowSurface, WindowSurfaceType, space::SpaceElement, utils::OutputPresentationFeedback, + }, + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, MotionEvent, PointerTarget, RelativeMotionEvent, + }, + tablet::tool::TabletToolTarget, + touch::{FrameMarker, TouchTarget}, + }, + output::Output, + reexports::{ + wayland_protocols::wp::presentation_time::server::wp_presentation_feedback, + wayland_server::protocol::wl_surface::WlSurface, + }, + render_elements, + utils::{IsAlive, Logical, Physical, Point, Rectangle, Scale, Serial, user_data::UserDataMap}, + wayland::{compositor::SurfaceData as WlSurfaceData, dmabuf::DmabufFeedback, seat::WaylandFocus}, +}; + +use super::ssd::HEADER_BAR_HEIGHT; +use crate::{AnvilState, focus::PointerFocusTarget, state::Backend}; + +#[derive(Debug, Clone, PartialEq)] +pub struct WindowElement(pub Window); + +impl WindowElement { + pub fn surface_under( + &self, + location: Point, + window_type: WindowSurfaceType, + ) -> Option<(PointerFocusTarget, Point)> { + let state = self.decoration_state(); + if state.is_ssd && location.y < HEADER_BAR_HEIGHT as f64 { + return Some((PointerFocusTarget::SSD(SSD(self.clone())), Point::default())); + } + let offset = if state.is_ssd { + Point::from((0, HEADER_BAR_HEIGHT)) + } else { + Point::default() + }; + + let surface_under = self.0.surface_under(location - offset.to_f64(), window_type); + let (under, loc) = match self.0.underlying_surface() { + WindowSurface::Wayland(_) => { + surface_under.map(|(surface, loc)| (PointerFocusTarget::WlSurface(surface), loc)) + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => { + surface_under.map(|(_, loc)| (PointerFocusTarget::X11Surface(s.clone()), loc)) + } + }?; + Some((under, loc + offset)) + } + + pub fn with_surfaces(&self, processor: F) + where + F: FnMut(&WlSurface, &WlSurfaceData), + { + self.0.with_surfaces(processor); + } + + pub fn send_frame( + &self, + output: &Output, + time: T, + throttle: Option, + primary_scan_out_output: F, + ) where + T: Into, + F: FnMut(&WlSurface, &WlSurfaceData) -> Option + Copy, + { + self.0.send_frame(output, time, throttle, primary_scan_out_output) + } + + pub fn send_dmabuf_feedback<'a, P, F>( + &self, + output: &Output, + primary_scan_out_output: P, + select_dmabuf_feedback: F, + ) where + P: FnMut(&WlSurface, &WlSurfaceData) -> Option + Copy, + F: Fn(&WlSurface, &WlSurfaceData) -> &'a DmabufFeedback + Copy, + { + self.0 + .send_dmabuf_feedback(output, primary_scan_out_output, select_dmabuf_feedback) + } + + pub fn take_presentation_feedback( + &self, + output_feedback: &mut OutputPresentationFeedback, + primary_scan_out_output: F1, + presentation_feedback_flags: F2, + ) where + F1: FnMut(&WlSurface, &WlSurfaceData) -> Option + Copy, + F2: FnMut(&WlSurface, &WlSurfaceData) -> wp_presentation_feedback::Kind + Copy, + { + self.0.take_presentation_feedback( + output_feedback, + primary_scan_out_output, + presentation_feedback_flags, + ) + } + + #[cfg(feature = "xwayland")] + #[inline] + pub fn is_x11(&self) -> bool { + self.0.is_x11() + } + + #[inline] + pub fn is_wayland(&self) -> bool { + self.0.is_wayland() + } + + #[inline] + pub fn wl_surface(&self) -> Option> { + self.0.wl_surface() + } + + #[inline] + pub fn user_data(&self) -> &UserDataMap { + self.0.user_data() + } +} + +impl IsAlive for WindowElement { + #[inline] + fn alive(&self) -> bool { + self.0.alive() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SSD(WindowElement); + +impl IsAlive for SSD { + #[inline] + fn alive(&self) -> bool { + self.0.alive() + } +} + +impl WaylandFocus for SSD { + #[inline] + fn wl_surface(&self) -> Option> { + self.0.wl_surface() + } +} + +impl PointerTarget> for SSD { + fn enter( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + event: &MotionEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.pointer_enter(event.location); + } + } + fn motion( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + event: &MotionEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.pointer_enter(event.location); + } + } + fn relative_motion( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &RelativeMotionEvent, + ) { + } + fn button( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &ButtonEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.clicked(seat, data, &self.0, event.serial); + } + } + fn axis( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _frame: AxisFrame, + ) { + } + fn frame(&self, _seat: &Seat>, _data: &mut AnvilState) {} + fn leave( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _serial: Serial, + _time: InputTime, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.pointer_leave(); + } + } + fn gesture_swipe_begin( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GestureSwipeBeginEvent, + ) { + } + fn gesture_swipe_update( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GestureSwipeUpdateEvent, + ) { + } + fn gesture_swipe_end( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GestureSwipeEndEvent, + ) { + } + fn gesture_pinch_begin( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GesturePinchBeginEvent, + ) { + } + fn gesture_pinch_update( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GesturePinchUpdateEvent, + ) { + } + fn gesture_pinch_end( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GesturePinchEndEvent, + ) { + } + fn gesture_hold_begin( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GestureHoldBeginEvent, + ) { + } + fn gesture_hold_end( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &GestureHoldEndEvent, + ) { + } +} + +impl TouchTarget> for SSD { + fn down( + &self, + seat: &Seat>, + data: &mut AnvilState, + event: &smithay::input::touch::DownEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.pointer_enter(event.location); + state.header_bar.touch_down(seat, data, &self.0, event.serial); + } + } + + fn up( + &self, + seat: &Seat>, + data: &mut AnvilState, + _event: &smithay::input::touch::UpEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.touch_up(seat, data, &self.0); + } + } + + fn motion( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + event: &smithay::input::touch::MotionEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.pointer_enter(event.location); + } + } + + fn frame( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _marker: FrameMarker, + ) { + } + + fn cancel( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _marker: FrameMarker, + ) { + } + + fn shape( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &smithay::input::touch::ShapeEvent, + ) { + } + + fn orientation( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _event: &smithay::input::touch::OrientationEvent, + ) { + } + + fn last_frame( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + ) -> Option { + // It would be more correct to store the marker on frame and cancel, + // but since we're ignoring those anyway, no need for the added complexity. + None + } +} + +impl TabletToolTarget> for SSD { + fn proximity_in( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + _tablet: &smithay::input::tablet::Tablet, + _serial: Serial, + ) { + } + + fn proximity_out( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.pointer_leave(); + } + } + + fn down( + &self, + seat: &Seat>, + data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + event: &smithay::input::tablet::tool::DownEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.touch_down(seat, data, &self.0, event.serial); + } + } + + fn up( + &self, + seat: &Seat>, + data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + _event: &smithay::input::tablet::tool::UpEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.touch_up(seat, data, &self.0); + } + } + + fn motion( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + event: &smithay::input::tablet::tool::MotionEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.pointer_enter(event.location); + } + } + + fn button( + &self, + seat: &Seat>, + data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + event: &smithay::input::tablet::tool::ButtonEvent, + ) { + let mut state = self.0.decoration_state(); + if state.is_ssd { + state.header_bar.clicked(seat, data, &self.0, event.serial); + } + } + + fn axis( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + _frame: smithay::input::tablet::tool::AxisFrame, + ) { + } + + fn frame( + &self, + _seat: &Seat>, + _data: &mut AnvilState, + _tool_descriptor: &smithay::backend::input::TabletToolDescriptor, + _time: InputTime, + ) { + } +} + +impl SpaceElement for WindowElement { + fn geometry(&self) -> Rectangle { + let mut geo = SpaceElement::geometry(&self.0); + if self.decoration_state().is_ssd { + geo.size.h += HEADER_BAR_HEIGHT; + } + geo + } + fn bbox(&self) -> Rectangle { + let mut bbox = SpaceElement::bbox(&self.0); + if self.decoration_state().is_ssd { + bbox.size.h += HEADER_BAR_HEIGHT; + } + bbox + } + fn is_in_input_region(&self, point: &Point) -> bool { + if self.decoration_state().is_ssd { + point.y < HEADER_BAR_HEIGHT as f64 + || SpaceElement::is_in_input_region( + &self.0, + &(*point - Point::from((0.0, HEADER_BAR_HEIGHT as f64))), + ) + } else { + SpaceElement::is_in_input_region(&self.0, point) + } + } + fn z_index(&self) -> u8 { + SpaceElement::z_index(&self.0) + } + + fn set_activate(&self, activated: bool) { + SpaceElement::set_activate(&self.0, activated); + } + fn output_enter(&self, output: &Output, overlap: Rectangle) { + SpaceElement::output_enter(&self.0, output, overlap); + } + fn output_leave(&self, output: &Output) { + SpaceElement::output_leave(&self.0, output); + } + #[profiling::function] + fn refresh(&self) { + SpaceElement::refresh(&self.0); + } +} + +render_elements!( + pub WindowRenderElement where R: ImportAll + ImportMem; + Window=WaylandSurfaceRenderElement, + Decoration=SolidColorRenderElement, +); + +impl std::fmt::Debug for WindowRenderElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Window(arg0) => f.debug_tuple("Window").field(arg0).finish(), + Self::Decoration(arg0) => f.debug_tuple("Decoration").field(arg0).finish(), + Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(), + } + } +} + +impl AsRenderElements for WindowElement +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Clone + Texture + 'static, +{ + type RenderElement = WindowRenderElement; + + fn render_elements>( + &self, + renderer: &mut R, + mut location: Point, + scale: Scale, + alpha: f32, + ) -> Vec { + let window_bbox = SpaceElement::bbox(&self.0); + + if self.decoration_state().is_ssd && !window_bbox.is_empty() { + let window_geo = SpaceElement::geometry(&self.0); + + let mut state = self.decoration_state(); + let width = window_geo.size.w; + state.header_bar.redraw(width as u32); + let mut vec = AsRenderElements::::render_elements::>( + &state.header_bar, + renderer, + location, + scale, + alpha, + ); + + location.y += (scale.y * HEADER_BAR_HEIGHT as f64) as i32; + + let window_elements = + AsRenderElements::render_elements(&self.0, renderer, location, scale, alpha); + vec.extend(window_elements); + vec.into_iter().map(C::from).collect() + } else { + AsRenderElements::render_elements(&self.0, renderer, location, scale, alpha) + .into_iter() + .map(C::from) + .collect() + } + } +} diff --git a/vendor/smithay/anvil/src/shell/grabs.rs b/vendor/smithay/anvil/src/shell/grabs.rs new file mode 100644 index 0000000..ca2b17d --- /dev/null +++ b/vendor/smithay/anvil/src/shell/grabs.rs @@ -0,0 +1,855 @@ +use std::cell::RefCell; + +use smithay::{ + desktop::{WindowSurface, space::SpaceElement}, + input::{ + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, + }, + touch::{GrabStartData as TouchGrabStartData, TouchGrab}, + }, + reexports::wayland_protocols::xdg::shell::server::xdg_toplevel, + utils::{IsAlive, Logical, Point, Serial, Size}, + wayland::{compositor::with_states, shell::xdg::SurfaceCachedState}, +}; +#[cfg(feature = "xwayland")] +use smithay::{utils::Rectangle, xwayland::xwm::ResizeEdge as X11ResizeEdge}; + +use super::{SurfaceData, WindowElement}; +use crate::{ + focus::PointerFocusTarget, + state::{AnvilState, Backend}, +}; + +pub struct PointerMoveSurfaceGrab { + pub start_data: PointerGrabStartData>, + pub window: WindowElement, + pub initial_window_location: Point, +} + +impl PointerGrab> for PointerMoveSurfaceGrab { + fn motion( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + let delta = event.location - self.start_data.location; + let new_location = self.initial_window_location.to_f64() + delta; + + data.space + .map_element(self.window.clone(), new_location.to_i32_round(), true); + } + + fn relative_motion( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &ButtonEvent, + ) { + handle.button(data, event); + if handle.current_pressed().is_empty() { + // No more buttons are pressed, release the grab. + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn axis( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + ) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &PointerGrabStartData> { + &self.start_data + } + + fn unset(&mut self, _data: &mut AnvilState) {} +} + +pub struct TouchMoveSurfaceGrab { + pub start_data: TouchGrabStartData>, + pub window: WindowElement, + pub initial_window_location: Point, +} + +impl TouchGrab> for TouchMoveSurfaceGrab { + fn down( + &mut self, + _data: &mut AnvilState, + _handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + _focus: Option<( + as smithay::input::SeatHandler>::TouchFocus, + Point, + )>, + _event: &smithay::input::touch::DownEvent, + ) { + } + + fn up( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + event: &smithay::input::touch::UpEvent, + ) { + if event.slot != self.start_data.slot { + return; + } + + handle.up(data, event); + handle.unset_grab(self, data); + } + + fn motion( + &mut self, + data: &mut AnvilState, + _handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + _focus: Option<( + as smithay::input::SeatHandler>::TouchFocus, + Point, + )>, + event: &smithay::input::touch::MotionEvent, + ) { + if event.slot != self.start_data.slot { + return; + } + + let delta = event.location - self.start_data.location; + let new_location = self.initial_window_location.to_f64() + delta; + data.space + .map_element(self.window.clone(), new_location.to_i32_round(), true); + } + + fn frame( + &mut self, + _data: &mut AnvilState, + _handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + ) { + } + + fn cancel( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + ) { + handle.cancel(data); + handle.unset_grab(self, data); + } + + fn shape( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + event: &smithay::input::touch::ShapeEvent, + ) { + handle.shape(data, event); + } + + fn orientation( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + event: &smithay::input::touch::OrientationEvent, + ) { + handle.orientation(data, event); + } + + fn start_data(&self) -> &smithay::input::touch::GrabStartData> { + &self.start_data + } + + fn unset(&mut self, _data: &mut AnvilState) {} +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct ResizeEdge: u32 { + const NONE = 0; + const TOP = 1; + const BOTTOM = 2; + const LEFT = 4; + const TOP_LEFT = 5; + const BOTTOM_LEFT = 6; + const RIGHT = 8; + const TOP_RIGHT = 9; + const BOTTOM_RIGHT = 10; + } +} + +impl From for ResizeEdge { + #[inline] + fn from(x: xdg_toplevel::ResizeEdge) -> Self { + Self::from_bits(x as u32).unwrap() + } +} + +impl From for xdg_toplevel::ResizeEdge { + #[inline] + fn from(x: ResizeEdge) -> Self { + Self::try_from(x.bits()).unwrap() + } +} + +#[cfg(feature = "xwayland")] +impl From for ResizeEdge { + #[inline] + fn from(edge: X11ResizeEdge) -> Self { + match edge { + X11ResizeEdge::Bottom => ResizeEdge::BOTTOM, + X11ResizeEdge::BottomLeft => ResizeEdge::BOTTOM_LEFT, + X11ResizeEdge::BottomRight => ResizeEdge::BOTTOM_RIGHT, + X11ResizeEdge::Left => ResizeEdge::LEFT, + X11ResizeEdge::Right => ResizeEdge::RIGHT, + X11ResizeEdge::Top => ResizeEdge::TOP, + X11ResizeEdge::TopLeft => ResizeEdge::TOP_LEFT, + X11ResizeEdge::TopRight => ResizeEdge::TOP_RIGHT, + } + } +} + +/// Information about the resize operation. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct ResizeData { + /// The edges the surface is being resized with. + pub edges: ResizeEdge, + /// The initial window location. + pub initial_window_location: Point, + /// The initial window size (geometry width and height). + pub initial_window_size: Size, +} + +/// State of the resize operation. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub enum ResizeState { + /// The surface is not being resized. + #[default] + NotResizing, + /// The surface is currently being resized. + Resizing(ResizeData), + /// The resize has finished, and the surface needs to ack the final configure. + WaitingForFinalAck(ResizeData, Serial), + /// The resize has finished, and the surface needs to commit its final state. + WaitingForCommit(ResizeData), +} + +pub struct PointerResizeSurfaceGrab { + pub start_data: PointerGrabStartData>, + pub window: WindowElement, + pub edges: ResizeEdge, + pub initial_window_location: Point, + pub initial_window_size: Size, + pub last_window_size: Size, +} + +impl PointerGrab> for PointerResizeSurfaceGrab { + fn motion( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + // It is impossible to get `min_size` and `max_size` of dead toplevel, so we return early. + if !self.window.alive() { + handle.unset_grab(self, data, event.serial, event.time, true); + return; + } + + let (mut dx, mut dy) = (event.location - self.start_data.location).into(); + + let mut new_window_width = self.initial_window_size.w; + let mut new_window_height = self.initial_window_size.h; + + let left_right = ResizeEdge::LEFT | ResizeEdge::RIGHT; + let top_bottom = ResizeEdge::TOP | ResizeEdge::BOTTOM; + + if self.edges.intersects(left_right) { + if self.edges.intersects(ResizeEdge::LEFT) { + dx = -dx; + } + + new_window_width = (self.initial_window_size.w as f64 + dx) as i32; + } + + if self.edges.intersects(top_bottom) { + if self.edges.intersects(ResizeEdge::TOP) { + dy = -dy; + } + + new_window_height = (self.initial_window_size.h as f64 + dy) as i32; + } + + let (min_size, max_size) = if let Some(surface) = self.window.wl_surface() { + with_states(&surface, |states| { + let mut guard = states.cached_state.get::(); + let data = guard.current(); + (data.min_size, data.max_size) + }) + } else { + ((0, 0).into(), (0, 0).into()) + }; + + let min_width = min_size.w.max(1); + let min_height = min_size.h.max(1); + let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w }; + let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h }; + + new_window_width = new_window_width.max(min_width).min(max_width); + new_window_height = new_window_height.max(min_height).min(max_height); + + self.last_window_size = (new_window_width, new_window_height).into(); + + match &self.window.0.underlying_surface() { + WindowSurface::Wayland(xdg) => { + xdg.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + xdg.send_pending_configure(); + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(x11) => { + let location = data.space.element_location(&self.window).unwrap(); + x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None) + .unwrap(); + } + } + } + + fn relative_motion( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &ButtonEvent, + ) { + handle.button(data, event); + if handle.current_pressed().is_empty() { + // No more buttons are pressed, release the grab. + handle.unset_grab(self, data, event.serial, event.time, true); + + // If toplevel is dead, we can't resize it, so we return early. + if !self.window.alive() { + return; + } + + match &self.window.0.underlying_surface() { + WindowSurface::Wayland(xdg) => { + xdg.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + xdg.send_pending_configure(); + if self.edges.intersects(ResizeEdge::TOP_LEFT) { + let geometry = self.window.geometry(); + let mut location = data.space.element_location(&self.window).unwrap(); + + if self.edges.intersects(ResizeEdge::LEFT) { + location.x = self.initial_window_location.x + + (self.initial_window_size.w - geometry.size.w); + } + if self.edges.intersects(ResizeEdge::TOP) { + location.y = self.initial_window_location.y + + (self.initial_window_size.h - geometry.size.h); + } + + data.space.map_element(self.window.clone(), location, true); + } + + with_states(&self.window.wl_surface().unwrap(), |states| { + let mut data = states + .data_map + .get::>() + .unwrap() + .borrow_mut(); + if let ResizeState::Resizing(resize_data) = data.resize_state { + data.resize_state = ResizeState::WaitingForFinalAck(resize_data, event.serial); + } else { + panic!("invalid resize state: {:?}", data.resize_state); + } + }); + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(x11) => { + let mut location = data.space.element_location(&self.window).unwrap(); + if self.edges.intersects(ResizeEdge::TOP_LEFT) { + let geometry = self.window.geometry(); + + if self.edges.intersects(ResizeEdge::LEFT) { + location.x = self.initial_window_location.x + + (self.initial_window_size.w - geometry.size.w); + } + if self.edges.intersects(ResizeEdge::TOP) { + location.y = self.initial_window_location.y + + (self.initial_window_size.h - geometry.size.h); + } + + data.space.map_element(self.window.clone(), location, true); + } + x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None) + .unwrap(); + + let Some(surface) = self.window.wl_surface() else { + // X11 Window got unmapped, abort + return; + }; + with_states(&surface, |states| { + let mut data = states + .data_map + .get::>() + .unwrap() + .borrow_mut(); + if let ResizeState::Resizing(resize_data) = data.resize_state { + data.resize_state = ResizeState::WaitingForCommit(resize_data); + } else { + panic!("invalid resize state: {:?}", data.resize_state); + } + }); + } + } + } + } + + fn axis( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + ) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut AnvilState, + handle: &mut PointerInnerHandle<'_, AnvilState>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &PointerGrabStartData> { + &self.start_data + } + + fn unset(&mut self, _data: &mut AnvilState) {} +} + +pub struct TouchResizeSurfaceGrab { + pub start_data: TouchGrabStartData>, + pub window: WindowElement, + pub edges: ResizeEdge, + pub initial_window_location: Point, + pub initial_window_size: Size, + pub last_window_size: Size, +} + +impl TouchGrab> for TouchResizeSurfaceGrab { + fn down( + &mut self, + _data: &mut AnvilState, + _handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + _focus: Option<( + as smithay::input::SeatHandler>::TouchFocus, + Point, + )>, + _event: &smithay::input::touch::DownEvent, + ) { + } + + fn up( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + event: &smithay::input::touch::UpEvent, + ) { + if event.slot != self.start_data.slot { + return; + } + handle.unset_grab(self, data); + + // If toplevel is dead, we can't resize it, so we return early. + if !self.window.alive() { + return; + } + + match self.window.0.underlying_surface() { + WindowSurface::Wayland(xdg) => { + xdg.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + xdg.send_pending_configure(); + if self.edges.intersects(ResizeEdge::TOP_LEFT) { + let geometry = self.window.geometry(); + let mut location = data.space.element_location(&self.window).unwrap(); + + if self.edges.intersects(ResizeEdge::LEFT) { + location.x = + self.initial_window_location.x + (self.initial_window_size.w - geometry.size.w); + } + if self.edges.intersects(ResizeEdge::TOP) { + location.y = + self.initial_window_location.y + (self.initial_window_size.h - geometry.size.h); + } + + data.space.map_element(self.window.clone(), location, true); + } + + with_states(&self.window.wl_surface().unwrap(), |states| { + let mut data = states + .data_map + .get::>() + .unwrap() + .borrow_mut(); + if let ResizeState::Resizing(resize_data) = data.resize_state { + data.resize_state = ResizeState::WaitingForFinalAck(resize_data, event.serial); + } else { + panic!("invalid resize state: {:?}", data.resize_state); + } + }); + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(x11) => { + let mut location = data.space.element_location(&self.window).unwrap(); + if self.edges.intersects(ResizeEdge::TOP_LEFT) { + let geometry = self.window.geometry(); + + if self.edges.intersects(ResizeEdge::LEFT) { + location.x = + self.initial_window_location.x + (self.initial_window_size.w - geometry.size.w); + } + if self.edges.intersects(ResizeEdge::TOP) { + location.y = + self.initial_window_location.y + (self.initial_window_size.h - geometry.size.h); + } + + data.space.map_element(self.window.clone(), location, true); + } + x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None) + .unwrap(); + + let Some(surface) = self.window.wl_surface() else { + // X11 Window got unmapped, abort + return; + }; + with_states(&surface, |states| { + let mut data = states + .data_map + .get::>() + .unwrap() + .borrow_mut(); + if let ResizeState::Resizing(resize_data) = data.resize_state { + data.resize_state = ResizeState::WaitingForCommit(resize_data); + } else { + panic!("invalid resize state: {:?}", data.resize_state); + } + }); + } + } + } + + fn motion( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + _focus: Option<( + as smithay::input::SeatHandler>::TouchFocus, + Point, + )>, + event: &smithay::input::touch::MotionEvent, + ) { + if event.slot != self.start_data.slot { + return; + } + + // It is impossible to get `min_size` and `max_size` of dead toplevel, so we return early. + if !self.window.alive() { + handle.unset_grab(self, data); + return; + } + + let (mut dx, mut dy) = (event.location - self.start_data.location).into(); + + let mut new_window_width = self.initial_window_size.w; + let mut new_window_height = self.initial_window_size.h; + + let left_right = ResizeEdge::LEFT | ResizeEdge::RIGHT; + let top_bottom = ResizeEdge::TOP | ResizeEdge::BOTTOM; + + if self.edges.intersects(left_right) { + if self.edges.intersects(ResizeEdge::LEFT) { + dx = -dx; + } + + new_window_width = (self.initial_window_size.w as f64 + dx) as i32; + } + + if self.edges.intersects(top_bottom) { + if self.edges.intersects(ResizeEdge::TOP) { + dy = -dy; + } + + new_window_height = (self.initial_window_size.h as f64 + dy) as i32; + } + + let (min_size, max_size) = if let Some(surface) = self.window.wl_surface() { + with_states(&surface, |states| { + let mut guard = states.cached_state.get::(); + let data = guard.current(); + (data.min_size, data.max_size) + }) + } else { + ((0, 0).into(), (0, 0).into()) + }; + + let min_width = min_size.w.max(1); + let min_height = min_size.h.max(1); + let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w }; + let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h }; + + new_window_width = new_window_width.max(min_width).min(max_width); + new_window_height = new_window_height.max(min_height).min(max_height); + + self.last_window_size = (new_window_width, new_window_height).into(); + + match self.window.0.underlying_surface() { + WindowSurface::Wayland(xdg) => { + xdg.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + xdg.send_pending_configure(); + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(x11) => { + let location = data.space.element_location(&self.window).unwrap(); + x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None) + .unwrap(); + } + } + } + + fn frame( + &mut self, + _data: &mut AnvilState, + _handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + ) { + } + + fn cancel( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + ) { + handle.cancel(data); + handle.unset_grab(self, data); + } + + fn shape( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + event: &smithay::input::touch::ShapeEvent, + ) { + handle.shape(data, event); + } + + fn orientation( + &mut self, + data: &mut AnvilState, + handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState>, + event: &smithay::input::touch::OrientationEvent, + ) { + handle.orientation(data, event); + } + + fn start_data(&self) -> &smithay::input::touch::GrabStartData> { + &self.start_data + } + + fn unset(&mut self, _data: &mut AnvilState) {} +} diff --git a/vendor/smithay/anvil/src/shell/mod.rs b/vendor/smithay/anvil/src/shell/mod.rs new file mode 100644 index 0000000..2c4f43a --- /dev/null +++ b/vendor/smithay/anvil/src/shell/mod.rs @@ -0,0 +1,475 @@ +use std::cell::RefCell; + +#[cfg(feature = "xwayland")] +use smithay::xwayland::XWaylandClientData; + +#[cfg(feature = "udev")] +use smithay::wayland::drm_syncobj::DrmSyncobjCachedState; + +use smithay::{ + backend::renderer::utils::on_commit_buffer_handler, + desktop::{ + LayerSurface, PopupKind, PopupManager, Space, WindowSurfaceType, layer_map_for_output, + space::SpaceElement, + }, + input::pointer::{CursorImageStatus, CursorImageSurfaceData}, + output::Output, + reexports::{ + calloop::Interest, + wayland_server::{ + Client, Resource, + protocol::{wl_buffer::WlBuffer, wl_output, wl_surface::WlSurface}, + }, + }, + utils::{IsAlive, Logical, Point, Rectangle, Size}, + wayland::{ + buffer::BufferHandler, + compositor::{ + BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes, + TraversalAction, add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, with_states, + with_surface_tree_upward, + }, + dmabuf::get_dmabuf, + shell::{ + wlr_layer::{ + Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler, + WlrLayerShellState, + }, + xdg::XdgToplevelSurfaceData, + }, + }, +}; + +use crate::{ + ClientState, + state::{AnvilState, Backend}, +}; + +mod element; +mod grabs; +pub(crate) mod ssd; +#[cfg(feature = "xwayland")] +mod x11; +mod xdg; + +pub use self::element::*; +pub use self::grabs::*; + +fn fullscreen_output_geometry( + wl_surface: &WlSurface, + wl_output: Option<&wl_output::WlOutput>, + space: &mut Space, +) -> Option> { + // First test if a specific output has been requested + // if the requested output is not found ignore the request + wl_output + .and_then(Output::from_resource) + .or_else(|| { + let w = space + .elements() + .find(|window| window.wl_surface().map(|s| &*s == wl_surface).unwrap_or(false)); + w.and_then(|w| space.outputs_for_element(w).first().cloned()) + }) + .as_ref() + .and_then(|o| space.output_geometry(o)) +} + +#[derive(Default)] +pub struct FullscreenSurface(RefCell>); + +impl FullscreenSurface { + pub fn set(&self, window: WindowElement) { + *self.0.borrow_mut() = Some(window); + } + + pub fn get(&self) -> Option { + let mut window = self.0.borrow_mut(); + if window.as_ref().map(|w| !w.alive()).unwrap_or(false) { + *window = None; + } + window.clone() + } + + pub fn clear(&self) -> Option { + self.0.borrow_mut().take() + } +} + +impl BufferHandler for AnvilState { + fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {} +} + +impl CompositorHandler for AnvilState { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + #[cfg(feature = "xwayland")] + if let Some(state) = client.get_data::() { + return &state.compositor_state; + } + if let Some(state) = client.get_data::() { + return &state.compositor_state; + } + panic!("Unknown client data type") + } + + fn new_surface(&mut self, surface: &WlSurface) { + add_pre_commit_hook::(surface, move |state, _dh, surface| { + #[cfg(feature = "udev")] + let mut acquire_point = None; + let maybe_dmabuf = with_states(surface, |surface_data| { + #[cfg(feature = "udev")] + acquire_point.clone_from( + &surface_data + .cached_state + .get::() + .pending() + .acquire_point, + ); + surface_data + .cached_state + .get::() + .pending() + .buffer + .as_ref() + .and_then(|assignment| match assignment { + BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(), + _ => None, + }) + }); + if let Some(dmabuf) = maybe_dmabuf { + #[cfg(feature = "udev")] + if let Some(acquire_point) = acquire_point { + if let Ok((blocker, source)) = acquire_point.generate_blocker() { + let client = surface.client().unwrap(); + let res = state.handle.insert_source(source, move |_, _, data| { + let dh = data.display_handle.clone(); + data.client_compositor_state(&client).blocker_cleared(data, &dh); + Ok(()) + }); + if res.is_ok() { + add_blocker(surface, blocker); + return; + } + } + } + if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) { + if let Some(client) = surface.client() { + let res = state.handle.insert_source(source, move |_, _, data| { + let dh = data.display_handle.clone(); + data.client_compositor_state(&client).blocker_cleared(data, &dh); + Ok(()) + }); + if res.is_ok() { + add_blocker(surface, blocker); + } + } + } + } + }); + } + + fn commit(&mut self, surface: &WlSurface) { + on_commit_buffer_handler::(surface); + self.backend_data.early_import(surface); + + if !is_sync_subsurface(surface) { + let mut root = surface.clone(); + while let Some(parent) = get_parent(&root) { + root = parent; + } + if let Some(window) = self.window_for_surface(&root) { + window.0.on_commit(); + + if &root == surface { + let buffer_offset = with_states(surface, |states| { + states + .cached_state + .get::() + .current() + .buffer_delta + .take() + }); + + if let Some(buffer_offset) = buffer_offset { + let current_loc = self.space.element_location(&window).unwrap(); + self.space.map_element(window, current_loc + buffer_offset, false); + } + } + } + } + self.popups.commit(surface); + + if matches!(&self.cursor_status, CursorImageStatus::Surface(cursor_surface) if cursor_surface == surface) + { + with_states(surface, |states| { + let cursor_image_attributes = states.data_map.get::(); + + if let Some(mut cursor_image_attributes) = + cursor_image_attributes.map(|attrs| attrs.lock().unwrap()) + { + let buffer_delta = states + .cached_state + .get::() + .current() + .buffer_delta + .take(); + if let Some(buffer_delta) = buffer_delta { + tracing::trace!(hotspot = ?cursor_image_attributes.hotspot, ?buffer_delta, "decrementing cursor hotspot"); + cursor_image_attributes.hotspot -= buffer_delta; + } + } + }); + } + + if matches!(&self.dnd_icon, Some(icon) if &icon.surface == surface) { + let dnd_icon = self.dnd_icon.as_mut().unwrap(); + with_states(&dnd_icon.surface, |states| { + let buffer_delta = states + .cached_state + .get::() + .current() + .buffer_delta + .take() + .unwrap_or_default(); + tracing::trace!(offset = ?dnd_icon.offset, ?buffer_delta, "moving dnd offset"); + dnd_icon.offset += buffer_delta; + }); + } + + ensure_initial_configure(surface, &self.space, &mut self.popups) + } +} + +impl WlrLayerShellHandler for AnvilState { + fn shell_state(&mut self) -> &mut WlrLayerShellState { + &mut self.layer_shell_state + } + + fn new_layer_surface( + &mut self, + surface: WlrLayerSurface, + wl_output: Option, + _layer: Layer, + namespace: String, + ) { + let output = wl_output + .as_ref() + .and_then(Output::from_resource) + .unwrap_or_else(|| self.space.outputs().next().unwrap().clone()); + let mut map = layer_map_for_output(&output); + map.map_layer(&LayerSurface::new(surface, namespace)).unwrap(); + } + + fn layer_destroyed(&mut self, surface: WlrLayerSurface) { + if let Some((mut map, layer)) = self.space.outputs().find_map(|o| { + let map = layer_map_for_output(o); + let layer = map + .layers() + .find(|&layer| layer.layer_surface() == &surface) + .cloned(); + layer.map(|layer| (map, layer)) + }) { + map.unmap_layer(&layer); + } + } +} + +impl AnvilState { + pub fn window_for_surface(&self, surface: &WlSurface) -> Option { + self.space + .elements() + .find(|window| window.wl_surface().map(|s| &*s == surface).unwrap_or(false)) + .cloned() + } +} + +#[derive(Default)] +pub struct SurfaceData { + pub geometry: Option>, + pub resize_state: ResizeState, +} + +fn ensure_initial_configure(surface: &WlSurface, space: &Space, popups: &mut PopupManager) { + with_surface_tree_upward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |_, states, _| { + states + .data_map + .insert_if_missing(|| RefCell::new(SurfaceData::default())); + }, + |_, _, _| true, + ); + + if let Some(window) = space + .elements() + .find(|window| window.wl_surface().map(|s| &*s == surface).unwrap_or(false)) + .cloned() + { + // send the initial configure if relevant + #[cfg_attr(not(feature = "xwayland"), allow(irrefutable_let_patterns))] + if let Some(toplevel) = window.0.toplevel() { + let initial_configure_sent = with_states(surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }); + if !initial_configure_sent { + toplevel.send_configure(); + } + } + + with_states(surface, |states| { + let mut data = states + .data_map + .get::>() + .unwrap() + .borrow_mut(); + + // Finish resizing. + if let ResizeState::WaitingForCommit(_) = data.resize_state { + data.resize_state = ResizeState::NotResizing; + } + }); + + return; + } + + if let Some(popup) = popups.find_popup(surface) { + let popup = match popup { + PopupKind::Xdg(ref popup) => popup, + // Doesn't require configure + PopupKind::InputMethod(ref _input_popup) => { + return; + } + }; + + if !popup.is_initial_configure_sent() { + // NOTE: This should never fail as the initial configure is always + // allowed. + popup.send_configure().expect("initial configure failed"); + } + + return; + }; + + if let Some(output) = space.outputs().find(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) + .is_some() + }) { + let initial_configure_sent = with_states(surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }); + + let mut map = layer_map_for_output(output); + + // arrange the layers before sending the initial configure + // to respect any size the client may have sent + map.arrange(); + // send the initial configure if relevant + if !initial_configure_sent { + let layer = map + .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) + .unwrap(); + + layer.layer_surface().send_configure(); + } + }; +} + +fn place_new_window( + space: &mut Space, + pointer_location: Point, + window: &WindowElement, + activate: bool, +) { + // place the window at a random location on same output as pointer + // or if there is not output in a [0;800]x[0;800] square + use rand::distributions::{Distribution, Uniform}; + + let output = space + .output_under(pointer_location) + .next() + .or_else(|| space.outputs().next()) + .cloned(); + let output_geometry = output + .and_then(|o| { + let geo = space.output_geometry(&o)?; + let map = layer_map_for_output(&o); + let zone = map.non_exclusive_zone(); + Some(Rectangle::new(geo.loc + zone.loc, zone.size)) + }) + .unwrap_or_else(|| Rectangle::from_size((800, 800).into())); + + // set the initial toplevel bounds + #[allow(irrefutable_let_patterns)] + if let Some(toplevel) = window.0.toplevel() { + toplevel.with_pending_state(|state| { + state.bounds = Some(output_geometry.size); + }); + } + + let max_x = output_geometry.loc.x + (((output_geometry.size.w as f32) / 3.0) * 2.0) as i32; + let max_y = output_geometry.loc.y + (((output_geometry.size.h as f32) / 3.0) * 2.0) as i32; + let x_range = Uniform::new(output_geometry.loc.x, max_x); + let y_range = Uniform::new(output_geometry.loc.y, max_y); + let mut rng = rand::thread_rng(); + let x = x_range.sample(&mut rng); + let y = y_range.sample(&mut rng); + + space.map_element(window.clone(), (x, y), activate); +} + +pub fn fixup_positions(space: &mut Space, pointer_location: Point) { + // fixup outputs + let mut offset = Point::::from((0, 0)); + for output in space.outputs().cloned().collect::>().into_iter() { + let size = space + .output_geometry(&output) + .map(|geo| geo.size) + .unwrap_or_else(|| Size::from((0, 0))); + space.map_output(&output, offset); + layer_map_for_output(&output).arrange(); + offset.x += size.w; + } + + // fixup windows + let mut orphaned_windows = Vec::new(); + let outputs = space + .outputs() + .flat_map(|o| { + let geo = space.output_geometry(o)?; + let map = layer_map_for_output(o); + let zone = map.non_exclusive_zone(); + Some(Rectangle::new(geo.loc + zone.loc, zone.size)) + }) + .collect::>(); + for window in space.elements() { + let window_location = match space.element_location(window) { + Some(loc) => loc, + None => continue, + }; + let geo_loc = window.bbox().loc + window_location; + + if !outputs.iter().any(|o_geo| o_geo.contains(geo_loc)) { + orphaned_windows.push(window.clone()); + } + } + for window in orphaned_windows.into_iter() { + place_new_window(space, pointer_location, &window, false); + } +} diff --git a/vendor/smithay/anvil/src/shell/ssd.rs b/vendor/smithay/anvil/src/shell/ssd.rs new file mode 100644 index 0000000..9d55601 --- /dev/null +++ b/vendor/smithay/anvil/src/shell/ssd.rs @@ -0,0 +1,294 @@ +use smithay::{ + backend::renderer::{ + Renderer, + element::{ + AsRenderElements, Kind, + solid::{SolidColorBuffer, SolidColorRenderElement}, + }, + }, + desktop::WindowSurface, + input::Seat, + utils::{Logical, Point, Serial}, + wayland::shell::xdg::XdgShellHandler, +}; + +use std::cell::{RefCell, RefMut}; + +use crate::{AnvilState, state::Backend}; + +use super::WindowElement; + +pub struct WindowState { + pub is_ssd: bool, + pub header_bar: HeaderBar, +} + +#[derive(Debug, Clone)] +pub struct HeaderBar { + pub pointer_loc: Option>, + pub width: u32, + pub close_button_hover: bool, + pub maximize_button_hover: bool, + pub background: SolidColorBuffer, + pub close_button: SolidColorBuffer, + pub maximize_button: SolidColorBuffer, +} + +const BG_COLOR: [f32; 4] = [0.75f32, 0.9f32, 0.78f32, 1f32]; +const MAX_COLOR: [f32; 4] = [1f32, 0.965f32, 0.71f32, 1f32]; +const CLOSE_COLOR: [f32; 4] = [1f32, 0.66f32, 0.612f32, 1f32]; +const MAX_COLOR_HOVER: [f32; 4] = [0.71f32, 0.624f32, 0f32, 1f32]; +const CLOSE_COLOR_HOVER: [f32; 4] = [0.75f32, 0.11f32, 0.016f32, 1f32]; + +pub const HEADER_BAR_HEIGHT: i32 = 32; +const BUTTON_HEIGHT: u32 = HEADER_BAR_HEIGHT as u32; +const BUTTON_WIDTH: u32 = 32; + +impl HeaderBar { + pub fn pointer_enter(&mut self, loc: Point) { + self.pointer_loc = Some(loc); + } + + pub fn pointer_leave(&mut self) { + self.pointer_loc = None; + } + + pub fn clicked( + &mut self, + seat: &Seat>, + state: &mut AnvilState, + window: &WindowElement, + serial: Serial, + ) { + match self.pointer_loc.as_ref() { + Some(loc) if loc.x >= (self.width - BUTTON_WIDTH) as f64 => { + match window.0.underlying_surface() { + WindowSurface::Wayland(w) => w.send_close(), + #[cfg(feature = "xwayland")] + WindowSurface::X11(w) => { + let _ = w.close(); + } + }; + } + Some(loc) if loc.x >= (self.width - (BUTTON_WIDTH * 2)) as f64 => { + match window.0.underlying_surface() { + WindowSurface::Wayland(w) => state.maximize_request(w.clone()), + #[cfg(feature = "xwayland")] + WindowSurface::X11(w) => { + let surface = w.clone(); + state + .handle + .insert_idle(move |data| data.maximize_request_x11(&surface)); + } + }; + } + Some(_) => { + match window.0.underlying_surface() { + WindowSurface::Wayland(w) => { + let seat = seat.clone(); + let toplevel = w.clone(); + state + .handle + .insert_idle(move |data| data.move_request_xdg(&toplevel, &seat, serial)); + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(w) => { + let window = w.clone(); + state + .handle + .insert_idle(move |data| data.move_request_x11(&window)); + } + }; + } + _ => {} + }; + } + + pub fn touch_down( + &mut self, + seat: &Seat>, + state: &mut AnvilState, + window: &WindowElement, + serial: Serial, + ) { + match self.pointer_loc.as_ref() { + Some(loc) if loc.x >= (self.width - BUTTON_WIDTH) as f64 => {} + Some(loc) if loc.x >= (self.width - (BUTTON_WIDTH * 2)) as f64 => {} + Some(_) => { + match window.0.underlying_surface() { + WindowSurface::Wayland(w) => { + let seat = seat.clone(); + let toplevel = w.clone(); + state + .handle + .insert_idle(move |data| data.move_request_xdg(&toplevel, &seat, serial)); + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(w) => { + let window = w.clone(); + state + .handle + .insert_idle(move |data| data.move_request_x11(&window)); + } + }; + } + _ => {} + }; + } + + pub fn touch_up( + &mut self, + _seat: &Seat>, + state: &mut AnvilState, + window: &WindowElement, + ) { + match self.pointer_loc.as_ref() { + Some(loc) if loc.x >= (self.width - BUTTON_WIDTH) as f64 => { + match window.0.underlying_surface() { + WindowSurface::Wayland(w) => w.send_close(), + #[cfg(feature = "xwayland")] + WindowSurface::X11(w) => { + let _ = w.close(); + } + }; + } + Some(loc) if loc.x >= (self.width - (BUTTON_WIDTH * 2)) as f64 => { + match window.0.underlying_surface() { + WindowSurface::Wayland(w) => state.maximize_request(w.clone()), + #[cfg(feature = "xwayland")] + WindowSurface::X11(w) => { + let surface = w.clone(); + state + .handle + .insert_idle(move |data| data.maximize_request_x11(&surface)); + } + }; + } + _ => {} + }; + } + + pub fn redraw(&mut self, width: u32) { + if width == 0 { + self.width = 0; + return; + } + + self.background + .update((width as i32, HEADER_BAR_HEIGHT), BG_COLOR); + + let mut needs_redraw_buttons = false; + if width != self.width { + needs_redraw_buttons = true; + self.width = width; + } + + if self + .pointer_loc + .as_ref() + .map(|l| l.x >= (width - BUTTON_WIDTH) as f64) + .unwrap_or(false) + && (needs_redraw_buttons || !self.close_button_hover) + { + self.close_button + .update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), CLOSE_COLOR_HOVER); + self.close_button_hover = true; + } else if !self + .pointer_loc + .as_ref() + .map(|l| l.x >= (width - BUTTON_WIDTH) as f64) + .unwrap_or(false) + && (needs_redraw_buttons || self.close_button_hover) + { + self.close_button + .update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), CLOSE_COLOR); + self.close_button_hover = false; + } + + if self + .pointer_loc + .as_ref() + .map(|l| l.x >= (width - BUTTON_WIDTH * 2) as f64 && l.x <= (width - BUTTON_WIDTH) as f64) + .unwrap_or(false) + && (needs_redraw_buttons || !self.maximize_button_hover) + { + self.maximize_button + .update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), MAX_COLOR_HOVER); + self.maximize_button_hover = true; + } else if !self + .pointer_loc + .as_ref() + .map(|l| l.x >= (width - BUTTON_WIDTH * 2) as f64 && l.x <= (width - BUTTON_WIDTH) as f64) + .unwrap_or(false) + && (needs_redraw_buttons || self.maximize_button_hover) + { + self.maximize_button + .update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), MAX_COLOR); + self.maximize_button_hover = false; + } + } +} + +impl AsRenderElements for HeaderBar { + type RenderElement = SolidColorRenderElement; + + fn render_elements>( + &self, + _renderer: &mut R, + location: Point, + scale: smithay::utils::Scale, + alpha: f32, + ) -> Vec { + let header_end_offset: Point = Point::from((self.width as i32, 0)); + let button_offset: Point = Point::from((BUTTON_WIDTH as i32, 0)); + + vec![ + SolidColorRenderElement::from_buffer( + &self.close_button, + location + (header_end_offset - button_offset).to_physical_precise_round(scale), + scale, + alpha, + Kind::Unspecified, + ) + .into(), + SolidColorRenderElement::from_buffer( + &self.maximize_button, + location + (header_end_offset - button_offset.upscale(2)).to_physical_precise_round(scale), + scale, + alpha, + Kind::Unspecified, + ) + .into(), + SolidColorRenderElement::from_buffer(&self.background, location, scale, alpha, Kind::Unspecified) + .into(), + ] + } +} + +impl WindowElement { + pub fn decoration_state(&self) -> RefMut<'_, WindowState> { + self.user_data().insert_if_missing(|| { + RefCell::new(WindowState { + is_ssd: false, + header_bar: HeaderBar { + pointer_loc: None, + width: 0, + close_button_hover: false, + maximize_button_hover: false, + background: SolidColorBuffer::default(), + close_button: SolidColorBuffer::default(), + maximize_button: SolidColorBuffer::default(), + }, + }) + }); + + self.user_data() + .get::>() + .unwrap() + .borrow_mut() + } + + pub fn set_ssd(&self, ssd: bool) { + self.decoration_state().is_ssd = ssd; + } +} diff --git a/vendor/smithay/anvil/src/shell/x11.rs b/vendor/smithay/anvil/src/shell/x11.rs new file mode 100644 index 0000000..253ac93 --- /dev/null +++ b/vendor/smithay/anvil/src/shell/x11.rs @@ -0,0 +1,431 @@ +use std::{cell::RefCell, os::unix::io::OwnedFd}; + +use smithay::{ + desktop::{Window, space::SpaceElement}, + input::pointer::Focus, + utils::{Logical, Rectangle, SERIAL_COUNTER}, + wayland::{ + compositor::with_states, + selection::{ + SelectionTarget, + data_device::{ + clear_data_device_selection, current_data_device_selection_userdata, + request_data_device_client_selection, set_data_device_selection, + }, + primary_selection::{ + clear_primary_selection, current_primary_selection_userdata, + request_primary_client_selection, set_primary_selection, + }, + }, + xwayland_shell::{XWaylandShellHandler, XWaylandShellState}, + }, + xwayland::{ + X11Surface, X11Wm, XwmHandler, + xwm::{Reorder, ResizeEdge as X11ResizeEdge, XwmId}, + }, +}; +use tracing::{error, trace}; + +use crate::{AnvilState, focus::KeyboardFocusTarget, state::Backend}; + +use super::{ + FullscreenSurface, PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeData, ResizeState, + SurfaceData, TouchMoveSurfaceGrab, WindowElement, place_new_window, +}; + +#[derive(Debug, Default)] +struct OldGeometry(RefCell>>); +impl OldGeometry { + pub fn save(&self, geo: Rectangle) { + *self.0.borrow_mut() = Some(geo); + } + + pub fn restore(&self) -> Option> { + self.0.borrow_mut().take() + } +} + +impl XWaylandShellHandler for AnvilState { + fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState { + &mut self.xwayland_shell_state + } +} + +impl XwmHandler for AnvilState { + fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm { + self.xwm.as_mut().unwrap() + } + + fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + + fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) { + window.set_mapped(true).unwrap(); + let window = WindowElement(Window::new_x11_window(window)); + place_new_window(&mut self.space, self.pointer.current_location(), &window, true); + let bbox = self.space.element_bbox(&window).unwrap(); + let Some(xsurface) = window.0.x11_surface() else { + unreachable!() + }; + xsurface.configure(Some(bbox)).unwrap(); + window.set_ssd(!xsurface.is_decorated()); + } + + fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) { + let location = window.last_configure().loc; + let window = WindowElement(Window::new_x11_window(window)); + self.space.map_element(window, location, true); + } + + fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) { + let maybe = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window)) + .cloned(); + if let Some(elem) = maybe { + self.space.unmap_elem(&elem) + } + if !window.is_override_redirect() { + window.set_mapped(false).unwrap(); + } + } + + fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + + fn configure_request( + &mut self, + _xwm: XwmId, + window: X11Surface, + _x: Option, + _y: Option, + w: Option, + h: Option, + _reorder: Option, + ) { + // we just set the new size, but don't let windows move themselves around freely + let mut geo = window.last_configure(); + if let Some(w) = w { + geo.size.w = w as i32; + } + if let Some(h) = h { + geo.size.h = h as i32; + } + let _ = window.configure(geo); + } + + fn configure_notify( + &mut self, + _xwm: XwmId, + window: X11Surface, + geometry: Rectangle, + _above: Option, + ) { + let Some(elem) = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window)) + .cloned() + else { + return; + }; + self.space.map_element(elem, geometry.loc, false); + // TODO: We don't properly handle the order of override-redirect windows here, + // they are always mapped top and then never reordered. + } + + fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) { + self.maximize_request_x11(&window); + } + + fn unmaximize_request(&mut self, _xwm: XwmId, window: X11Surface) { + let Some(elem) = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window)) + .cloned() + else { + return; + }; + + window.set_maximized(false).unwrap(); + if let Some(old_geo) = window + .user_data() + .get::() + .and_then(|data| data.restore()) + { + window.configure(old_geo).unwrap(); + self.space.map_element(elem, old_geo.loc, false); + } + } + + fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) { + if let Some(elem) = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window)) + { + let outputs_for_window = self.space.outputs_for_element(elem); + let output = outputs_for_window + .first() + // The window hasn't been mapped yet, use the primary output instead + .or_else(|| self.space.outputs().next()) + // Assumes that at least one output exists + .expect("No outputs found"); + let geometry = self.space.output_geometry(output).unwrap(); + + window.set_fullscreen(true).unwrap(); + elem.set_ssd(false); + window.configure(geometry).unwrap(); + output.user_data().insert_if_missing(FullscreenSurface::default); + output + .user_data() + .get::() + .unwrap() + .set(elem.clone()); + trace!("Fullscreening: {:?}", elem); + } + } + + fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) { + if let Some(elem) = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window)) + { + window.set_fullscreen(false).unwrap(); + elem.set_ssd(!window.is_decorated()); + if let Some(output) = self.space.outputs().find(|o| { + o.user_data() + .get::() + .and_then(|f| f.get()) + .map(|w| &w == elem) + .unwrap_or(false) + }) { + trace!("Unfullscreening: {:?}", elem); + output.user_data().get::().unwrap().clear(); + window.configure(self.space.element_bbox(elem)).unwrap(); + self.backend_data.reset_buffers(output); + } + } + } + + fn resize_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32, edges: X11ResizeEdge) { + // luckily anvil only supports one seat anyway... + let start_data = self.pointer.grab_start_data().unwrap(); + + let Some(element) = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window)) + else { + return; + }; + + let geometry = element.geometry(); + let loc = self.space.element_location(element).unwrap(); + let (initial_window_location, initial_window_size) = (loc, geometry.size); + + with_states(&element.wl_surface().unwrap(), move |states| { + states + .data_map + .get::>() + .unwrap() + .borrow_mut() + .resize_state = ResizeState::Resizing(ResizeData { + edges: edges.into(), + initial_window_location, + initial_window_size, + }); + }); + + let grab = PointerResizeSurfaceGrab { + start_data, + window: element.clone(), + edges: edges.into(), + initial_window_location, + initial_window_size, + last_window_size: initial_window_size, + }; + + let pointer = self.pointer.clone(); + pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear); + } + + fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) { + self.move_request_x11(&window) + } + + fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool { + if let Some(keyboard) = self.seat.get_keyboard() { + // check that an X11 window is focused + if let Some(KeyboardFocusTarget::Window(w)) = keyboard.current_focus() { + if let Some(surface) = w.x11_surface() { + if surface.xwm_id().unwrap() == xwm { + return true; + } + } + } + } + false + } + + fn send_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_type: String, fd: OwnedFd) { + match selection { + SelectionTarget::Clipboard => { + if let Err(err) = request_data_device_client_selection(&self.seat, mime_type, fd) { + error!(?err, "Failed to request current wayland clipboard for Xwayland",); + } + } + SelectionTarget::Primary => { + if let Err(err) = request_primary_client_selection(&self.seat, mime_type, fd) { + error!( + ?err, + "Failed to request current wayland primary selection for Xwayland", + ); + } + } + } + } + + fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec) { + trace!(?selection, ?mime_types, "Got Selection from X11",); + // TODO check, that focused windows is X11 window before doing this + match selection { + SelectionTarget::Clipboard => { + set_data_device_selection(&self.display_handle, &self.seat, mime_types, ()) + } + SelectionTarget::Primary => { + set_primary_selection(&self.display_handle, &self.seat, mime_types, ()) + } + } + } + + fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) { + match selection { + SelectionTarget::Clipboard => { + if current_data_device_selection_userdata(&self.seat).is_some() { + clear_data_device_selection(&self.display_handle, &self.seat) + } + } + SelectionTarget::Primary => { + if current_primary_selection_userdata(&self.seat).is_some() { + clear_primary_selection(&self.display_handle, &self.seat) + } + } + } + } + + fn disconnected(&mut self, _xwm: XwmId) { + self.xwm = None; + } +} + +impl AnvilState { + pub fn maximize_request_x11(&mut self, window: &X11Surface) { + let Some(elem) = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == window)) + .cloned() + else { + return; + }; + + let old_geo = self.space.element_bbox(&elem).unwrap(); + let outputs_for_window = self.space.outputs_for_element(&elem); + let output = outputs_for_window + .first() + // The window hasn't been mapped yet, use the primary output instead + .or_else(|| self.space.outputs().next()) + // Assumes that at least one output exists + .expect("No outputs found"); + let geometry = self.space.output_geometry(output).unwrap(); + + window.set_maximized(true).unwrap(); + window.configure(geometry).unwrap(); + window.user_data().insert_if_missing(OldGeometry::default); + window.user_data().get::().unwrap().save(old_geo); + self.space.map_element(elem, geometry.loc, false); + } + + pub fn move_request_x11(&mut self, window: &X11Surface) { + if let Some(touch) = self.seat.get_touch() { + if let Some(start_data) = touch.grab_start_data() { + let element = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == window)); + + if let Some(element) = element { + let mut initial_window_location = self.space.element_location(element).unwrap(); + + // If surface is maximized then unmaximize it + if window.is_maximized() { + window.set_maximized(false).unwrap(); + let pos = start_data.location; + initial_window_location = (pos.x as i32, pos.y as i32).into(); + if let Some(old_geo) = window + .user_data() + .get::() + .and_then(|data| data.restore()) + { + window + .configure(Rectangle::new(initial_window_location, old_geo.size)) + .unwrap(); + } + } + + let grab = TouchMoveSurfaceGrab { + start_data, + window: element.clone(), + initial_window_location, + }; + + touch.set_grab(self, grab, SERIAL_COUNTER.next_serial()); + return; + } + } + } + + // luckily anvil only supports one seat anyway... + let Some(start_data) = self.pointer.grab_start_data() else { + return; + }; + + let Some(element) = self + .space + .elements() + .find(|e| matches!(e.0.x11_surface(), Some(w) if w == window)) + else { + return; + }; + + let mut initial_window_location = self.space.element_location(element).unwrap(); + + // If surface is maximized then unmaximize it + if window.is_maximized() { + window.set_maximized(false).unwrap(); + let pos = self.pointer.current_location(); + initial_window_location = (pos.x as i32, pos.y as i32).into(); + if let Some(old_geo) = window + .user_data() + .get::() + .and_then(|data| data.restore()) + { + window + .configure(Rectangle::new(initial_window_location, old_geo.size)) + .unwrap(); + } + } + + let grab = PointerMoveSurfaceGrab { + start_data, + window: element.clone(), + initial_window_location, + }; + + let pointer = self.pointer.clone(); + pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear); + } +} diff --git a/vendor/smithay/anvil/src/shell/xdg.rs b/vendor/smithay/anvil/src/shell/xdg.rs new file mode 100644 index 0000000..1521ad7 --- /dev/null +++ b/vendor/smithay/anvil/src/shell/xdg.rs @@ -0,0 +1,641 @@ +use std::cell::RefCell; + +use smithay::{ + desktop::{ + PopupKeyboardGrab, PopupKind, PopupPointerGrab, PopupUngrabStrategy, Space, Window, + WindowSurfaceType, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, + space::SpaceElement, + }, + input::{Seat, pointer::Focus}, + output::Output, + reexports::{ + wayland_protocols::xdg::{decoration as xdg_decoration, shell::server::xdg_toplevel}, + wayland_server::{ + Resource, + protocol::{wl_output, wl_seat, wl_surface::WlSurface}, + }, + }, + utils::{Logical, Point, Serial}, + wayland::{ + compositor::{self, with_states}, + seat::WaylandFocus, + shell::xdg::{ + Configure, PopupSurface, PositionerState, ToplevelCachedState, ToplevelSurface, XdgShellHandler, + XdgShellState, + }, + }, +}; +use tracing::{trace, warn}; + +use crate::{ + focus::KeyboardFocusTarget, + shell::{TouchMoveSurfaceGrab, TouchResizeSurfaceGrab}, + state::{AnvilState, Backend}, +}; + +use super::{ + FullscreenSurface, PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeData, ResizeEdge, ResizeState, + SurfaceData, WindowElement, fullscreen_output_geometry, place_new_window, +}; + +impl XdgShellHandler for AnvilState { + fn xdg_shell_state(&mut self) -> &mut XdgShellState { + &mut self.xdg_shell_state + } + + fn new_toplevel(&mut self, surface: ToplevelSurface) { + // Do not send a configure here, the initial configure + // of a xdg_surface has to be sent during the commit if + // the surface is not already configured + let window = WindowElement(Window::new_wayland_window(surface.clone())); + place_new_window(&mut self.space, self.pointer.current_location(), &window, true); + + compositor::add_post_commit_hook(surface.wl_surface(), |state: &mut Self, _, surface| { + handle_toplevel_commit(&mut state.space, surface); + }); + } + + fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { + // Do not send a configure here, the initial configure + // of a xdg_surface has to be sent during the commit if + // the surface is not already configured + + self.unconstrain_popup(&surface); + + if let Err(err) = self.popups.track_popup(PopupKind::from(surface)) { + warn!("Failed to track popup: {}", err); + } + } + + fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) { + surface.with_pending_state(|state| { + let geometry = positioner.get_geometry(); + state.geometry = geometry; + state.positioner = positioner; + }); + self.unconstrain_popup(&surface); + surface.send_repositioned(token); + } + + fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) { + let seat: Seat> = Seat::from_resource(&seat).unwrap(); + self.move_request_xdg(&surface, &seat, serial) + } + + fn resize_request( + &mut self, + surface: ToplevelSurface, + seat: wl_seat::WlSeat, + serial: Serial, + edges: xdg_toplevel::ResizeEdge, + ) { + let seat: Seat> = Seat::from_resource(&seat).unwrap(); + + if let Some(touch) = seat.get_touch() { + if touch.has_grab(serial) { + let start_data = touch.grab_start_data().unwrap(); + tracing::info!(?start_data); + + // If the client disconnects after requesting a move + // we can just ignore the request + let Some(window) = self.window_for_surface(surface.wl_surface()) else { + tracing::info!("no window"); + return; + }; + + // If the focus was for a different surface, ignore the request. + if start_data.focus.is_none() + || !start_data + .focus + .as_ref() + .unwrap() + .0 + .same_client_as(&surface.wl_surface().id()) + { + tracing::info!("different surface"); + return; + } + let geometry = window.geometry(); + let loc = self.space.element_location(&window).unwrap(); + let (initial_window_location, initial_window_size) = (loc, geometry.size); + + with_states(surface.wl_surface(), move |states| { + states + .data_map + .get::>() + .unwrap() + .borrow_mut() + .resize_state = ResizeState::Resizing(ResizeData { + edges: edges.into(), + initial_window_location, + initial_window_size, + }); + }); + + let grab = TouchResizeSurfaceGrab { + start_data, + window, + edges: edges.into(), + initial_window_location, + initial_window_size, + last_window_size: initial_window_size, + }; + + touch.set_grab(self, grab, serial); + return; + } + } + + let pointer = seat.get_pointer().unwrap(); + + // Check that this surface has a click grab. + if !pointer.has_grab(serial) { + return; + } + + let start_data = pointer.grab_start_data().unwrap(); + + let window = self.window_for_surface(surface.wl_surface()).unwrap(); + + // If the focus was for a different surface, ignore the request. + if start_data.focus.is_none() + || !start_data + .focus + .as_ref() + .unwrap() + .0 + .same_client_as(&surface.wl_surface().id()) + { + return; + } + + let geometry = window.geometry(); + let loc = self.space.element_location(&window).unwrap(); + let (initial_window_location, initial_window_size) = (loc, geometry.size); + + with_states(surface.wl_surface(), move |states| { + states + .data_map + .get::>() + .unwrap() + .borrow_mut() + .resize_state = ResizeState::Resizing(ResizeData { + edges: edges.into(), + initial_window_location, + initial_window_size, + }); + }); + + let grab = PointerResizeSurfaceGrab { + start_data, + window, + edges: edges.into(), + initial_window_location, + initial_window_size, + last_window_size: initial_window_size, + }; + + pointer.set_grab(self, grab, serial, Focus::Clear); + } + + fn ack_configure(&mut self, surface: WlSurface, configure: Configure) { + if let Configure::Toplevel(configure) = configure { + if let Some(serial) = with_states(&surface, |states| { + if let Some(data) = states.data_map.get::>() { + if let ResizeState::WaitingForFinalAck(_, serial) = data.borrow().resize_state { + return Some(serial); + } + } + + None + }) { + // When the resize grab is released the surface + // resize state will be set to WaitingForFinalAck + // and the client will receive a configure request + // without the resize state to inform the client + // resizing has finished. Here we will wait for + // the client to acknowledge the end of the + // resizing. To check if the surface was resizing + // before sending the configure we need to use + // the current state as the received acknowledge + // will no longer have the resize state set + let is_resizing = with_states(&surface, |states| { + states + .cached_state + .get::() + .current() + .last_acked + .as_ref() + .is_some_and(|c| c.state.states.contains(xdg_toplevel::State::Resizing)) + }); + + if configure.serial >= serial && is_resizing { + with_states(&surface, |states| { + let mut data = states + .data_map + .get::>() + .unwrap() + .borrow_mut(); + if let ResizeState::WaitingForFinalAck(resize_data, _) = data.resize_state { + data.resize_state = ResizeState::WaitingForCommit(resize_data); + } else { + unreachable!() + } + }); + } + } + + let window = self + .space + .elements() + .find(|element| element.wl_surface().as_deref() == Some(&surface)); + if let Some(window) = window { + use xdg_decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode; + let is_ssd = configure + .state + .decoration_mode + .map(|mode| mode == Mode::ServerSide) + .unwrap_or(false); + window.set_ssd(is_ssd); + } + } + } + + fn fullscreen_request(&mut self, surface: ToplevelSurface, mut wl_output: Option) { + // NOTE: This is only one part of the solution. We can set the + // location and configure size here, but the surface should be rendered fullscreen + // independently from its buffer size + let wl_surface = surface.wl_surface(); + + let output_geometry = fullscreen_output_geometry(wl_surface, wl_output.as_ref(), &mut self.space); + + if let Some(geometry) = output_geometry { + let output = wl_output + .as_ref() + .and_then(Output::from_resource) + .unwrap_or_else(|| self.space.outputs().next().unwrap().clone()); + let client = match self.display_handle.get_client(wl_surface.id()) { + Ok(client) => client, + Err(_) => return, + }; + for output in output.client_outputs(&client) { + wl_output = Some(output); + } + let window = self + .space + .elements() + .find(|window| window.wl_surface().map(|s| &*s == wl_surface).unwrap_or(false)) + .unwrap(); + + surface.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Fullscreen); + state.size = Some(geometry.size); + state.fullscreen_output = wl_output; + }); + output.user_data().insert_if_missing(FullscreenSurface::default); + output + .user_data() + .get::() + .unwrap() + .set(window.clone()); + trace!("Fullscreening: {:?}", window); + } + + // The protocol demands us to always reply with a configure, + // regardless of we fulfilled the request or not + if surface.is_initial_configure_sent() { + surface.send_configure(); + } else { + // Will be sent during initial configure + } + } + + fn unfullscreen_request(&mut self, surface: ToplevelSurface) { + let ret = surface.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Fullscreen); + state.size = None; + state.fullscreen_output.take() + }); + if let Some(output) = ret { + let output = Output::from_resource(&output).unwrap(); + if let Some(fullscreen) = output.user_data().get::() { + trace!("Unfullscreening: {:?}", fullscreen.get()); + fullscreen.clear(); + self.backend_data.reset_buffers(&output); + } + } + + // The protocol demands us to always reply with a configure, + // regardless of we fulfilled the request or not + if surface.is_initial_configure_sent() { + surface.send_configure(); + } else { + // Will be sent during initial configure + } + } + + fn maximize_request(&mut self, surface: ToplevelSurface) { + // NOTE: This should use layer-shell when it is implemented to + // get the correct maximum size + let window = self.window_for_surface(surface.wl_surface()).unwrap(); + let outputs_for_window = self.space.outputs_for_element(&window); + let output = outputs_for_window + .first() + // The window hasn't been mapped yet, use the primary output instead + .or_else(|| self.space.outputs().next()) + // Assumes that at least one output exists + .expect("No outputs found"); + let geometry = self.space.output_geometry(output).unwrap(); + + surface.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Maximized); + state.size = Some(geometry.size); + }); + self.space.map_element(window, geometry.loc, true); + + // The protocol demands us to always reply with a configure, + // regardless of we fulfilled the request or not + if surface.is_initial_configure_sent() { + surface.send_configure(); + } else { + // Will be sent during initial configure + } + } + + fn unmaximize_request(&mut self, surface: ToplevelSurface) { + surface.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Maximized); + state.size = None; + }); + + // The protocol demands us to always reply with a configure, + // regardless of we fulfilled the request or not + if surface.is_initial_configure_sent() { + surface.send_configure(); + } else { + // Will be sent during initial configure + } + } + + fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) { + let seat: Seat> = Seat::from_resource(&seat).unwrap(); + let kind = PopupKind::Xdg(surface); + if let Some(root) = find_popup_root_surface(&kind).ok().and_then(|root| { + self.space + .elements() + .find(|w| w.wl_surface().map(|s| *s == root).unwrap_or(false)) + .cloned() + .map(KeyboardFocusTarget::from) + .or_else(|| { + self.space + .outputs() + .find_map(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL).cloned() + }) + .map(KeyboardFocusTarget::LayerSurface) + }) + }) { + let ret = self.popups.grab_popup(root, kind, &seat, serial); + + if let Ok(mut grab) = ret { + if let Some(keyboard) = seat.get_keyboard() { + if keyboard.is_grabbed() + && !(keyboard.has_grab(serial) + || keyboard.has_grab(grab.previous_serial().unwrap_or(serial))) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + keyboard.set_focus(self, grab.current_grab(), serial); + keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); + } + if let Some(pointer) = seat.get_pointer() { + if pointer.is_grabbed() + && !(pointer.has_grab(serial) + || pointer.has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial()))) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep); + } + } + } + } +} + +impl AnvilState { + pub fn move_request_xdg(&mut self, surface: &ToplevelSurface, seat: &Seat, serial: Serial) { + if let Some(touch) = seat.get_touch() { + if touch.has_grab(serial) { + let start_data = touch.grab_start_data().unwrap(); + + // If the client disconnects after requesting a move + // we can just ignore the request + let Some(window) = self.window_for_surface(surface.wl_surface()) else { + return; + }; + + // If the focus was for a different surface, ignore the request. + if start_data.focus.is_none() + || !start_data + .focus + .as_ref() + .unwrap() + .0 + .same_client_as(&surface.wl_surface().id()) + { + return; + } + + let mut initial_window_location = self.space.element_location(&window).unwrap(); + + // If surface is maximized then unmaximize it + let changed = surface.with_pending_state(|state| { + if state.states.unset(xdg_toplevel::State::Maximized) { + state.size = None; + true + } else { + false + } + }); + if changed { + surface.send_configure(); + + // NOTE: In real compositor mouse location should be mapped to a new window size + // For example, you could: + // 1) transform mouse pointer position from compositor space to window space (location relative) + // 2) divide the x coordinate by width of the window to get the percentage + // - 0.0 would be on the far left of the window + // - 0.5 would be in middle of the window + // - 1.0 would be on the far right of the window + // 3) multiply the percentage by new window width + // 4) by doing that, drag will look a lot more natural + // + // but for anvil needs setting location to pointer location is fine + initial_window_location = start_data.location.to_i32_round(); + } + + let grab = TouchMoveSurfaceGrab { + start_data, + window, + initial_window_location, + }; + + touch.set_grab(self, grab, serial); + return; + } + } + + let pointer = seat.get_pointer().unwrap(); + + // Check that this surface has a click grab. + if !pointer.has_grab(serial) { + return; + } + + let start_data = pointer.grab_start_data().unwrap(); + + // If the client disconnects after requesting a move + // we can just ignore the request + let Some(window) = self.window_for_surface(surface.wl_surface()) else { + return; + }; + + // If the focus was for a different surface, ignore the request. + if start_data.focus.is_none() + || !start_data + .focus + .as_ref() + .unwrap() + .0 + .same_client_as(&surface.wl_surface().id()) + { + return; + } + + let mut initial_window_location = self.space.element_location(&window).unwrap(); + + // If surface is maximized then unmaximize it + let changed = surface.with_pending_state(|state| { + if state.states.unset(xdg_toplevel::State::Maximized) { + state.size = None; + true + } else { + false + } + }); + if changed { + surface.send_configure(); + + // NOTE: In real compositor mouse location should be mapped to a new window size + // For example, you could: + // 1) transform mouse pointer position from compositor space to window space (location relative) + // 2) divide the x coordinate by width of the window to get the percentage + // - 0.0 would be on the far left of the window + // - 0.5 would be in middle of the window + // - 1.0 would be on the far right of the window + // 3) multiply the percentage by new window width + // 4) by doing that, drag will look a lot more natural + // + // but for anvil needs setting location to pointer location is fine + let pos = pointer.current_location(); + initial_window_location = (pos.x as i32, pos.y as i32).into(); + } + + let grab = PointerMoveSurfaceGrab { + start_data, + window, + initial_window_location, + }; + + pointer.set_grab(self, grab, serial, Focus::Clear); + } + + fn unconstrain_popup(&self, popup: &PopupSurface) { + let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else { + return; + }; + let Some(window) = self.window_for_surface(&root) else { + return; + }; + + let mut outputs_for_window = self.space.outputs_for_element(&window); + if outputs_for_window.is_empty() { + return; + } + + // Get a union of all outputs' geometries. + let mut outputs_geo = self + .space + .output_geometry(&outputs_for_window.pop().unwrap()) + .unwrap(); + for output in outputs_for_window { + outputs_geo = outputs_geo.merge(self.space.output_geometry(&output).unwrap()); + } + + let window_geo = self.space.element_geometry(&window).unwrap(); + + // The target geometry for the positioner should be relative to its parent's geometry, so + // we will compute that here. + let mut target = outputs_geo; + target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())); + target.loc -= window_geo.loc; + + popup.with_pending_state(|state| { + state.geometry = state.positioner.get_unconstrained_geometry(target); + }); + } +} + +/// Should be called on `WlSurface::commit` of xdg toplevel +fn handle_toplevel_commit(space: &mut Space, surface: &WlSurface) -> Option<()> { + let window = space + .elements() + .find(|w| w.wl_surface().as_deref() == Some(surface)) + .cloned()?; + + let mut window_loc = space.element_location(&window)?; + let geometry = window.geometry(); + + let new_loc: Point, Logical> = with_states(window.wl_surface().as_deref()?, |states| { + let data = states.data_map.get::>()?.borrow_mut(); + + if let ResizeState::Resizing(resize_data) = data.resize_state { + let edges = resize_data.edges; + let loc = resize_data.initial_window_location; + let size = resize_data.initial_window_size; + + // If the window is being resized by top or left, its location must be adjusted + // accordingly. + edges.intersects(ResizeEdge::TOP_LEFT).then(|| { + let new_x = edges + .intersects(ResizeEdge::LEFT) + .then_some(loc.x + (size.w - geometry.size.w)); + + let new_y = edges + .intersects(ResizeEdge::TOP) + .then_some(loc.y + (size.h - geometry.size.h)); + + (new_x, new_y).into() + }) + } else { + None + } + })?; + + if let Some(new_x) = new_loc.x { + window_loc.x = new_x; + } + if let Some(new_y) = new_loc.y { + window_loc.y = new_y; + } + + if new_loc.x.is_some() || new_loc.y.is_some() { + // If TOP or LEFT side of the window got resized, we have to move it + space.map_element(window, window_loc, false); + } + + Some(()) +} diff --git a/vendor/smithay/anvil/src/state.rs b/vendor/smithay/anvil/src/state.rs new file mode 100644 index 0000000..734a2c5 --- /dev/null +++ b/vendor/smithay/anvil/src/state.rs @@ -0,0 +1,1215 @@ +#[cfg(feature = "xwayland")] +use std::os::unix::io::OwnedFd; +use std::{ + collections::HashMap, + sync::{Arc, atomic::AtomicBool}, + time::Duration, +}; + +use tracing::{info, warn}; + +use smithay::{ + backend::{ + input::TabletToolDescriptor, + renderer::element::{ + RenderElementStates, default_primary_scanout_output_compare, utils::select_dmabuf_feedback, + }, + }, + delegate_dispatch2, + desktop::{ + PopupKind, PopupManager, Space, + space::SpaceElement, + utils::{ + OutputPresentationFeedback, surface_presentation_feedback_flags_from_states, + surface_primary_scanout_output, update_surface_primary_scanout_output, + with_surfaces_surface_tree, + }, + }, + input::{ + Seat, SeatHandler, SeatState, + dnd::{DnDGrab, DndGrabHandler, DndTarget, GrabType, Source}, + keyboard::{Keysym, LedState, XkbConfig}, + pointer::{CursorImageStatus, Focus, PointerHandle}, + tablet::TabletSeatHandler, + }, + output::Output, + reexports::{ + calloop::{Interest, LoopHandle, Mode, PostAction, generic::Generic}, + wayland_protocols::xdg::decoration::{ + self as xdg_decoration, zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode, + }, + wayland_server::{ + Client, Display, DisplayHandle, Resource, + backend::{ClientData, ClientId, DisconnectReason}, + protocol::wl_surface::WlSurface, + }, + }, + utils::{Clock, Logical, Monotonic, Point, Rectangle, Serial, Time}, + wayland::{ + commit_timing::{CommitTimerBarrierStateUserData, CommitTimingManagerState}, + compositor::{CompositorClientState, CompositorHandler, CompositorState, get_parent, with_states}, + dmabuf::DmabufFeedback, + fifo::{FifoBarrierCachedState, FifoManagerState}, + fixes::FixesState, + fractional_scale::{FractionalScaleHandler, FractionalScaleManagerState, with_fractional_scale}, + image_capture_source::{ + ImageCaptureSource, ImageCaptureSourceHandler, ImageCaptureSourceState, + OutputCaptureSourceHandler, OutputCaptureSourceState, + }, + image_copy_capture::{ + BufferConstraints, Frame, ImageCopyCaptureHandler, ImageCopyCaptureState, Session, SessionRef, + }, + input_method::{InputMethodHandler, InputMethodManagerState, PopupSurface}, + keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, + }, + output::{OutputHandler, OutputManagerState}, + pointer_constraints::{ + ConstraintRemove, PointerConstraint, PointerConstraintsHandler, PointerConstraintsState, + with_pointer_constraint, + }, + pointer_gestures::PointerGesturesState, + presentation::PresentationState, + relative_pointer::RelativePointerManagerState, + seat::WaylandFocus, + security_context::{ + SecurityContext, SecurityContextHandler, SecurityContextListenerSource, SecurityContextState, + }, + selection::{ + SelectionHandler, + data_device::{DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus}, + primary_selection::{PrimarySelectionHandler, PrimarySelectionState, set_primary_focus}, + wlr_data_control::{DataControlHandler, DataControlState}, + }, + shell::{ + wlr_layer::WlrLayerShellState, + xdg::{ + ToplevelSurface, XdgShellState, + decoration::{XdgDecorationHandler, XdgDecorationState}, + }, + }, + shm::{ShmHandler, ShmState}, + single_pixel_buffer::SinglePixelBufferState, + socket::ListeningSocketSource, + tablet_manager::TabletManagerState, + text_input::{TextInputActivation, TextInputManagerState}, + viewporter::ViewporterState, + virtual_keyboard::VirtualKeyboardManagerState, + xdg_activation::{ + XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, + }, + xdg_foreign::{XdgForeignHandler, XdgForeignState}, + }, +}; + +#[cfg(feature = "xwayland")] +use crate::cursor::Cursor; +use crate::{ + focus::{KeyboardFocusTarget, PointerFocusTarget}, + shell::WindowElement, +}; +#[cfg(feature = "xwayland")] +use smithay::{ + utils::Size, + wayland::selection::{SelectionSource, SelectionTarget}, + wayland::xwayland_keyboard_grab::{XWaylandKeyboardGrabHandler, XWaylandKeyboardGrabState}, + wayland::xwayland_shell, + xwayland::{X11Wm, XWayland, XWaylandEvent}, +}; + +#[derive(Debug, Default)] +pub struct ClientState { + pub compositor_state: CompositorClientState, + pub security_context: Option, +} +impl ClientData for ClientState { + /// Notification that a client was initialized + fn initialized(&self, _client_id: ClientId) {} + /// Notification that a client is disconnected + fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} +} + +#[derive(Debug)] +pub struct AnvilState { + pub backend_data: BackendData, + pub socket_name: Option, + pub display_handle: DisplayHandle, + pub running: Arc, + pub handle: LoopHandle<'static, AnvilState>, + + // desktop + pub space: Space, + pub popups: PopupManager, + + // smithay state + pub compositor_state: CompositorState, + pub data_device_state: DataDeviceState, + pub layer_shell_state: WlrLayerShellState, + pub output_manager_state: OutputManagerState, + pub primary_selection_state: PrimarySelectionState, + pub data_control_state: DataControlState, + pub seat_state: SeatState>, + pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, + pub shm_state: ShmState, + pub viewporter_state: ViewporterState, + pub xdg_activation_state: XdgActivationState, + pub xdg_decoration_state: XdgDecorationState, + pub xdg_shell_state: XdgShellState, + pub presentation_state: PresentationState, + pub fractional_scale_manager_state: FractionalScaleManagerState, + pub xdg_foreign_state: XdgForeignState, + #[cfg(feature = "xwayland")] + pub xwayland_shell_state: xwayland_shell::XWaylandShellState, + pub single_pixel_buffer_state: SinglePixelBufferState, + pub fifo_manager_state: FifoManagerState, + pub commit_timing_manager_state: CommitTimingManagerState, + pub image_capture_source_state: ImageCaptureSourceState, + pub output_capture_source_state: OutputCaptureSourceState, + pub image_copy_capture_state: ImageCopyCaptureState, + + pub dnd_icon: Option, + + // input-related fields + pub suppressed_keys: Vec, + pub cursor_status: CursorImageStatus, + pub seat_name: String, + pub seat: Seat>, + pub clock: Clock, + pub pointer: PointerHandle>, + pub cursor_position_hint: Option<(WlSurface, Point)>, + + #[cfg(feature = "xwayland")] + pub xwm: Option, + #[cfg(feature = "xwayland")] + pub xdisplay: Option, + + #[cfg(feature = "debug")] + pub renderdoc: Option>, + + pub show_window_preview: bool, +} + +#[derive(Debug)] +pub struct DndIcon { + pub surface: WlSurface, + pub offset: Point, +} + +impl DataDeviceHandler for AnvilState { + fn data_device_state(&mut self) -> &mut DataDeviceState { + &mut self.data_device_state + } +} + +impl WaylandDndGrabHandler for AnvilState { + fn dnd_requested( + &mut self, + source: S, + icon: Option, + seat: Seat, + serial: Serial, + type_: GrabType, + ) { + self.dnd_icon = icon.map(|surface| DndIcon { + surface, + offset: (0, 0).into(), + }); + + match type_ { + GrabType::Pointer => { + let pointer = seat.get_pointer().unwrap(); + let start_data = pointer.grab_start_data().unwrap(); + pointer.set_grab( + self, + DnDGrab::new_pointer(&self.display_handle, start_data, source, seat), + serial, + Focus::Keep, + ); + } + GrabType::Touch => { + let touch = seat.get_touch().unwrap(); + let start_data = touch.grab_start_data().unwrap(); + touch.set_grab( + self, + DnDGrab::new_touch(&self.display_handle, start_data, source, seat), + serial, + ); + } + } + } +} + +impl DndGrabHandler for AnvilState { + fn dropped( + &mut self, + _target: Option>, + _validated: bool, + _seat: Seat, + _location: Point, + ) { + self.dnd_icon = None; + } +} + +impl OutputHandler for AnvilState {} + +impl SelectionHandler for AnvilState { + type SelectionUserData = (); + + #[cfg(feature = "xwayland")] + fn new_selection(&mut self, ty: SelectionTarget, source: Option, _seat: Seat) { + if let Some(xwm) = self.xwm.as_mut() { + if let Err(err) = xwm.new_selection(ty, source.map(|source| source.mime_types())) { + warn!(?err, ?ty, "Failed to set Xwayland selection"); + } + } + } + + #[cfg(feature = "xwayland")] + fn send_selection( + &mut self, + ty: SelectionTarget, + mime_type: String, + fd: OwnedFd, + _seat: Seat, + _user_data: &(), + ) { + if let Some(xwm) = self.xwm.as_mut() { + if let Err(err) = xwm.send_selection(ty, mime_type, fd) { + warn!(?err, "Failed to send primary (X11 -> Wayland)"); + } + } + } +} + +impl PrimarySelectionHandler for AnvilState { + fn primary_selection_state(&mut self) -> &mut PrimarySelectionState { + &mut self.primary_selection_state + } +} + +impl DataControlHandler for AnvilState { + fn data_control_state(&mut self) -> &mut DataControlState { + &mut self.data_control_state + } +} + +impl ShmHandler for AnvilState { + fn shm_state(&self) -> &ShmState { + &self.shm_state + } +} + +impl SeatHandler for AnvilState { + type KeyboardFocus = KeyboardFocusTarget; + type PointerFocus = PointerFocusTarget; + type TouchFocus = PointerFocusTarget; + + fn seat_state(&mut self) -> &mut SeatState> { + &mut self.seat_state + } + + fn focus_changed(&mut self, seat: &Seat, target: Option<&KeyboardFocusTarget>) { + let dh = &self.display_handle; + + let wl_surface = target.and_then(WaylandFocus::wl_surface); + + let focus = wl_surface.and_then(|s| dh.get_client(s.id()).ok()); + set_data_device_focus(dh, seat, focus.clone()); + set_primary_focus(dh, seat, focus); + } + fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { + self.cursor_status = image; + } + + fn led_state_changed(&mut self, _seat: &Seat, led_state: LedState) { + self.backend_data.update_led_state(led_state) + } +} + +impl TabletSeatHandler for AnvilState { + type ToolFocus = PointerFocusTarget; + + fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, image: CursorImageStatus) { + // TODO: tablet tools should have their own cursors + self.cursor_status = image; + } +} + +impl InputMethodHandler for AnvilState { + fn new_popup(&mut self, surface: PopupSurface) { + if let Err(err) = self.popups.track_popup(PopupKind::from(surface)) { + warn!("Failed to track popup: {}", err); + } + } + + fn popup_repositioned(&mut self, _: PopupSurface) {} + + fn dismiss_popup(&mut self, surface: PopupSurface) { + if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) { + let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface)); + } + } + + fn parent_geometry(&self, parent: &WlSurface) -> Rectangle { + self.space + .elements() + .find_map(|window| (window.wl_surface().as_deref() == Some(parent)).then(|| window.geometry())) + .unwrap_or_default() + } +} + +impl KeyboardShortcutsInhibitHandler for AnvilState { + fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState { + &mut self.keyboard_shortcuts_inhibit_state + } + + fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) { + // Just grant the wish for everyone + inhibitor.activate(); + } +} + +impl PointerConstraintsHandler for AnvilState { + fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle) { + // XXX region + let Some(current_focus) = pointer.current_focus() else { + return; + }; + if current_focus.wl_surface().as_deref() == Some(surface) { + with_pointer_constraint(surface, pointer, |constraint| { + constraint.unwrap().activate(); + }); + } + } + + fn remove_constraint( + &mut self, + _surface: &WlSurface, + pointer: &PointerHandle, + constraint_remove: ConstraintRemove, + ) { + // Clear cursor_position_hint to prevent a oneshot PointerLocked constraint + // from causing this function to be called again during PointerLeave and + // unexpectedly changing the cursor position. + let Some((hint_surface, hint_location)) = self.cursor_position_hint.take() else { + return; + }; + + match constraint_remove { + ConstraintRemove::Destroyed(pointer_constraint) => match pointer_constraint { + PointerConstraint::Confined(_confined_pointer) => {} + PointerConstraint::Locked(locked_pointer) => { + let origin = self + .space + .elements() + .find_map(|window| { + (window.wl_surface().as_deref() == Some(&hint_surface)).then(|| window.geometry()) + }) + .unwrap_or_default() + .loc + .to_f64(); + + let surface_location = origin + hint_location; + if let Some(region) = locked_pointer.region() + && region.contains(hint_location.to_i32_floor()) + { + pointer.set_location(surface_location); + } else { + pointer.set_location(surface_location); + } + } + }, + ConstraintRemove::PointerLeave(_region) => {} + } + } + + fn cursor_position_hint( + &mut self, + surface: &WlSurface, + pointer: &PointerHandle, + location: Point, + ) { + if with_pointer_constraint(surface, pointer, |constraint| { + constraint.is_some_and(|c| c.is_active()) + }) { + self.cursor_position_hint = Some((surface.clone(), location)); + } + } +} + +impl TextInputActivation for AnvilState {} + +impl XdgActivationHandler for AnvilState { + fn activation_state(&mut self) -> &mut XdgActivationState { + &mut self.xdg_activation_state + } + + fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool { + if let Some((serial, seat)) = data.serial { + let keyboard = self.seat.get_keyboard().unwrap(); + Seat::from_resource(&seat) == Some(self.seat.clone()) + && keyboard + .last_enter() + .map(|last_enter| serial.is_no_older_than(&last_enter)) + .unwrap_or(false) + } else { + false + } + } + + fn request_activation( + &mut self, + _token: XdgActivationToken, + token_data: XdgActivationTokenData, + surface: WlSurface, + ) { + if token_data.timestamp.elapsed().as_secs() < 10 { + // Just grant the wish + let w = self + .space + .elements() + .find(|window| window.wl_surface().map(|s| *s == surface).unwrap_or(false)) + .cloned(); + if let Some(window) = w { + self.space.raise_element(&window, true); + } + } + } +} + +impl XdgDecorationHandler for AnvilState { + fn new_decoration(&mut self, toplevel: ToplevelSurface) { + use xdg_decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode; + // Set the default to client side + toplevel.with_pending_state(|state| { + state.decoration_mode = Some(Mode::ClientSide); + }); + } + fn request_mode(&mut self, toplevel: ToplevelSurface, mode: DecorationMode) { + use xdg_decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode; + + toplevel.with_pending_state(|state| { + state.decoration_mode = Some(match mode { + DecorationMode::ServerSide => Mode::ServerSide, + _ => Mode::ClientSide, + }); + }); + + if toplevel.is_initial_configure_sent() { + toplevel.send_pending_configure(); + } + } + fn unset_mode(&mut self, toplevel: ToplevelSurface) { + use xdg_decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode; + toplevel.with_pending_state(|state| { + state.decoration_mode = Some(Mode::ClientSide); + }); + + if toplevel.is_initial_configure_sent() { + toplevel.send_pending_configure(); + } + } +} + +impl FractionalScaleHandler for AnvilState { + fn new_fractional_scale( + &mut self, + surface: smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, + ) { + // Here we can set the initial fractional scale + // + // First we look if the surface already has a primary scan-out output, if not + // we test if the surface is a subsurface and try to use the primary scan-out output + // of the root surface. If the root also has no primary scan-out output we just try + // to use the first output of the toplevel. + // If the surface is the root we also try to use the first output of the toplevel. + // + // If all the above tests do not lead to a output we just use the first output + // of the space (which in case of anvil will also be the output a toplevel will + // initially be placed on) + #[allow(clippy::redundant_clone)] + let mut root = surface.clone(); + while let Some(parent) = get_parent(&root) { + root = parent; + } + + with_states(&surface, |states| { + let primary_scanout_output = surface_primary_scanout_output(&surface, states) + .or_else(|| { + if root != surface { + with_states(&root, |states| { + surface_primary_scanout_output(&root, states).or_else(|| { + self.window_for_surface(&root).and_then(|window| { + self.space.outputs_for_element(&window).first().cloned() + }) + }) + }) + } else { + self.window_for_surface(&root) + .and_then(|window| self.space.outputs_for_element(&window).first().cloned()) + } + }) + .or_else(|| self.space.outputs().next().cloned()); + if let Some(output) = primary_scanout_output { + with_fractional_scale(states, |fractional_scale| { + fractional_scale.set_preferred_scale(output.current_scale().fractional_scale()); + }); + } + }); + } +} + +impl SecurityContextHandler for AnvilState { + fn context_created(&mut self, source: SecurityContextListenerSource, security_context: SecurityContext) { + self.handle + .insert_source(source, move |client_stream, _, data| { + let client_state = ClientState { + security_context: Some(security_context.clone()), + ..ClientState::default() + }; + if let Err(err) = data + .display_handle + .insert_client(client_stream, Arc::new(client_state)) + { + warn!("Error adding wayland client: {}", err); + }; + }) + .expect("Failed to init wayland socket source"); + } +} + +#[cfg(feature = "xwayland")] +impl XWaylandKeyboardGrabHandler for AnvilState { + fn keyboard_focus_for_xsurface(&self, surface: &WlSurface) -> Option { + let elem = self + .space + .elements() + .find(|elem| elem.wl_surface().as_deref() == Some(surface))?; + Some(KeyboardFocusTarget::Window(elem.0.clone())) + } +} + +impl XdgForeignHandler for AnvilState { + fn xdg_foreign_state(&mut self) -> &mut XdgForeignState { + &mut self.xdg_foreign_state + } +} + +impl ImageCaptureSourceHandler for AnvilState { + fn source_destroyed(&mut self, _source: ImageCaptureSource) { + // Anvil doesn't track sources + } +} + +impl OutputCaptureSourceHandler for AnvilState { + fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState { + &mut self.output_capture_source_state + } + + fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) { + source.user_data().insert_if_missing(|| output.downgrade()); + } +} + +impl ImageCopyCaptureHandler for AnvilState { + fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState { + &mut self.image_copy_capture_state + } + + fn capture_constraints(&mut self, source: &ImageCaptureSource) -> Option { + use smithay::output::WeakOutput; + let weak_output = source.user_data().get::()?; + let output = weak_output.upgrade()?; + let mode = output.current_mode()?; + + Some(BufferConstraints { + size: mode + .size + .to_logical(1) + .to_buffer(1, smithay::utils::Transform::Normal), + shm: vec![ + smithay::reexports::wayland_server::protocol::wl_shm::Format::Argb8888, + smithay::reexports::wayland_server::protocol::wl_shm::Format::Xrgb8888, + ], + #[cfg(any(feature = "udev", feature = "winit", feature = "x11"))] + dma: None, + }) + } + + fn new_session(&mut self, _session: Session) { + // Anvil doesn't track sessions; they clean up on drop + } + + fn frame(&mut self, _session: &SessionRef, frame: Frame) { + // Anvil doesn't implement actual capture + frame.fail(smithay::wayland::image_copy_capture::CaptureFailureReason::Unknown); + } +} + +delegate_dispatch2!(@ AnvilState); + +impl AnvilState { + pub fn init( + display: Display>, + handle: LoopHandle<'static, AnvilState>, + backend_data: BackendData, + listen_on_socket: bool, + ) -> AnvilState { + let dh = display.handle(); + + let clock = Clock::new(); + + // init wayland clients + let socket_name = if listen_on_socket { + let source = ListeningSocketSource::new_auto().unwrap(); + let socket_name = source.socket_name().to_string_lossy().into_owned(); + handle + .insert_source(source, |client_stream, _, data| { + if let Err(err) = data + .display_handle + .insert_client(client_stream, Arc::new(ClientState::default())) + { + warn!("Error adding wayland client: {}", err); + }; + }) + .expect("Failed to init wayland socket source"); + info!(name = socket_name, "Listening on wayland socket"); + Some(socket_name) + } else { + None + }; + handle + .insert_source( + Generic::new(display, Interest::READ, Mode::Level), + |_, display, data| { + profiling::scope!("dispatch_clients"); + // Safety: we don't drop the display + unsafe { + display.get_mut().dispatch_clients(data).unwrap(); + } + Ok(PostAction::Continue) + }, + ) + .expect("Failed to init wayland server source"); + + // init globals + let compositor_state = CompositorState::new::(&dh); + let data_device_state = DataDeviceState::new::(&dh); + let layer_shell_state = WlrLayerShellState::new::(&dh); + let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); + let primary_selection_state = PrimarySelectionState::new::(&dh); + let data_control_state = + DataControlState::new::(&dh, Some(&primary_selection_state), |_| true); + let mut seat_state = SeatState::new(); + let shm_state = ShmState::new::(&dh, vec![]); + let viewporter_state = ViewporterState::new::(&dh); + let xdg_activation_state = XdgActivationState::new::(&dh); + let xdg_decoration_state = XdgDecorationState::new::(&dh); + let xdg_shell_state = XdgShellState::new::(&dh); + let presentation_state = PresentationState::new::(&dh, clock.id() as u32); + let fractional_scale_manager_state = FractionalScaleManagerState::new::(&dh); + let xdg_foreign_state = XdgForeignState::new::(&dh); + let single_pixel_buffer_state = SinglePixelBufferState::new::(&dh); + let fifo_manager_state = FifoManagerState::new::(&dh); + let commit_timing_manager_state = CommitTimingManagerState::new::(&dh); + TextInputManagerState::new::(&dh); + InputMethodManagerState::new::(&dh, |_client| true); + VirtualKeyboardManagerState::new::(&dh, |_client| true); + // Expose global only if backend supports relative motion events + if BackendData::HAS_RELATIVE_MOTION { + RelativePointerManagerState::new::(&dh); + } + PointerConstraintsState::new::(&dh); + if BackendData::HAS_GESTURES { + PointerGesturesState::new::(&dh); + } + TabletManagerState::new::(&dh); + SecurityContextState::new::(&dh, |client| { + client + .get_data::() + .is_none_or(|client_state| client_state.security_context.is_none()) + }); + FixesState::new::(&dh); + + // Image capture protocols (screencopy) + let image_capture_source_state = ImageCaptureSourceState::new(); + let output_capture_source_state = OutputCaptureSourceState::new::(&dh); + let image_copy_capture_state = ImageCopyCaptureState::new::(&dh); + + // init input + let seat_name = backend_data.seat_name(); + let mut seat = seat_state.new_wl_seat(&dh, seat_name.clone()); + + let pointer = seat.add_pointer(); + seat.add_keyboard(XkbConfig::default(), 200, 25) + .expect("Failed to initialize the keyboard"); + + let keyboard_shortcuts_inhibit_state = KeyboardShortcutsInhibitState::new::(&dh); + + #[cfg(feature = "xwayland")] + let xwayland_shell_state = xwayland_shell::XWaylandShellState::new::(&dh.clone()); + + #[cfg(feature = "xwayland")] + XWaylandKeyboardGrabState::new::(&dh.clone()); + + AnvilState { + backend_data, + display_handle: dh, + socket_name, + running: Arc::new(AtomicBool::new(true)), + handle, + space: Space::default(), + popups: PopupManager::default(), + compositor_state, + data_device_state, + layer_shell_state, + output_manager_state, + primary_selection_state, + data_control_state, + seat_state, + keyboard_shortcuts_inhibit_state, + shm_state, + viewporter_state, + xdg_activation_state, + xdg_decoration_state, + xdg_shell_state, + presentation_state, + fractional_scale_manager_state, + xdg_foreign_state, + single_pixel_buffer_state, + fifo_manager_state, + commit_timing_manager_state, + image_capture_source_state, + output_capture_source_state, + image_copy_capture_state, + dnd_icon: None, + suppressed_keys: Vec::new(), + cursor_status: CursorImageStatus::default_named(), + seat_name, + seat, + pointer, + cursor_position_hint: None, + clock, + + #[cfg(feature = "xwayland")] + xwayland_shell_state, + #[cfg(feature = "xwayland")] + xwm: None, + #[cfg(feature = "xwayland")] + xdisplay: None, + #[cfg(feature = "debug")] + renderdoc: renderdoc::RenderDoc::new().ok(), + show_window_preview: false, + } + } + + #[cfg(feature = "xwayland")] + pub fn start_xwayland(&mut self) { + use std::process::Stdio; + + use smithay::wayland::compositor::CompositorHandler; + + let (xwayland, client) = XWayland::spawn( + &self.display_handle, + None, + std::iter::empty::<(String, String)>(), + std::iter::empty::(), + true, + Stdio::null(), + Stdio::null(), + |_| (), + ) + .expect("failed to start XWayland"); + + let display_handle = self.display_handle.clone(); + let ret = self + .handle + .insert_source(xwayland, move |event, _, data| match event { + XWaylandEvent::Ready { + x11_socket, + display_number, + } => { + let xwayland_scale = std::env::var("ANVIL_XWAYLAND_SCALE") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(1.); + data.client_compositor_state(&client) + .set_client_scale(xwayland_scale); + let mut wm = + X11Wm::start_wm(data.handle.clone(), &display_handle, x11_socket, client.clone()) + .expect("Failed to attach X11 Window Manager"); + + let cursor = Cursor::load(); + let image = cursor.get_image(1, Duration::ZERO); + wm.set_cursor( + &image.pixels_rgba, + Size::from((image.width as u16, image.height as u16)), + Point::from((image.xhot as u16, image.yhot as u16)), + ) + .expect("Failed to set xwayland default cursor"); + data.xwm = Some(wm); + data.xdisplay = Some(display_number); + } + XWaylandEvent::Error => { + warn!("XWayland crashed on startup"); + } + }); + if let Err(e) = ret { + tracing::error!("Failed to insert the XWaylandSource into the event loop: {}", e); + } + } +} + +impl AnvilState { + pub fn pre_repaint(&mut self, output: &Output, frame_target: impl Into>) { + let frame_target = frame_target.into(); + + #[allow(clippy::mutable_key_type)] + let mut clients: HashMap = HashMap::new(); + self.space.elements().for_each(|window| { + window.with_surfaces(|surface, states| { + if let Some(mut commit_timer_state) = states + .data_map + .get::() + .map(|commit_timer| commit_timer.lock().unwrap()) + { + commit_timer_state.signal_until(frame_target); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + }); + }); + + let map = smithay::desktop::layer_map_for_output(output); + for layer_surface in map.layers() { + layer_surface.with_surfaces(|surface, states| { + if let Some(mut commit_timer_state) = states + .data_map + .get::() + .map(|commit_timer| commit_timer.lock().unwrap()) + { + commit_timer_state.signal_until(frame_target); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + }); + } + // Drop the lock to the layer map before calling blocker_cleared, which might end up + // calling the commit handler which in turn again could access the layer map. + std::mem::drop(map); + + if let CursorImageStatus::Surface(ref surface) = self.cursor_status { + with_surfaces_surface_tree(surface, |surface, states| { + if let Some(mut commit_timer_state) = states + .data_map + .get::() + .map(|commit_timer| commit_timer.lock().unwrap()) + { + commit_timer_state.signal_until(frame_target); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + }); + } + + if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) { + with_surfaces_surface_tree(surface, |surface, states| { + if let Some(mut commit_timer_state) = states + .data_map + .get::() + .map(|commit_timer| commit_timer.lock().unwrap()) + { + commit_timer_state.signal_until(frame_target); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + }); + } + + let dh = self.display_handle.clone(); + for client in clients.into_values() { + self.client_compositor_state(&client).blocker_cleared(self, &dh); + } + } + + pub fn post_repaint( + &mut self, + output: &Output, + time: impl Into, + dmabuf_feedback: Option, + render_element_states: &RenderElementStates, + ) { + let time = time.into(); + let throttle = Some(Duration::from_secs(1)); + + #[allow(clippy::mutable_key_type)] + let mut clients: HashMap = HashMap::new(); + + self.space.elements().for_each(|window| { + window.with_surfaces(|surface, states| { + let primary_scanout_output = surface_primary_scanout_output(surface, states); + + if let Some(output) = primary_scanout_output.as_ref() { + with_fractional_scale(states, |fraction_scale| { + fraction_scale.set_preferred_scale(output.current_scale().fractional_scale()); + }); + } + + if primary_scanout_output + .as_ref() + .map(|o| o == output) + .unwrap_or(true) + { + let fifo_barrier = states + .cached_state + .get::() + .current() + .barrier + .take(); + + if let Some(fifo_barrier) = fifo_barrier { + fifo_barrier.signal(); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + } + }); + + if self.space.outputs_for_element(window).contains(output) { + window.send_frame(output, time, throttle, surface_primary_scanout_output); + if let Some(dmabuf_feedback) = dmabuf_feedback.as_ref() { + window.send_dmabuf_feedback(output, surface_primary_scanout_output, |surface, _| { + select_dmabuf_feedback( + surface, + render_element_states, + &dmabuf_feedback.render_feedback, + &dmabuf_feedback.scanout_feedback, + ) + }); + } + } + }); + let map = smithay::desktop::layer_map_for_output(output); + for layer_surface in map.layers() { + layer_surface.with_surfaces(|surface, states| { + let primary_scanout_output = surface_primary_scanout_output(surface, states); + + if let Some(output) = primary_scanout_output.as_ref() { + with_fractional_scale(states, |fraction_scale| { + fraction_scale.set_preferred_scale(output.current_scale().fractional_scale()); + }); + } + + if primary_scanout_output + .as_ref() + .map(|o| o == output) + .unwrap_or(true) + { + let fifo_barrier = states + .cached_state + .get::() + .current() + .barrier + .take(); + + if let Some(fifo_barrier) = fifo_barrier { + fifo_barrier.signal(); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + } + }); + + layer_surface.send_frame(output, time, throttle, surface_primary_scanout_output); + if let Some(dmabuf_feedback) = dmabuf_feedback.as_ref() { + layer_surface.send_dmabuf_feedback(output, surface_primary_scanout_output, |surface, _| { + select_dmabuf_feedback( + surface, + render_element_states, + &dmabuf_feedback.render_feedback, + &dmabuf_feedback.scanout_feedback, + ) + }); + } + } + // Drop the lock to the layer map before calling blocker_cleared, which might end up + // calling the commit handler which in turn again could access the layer map. + std::mem::drop(map); + + if let CursorImageStatus::Surface(ref surface) = self.cursor_status { + with_surfaces_surface_tree(surface, |surface, states| { + let primary_scanout_output = surface_primary_scanout_output(surface, states); + + if let Some(output) = primary_scanout_output.as_ref() { + with_fractional_scale(states, |fraction_scale| { + fraction_scale.set_preferred_scale(output.current_scale().fractional_scale()); + }); + } + + if primary_scanout_output + .as_ref() + .map(|o| o == output) + .unwrap_or(true) + { + let fifo_barrier = states + .cached_state + .get::() + .current() + .barrier + .take(); + + if let Some(fifo_barrier) = fifo_barrier { + fifo_barrier.signal(); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + } + }); + } + + if let Some(surface) = self.dnd_icon.as_ref().map(|icon| &icon.surface) { + with_surfaces_surface_tree(surface, |surface, states| { + let primary_scanout_output = surface_primary_scanout_output(surface, states); + + if let Some(output) = primary_scanout_output.as_ref() { + with_fractional_scale(states, |fraction_scale| { + fraction_scale.set_preferred_scale(output.current_scale().fractional_scale()); + }); + } + + if primary_scanout_output + .as_ref() + .map(|o| o == output) + .unwrap_or(true) + { + let fifo_barrier = states + .cached_state + .get::() + .current() + .barrier + .take(); + + if let Some(fifo_barrier) = fifo_barrier { + fifo_barrier.signal(); + let client = surface.client().unwrap(); + clients.insert(client.id(), client); + } + } + }); + } + + let dh = self.display_handle.clone(); + for client in clients.into_values() { + self.client_compositor_state(&client).blocker_cleared(self, &dh); + } + } +} + +pub fn update_primary_scanout_output( + space: &Space, + output: &Output, + dnd_icon: &Option, + cursor_status: &CursorImageStatus, + render_element_states: &RenderElementStates, +) { + space.elements().for_each(|window| { + window.with_surfaces(|surface, states| { + update_surface_primary_scanout_output( + surface, + output, + states, + None, + render_element_states, + default_primary_scanout_output_compare, + ); + }); + }); + let map = smithay::desktop::layer_map_for_output(output); + for layer_surface in map.layers() { + layer_surface.with_surfaces(|surface, states| { + update_surface_primary_scanout_output( + surface, + output, + states, + None, + render_element_states, + default_primary_scanout_output_compare, + ); + }); + } + + if let CursorImageStatus::Surface(surface) = cursor_status { + with_surfaces_surface_tree(surface, |surface, states| { + update_surface_primary_scanout_output( + surface, + output, + states, + None, + render_element_states, + default_primary_scanout_output_compare, + ); + }); + } + + if let Some(surface) = dnd_icon.as_ref().map(|icon| &icon.surface) { + with_surfaces_surface_tree(surface, |surface, states| { + update_surface_primary_scanout_output( + surface, + output, + states, + None, + render_element_states, + default_primary_scanout_output_compare, + ); + }); + } +} + +#[derive(Debug, Clone)] +pub struct SurfaceDmabufFeedback { + pub render_feedback: DmabufFeedback, + pub scanout_feedback: DmabufFeedback, +} + +#[profiling::function] +pub fn take_presentation_feedback( + output: &Output, + space: &Space, + render_element_states: &RenderElementStates, +) -> OutputPresentationFeedback { + let mut output_presentation_feedback = OutputPresentationFeedback::new(output); + + space.elements().for_each(|window| { + if space.outputs_for_element(window).contains(output) { + window.take_presentation_feedback( + &mut output_presentation_feedback, + surface_primary_scanout_output, + |surface, _| { + surface_presentation_feedback_flags_from_states(surface, None, render_element_states) + }, + ); + } + }); + let map = smithay::desktop::layer_map_for_output(output); + for layer_surface in map.layers() { + layer_surface.take_presentation_feedback( + &mut output_presentation_feedback, + surface_primary_scanout_output, + |surface, _| { + surface_presentation_feedback_flags_from_states(surface, None, render_element_states) + }, + ); + } + + output_presentation_feedback +} + +pub trait Backend { + const HAS_RELATIVE_MOTION: bool = false; + const HAS_GESTURES: bool = false; + fn seat_name(&self) -> String; + fn reset_buffers(&mut self, output: &Output); + fn early_import(&mut self, surface: &WlSurface); + fn update_led_state(&mut self, led_state: LedState); +} diff --git a/vendor/smithay/anvil/src/udev.rs b/vendor/smithay/anvil/src/udev.rs new file mode 100644 index 0000000..743ef3e --- /dev/null +++ b/vendor/smithay/anvil/src/udev.rs @@ -0,0 +1,1688 @@ +// Allow in this module because of existing usage +#![allow(clippy::uninlined_format_args)] +use std::{ + collections::hash_map::HashMap, + io, + ops::Not, + path::Path, + sync::{Mutex, Once, atomic::Ordering}, + time::{Duration, Instant}, +}; + +use crate::{ + drawing::*, + render::*, + shell::WindowElement, + state::{AnvilState, Backend, take_presentation_feedback, update_primary_scanout_output}, +}; +use crate::{ + shell::WindowRenderElement, + state::{DndIcon, SurfaceDmabufFeedback}, +}; +#[cfg(feature = "renderer_sync")] +use smithay::backend::drm::compositor::PrimaryPlaneElement; +#[cfg(feature = "egl")] +use smithay::backend::renderer::ImportEgl; +#[cfg(feature = "debug")] +use smithay::backend::renderer::{ImportMem, multigpu::MultiTexture}; +use smithay::{ + backend::{ + SwapBuffersError, + allocator::{ + Fourcc, Modifier, + dmabuf::Dmabuf, + format::FormatSet, + gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, + }, + drm::{ + CreateDrmNodeError, DrmAccessError, DrmDevice, DrmDeviceFd, DrmError, DrmEvent, DrmEventMetadata, + DrmEventTime, DrmNode, DrmSurface, GbmBufferedSurface, NodeType, + compositor::{DrmCompositor, FrameFlags}, + exporter::gbm::GbmFramebufferExporter, + output::{DrmOutput, DrmOutputManager, DrmOutputRenderElements}, + }, + egl::{self, EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, + input::InputEvent, + libinput::{LibinputInputBackend, LibinputSessionInterface}, + renderer::{ + DebugFlags, ImportDma, ImportMemWl, + damage::Error as OutputDamageTrackerError, + element::{AsRenderElements, RenderElementStates, memory::MemoryRenderBuffer}, + gles::{Capability, GlesRenderer}, + multigpu::{GpuManager, MultiRenderer, gbm::GbmGlesBackend}, + }, + session::{ + Event as SessionEvent, Session, + libseat::{self, LibSeatSession}, + }, + udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu}, + }, + desktop::{ + space::{Space, SurfaceTree}, + utils::OutputPresentationFeedback, + }, + input::{ + keyboard::LedState, + pointer::{CursorImageAttributes, CursorImageStatus}, + }, + output::{Mode as WlMode, Output, PhysicalProperties}, + reexports::{ + calloop::{ + EventLoop, RegistrationToken, + timer::{TimeoutAction, Timer}, + }, + drm::{ + Device as _, + control::{Device, ModeTypeFlags, connector, crtc}, + }, + input::{DeviceCapability, Libinput}, + rustix::fs::OFlags, + wayland_protocols::wp::{ + linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1, + presentation_time::server::wp_presentation_feedback, + }, + wayland_server::{Display, DisplayHandle, backend::GlobalId, protocol::wl_surface}, + }, + utils::{DeviceFd, IsAlive, Logical, Monotonic, Point, Scale, Time, Transform}, + wayland::{ + compositor, + dmabuf::{DmabufFeedbackBuilder, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}, + drm_lease::{ + DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected, + }, + drm_syncobj::{DrmSyncobjHandler, DrmSyncobjState, supports_syncobj_eventfd}, + presentation::Refresh, + }, +}; +use smithay_drm_extras::{ + display_info, + drm_scanner::{DrmScanEvent, DrmScanner}, +}; +use tracing::{debug, error, info, trace, warn}; + +// we cannot simply pick the first supported format of the intersection of *all* formats, because: +// - we do not want something like Abgr4444, which looses color information, if something better is available +// - some formats might perform terribly +// - we might need some work-arounds, if one supports modifiers, but the other does not +// +// So lets just pick `ARGB2101010` (10-bit) or `ARGB8888` (8-bit) for now, they are widely supported. +const SUPPORTED_FORMATS: &[Fourcc] = &[ + Fourcc::Abgr2101010, + Fourcc::Argb2101010, + Fourcc::Abgr8888, + Fourcc::Argb8888, +]; +const SUPPORTED_FORMATS_8BIT_ONLY: &[Fourcc] = &[Fourcc::Abgr8888, Fourcc::Argb8888]; + +type UdevRenderer<'a> = MultiRenderer< + 'a, + 'a, + GbmGlesBackend, + GbmGlesBackend, +>; + +#[derive(Debug, PartialEq)] +struct UdevOutputId { + device_id: DrmNode, + crtc: crtc::Handle, +} + +pub struct UdevData { + pub session: LibSeatSession, + dh: DisplayHandle, + dmabuf_state: Option<(DmabufState, DmabufGlobal)>, + syncobj_state: Option, + primary_gpu: DrmNode, + gpus: GpuManager>, + backends: HashMap, + pointer_images: Vec<(xcursor::parser::Image, MemoryRenderBuffer)>, + pointer_element: PointerElement, + #[cfg(feature = "debug")] + fps_texture: Option, + pointer_image: crate::cursor::Cursor, + debug_flags: DebugFlags, + keyboards: Vec, +} + +impl UdevData { + pub fn set_debug_flags(&mut self, flags: DebugFlags) { + if self.debug_flags != flags { + self.debug_flags = flags; + + for backend in self.backends.values_mut() { + for surface in backend.surfaces.values_mut() { + surface.drm_output.set_debug_flags(flags); + } + } + } + } + + pub fn debug_flags(&self) -> DebugFlags { + self.debug_flags + } +} + +impl DmabufHandler for AnvilState { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.backend_data.dmabuf_state.as_mut().unwrap().0 + } + + fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) { + if self + .backend_data + .gpus + .single_renderer(&self.backend_data.primary_gpu) + .and_then(|mut renderer| renderer.import_dmabuf(&dmabuf, None)) + .is_ok() + { + if dmabuf.node().is_none() { + dmabuf.set_node(self.backend_data.primary_gpu); + } + let _ = notifier.successful::>(); + } else { + notifier.failed(); + } + } +} + +impl Backend for UdevData { + const HAS_RELATIVE_MOTION: bool = true; + const HAS_GESTURES: bool = true; + + fn seat_name(&self) -> String { + self.session.seat() + } + + fn reset_buffers(&mut self, output: &Output) { + if let Some(id) = output.user_data().get::() { + if let Some(gpu) = self.backends.get_mut(&id.device_id) { + if let Some(surface) = gpu.surfaces.get_mut(&id.crtc) { + surface.drm_output.reset_buffers(); + } + } + } + } + + fn early_import(&mut self, surface: &wl_surface::WlSurface) { + if let Err(err) = self.gpus.early_import(self.primary_gpu, surface) { + warn!("Early buffer import failed: {}", err); + } + } + + fn update_led_state(&mut self, led_state: LedState) { + for keyboard in self.keyboards.iter_mut() { + keyboard.led_update(led_state.into()); + } + } +} + +pub fn run_udev() { + let mut event_loop = EventLoop::try_new().unwrap(); + let display = Display::new().unwrap(); + let mut display_handle = display.handle(); + + /* + * Initialize session + */ + let (session, notifier) = match LibSeatSession::new() { + Ok(ret) => ret, + Err(err) => { + error!("Could not initialize a session: {}", err); + return; + } + }; + + /* + * Initialize the compositor + */ + let primary_gpu = if let Ok(var) = std::env::var("ANVIL_DRM_DEVICE") { + DrmNode::from_path(var).expect("Invalid drm device path") + } else { + primary_gpu(session.seat()) + .unwrap() + .and_then(|x| DrmNode::from_path(x).ok()?.node_with_type(NodeType::Render)?.ok()) + .unwrap_or_else(|| { + all_gpus(session.seat()) + .unwrap() + .into_iter() + .find_map(|x| DrmNode::from_path(x).ok()) + .expect("No GPU!") + }) + }; + info!("Using {} as primary gpu.", primary_gpu); + + let gpus = GpuManager::new(GbmGlesBackend::with_factory(|display| { + let context = EGLContext::new_with_priority(display, ContextPriority::High)?; + let mut capabilities = unsafe { GlesRenderer::supported_capabilities(&context)? }; + if std::env::var("ANVIL_GLES_DISABLE_INSTANCING").is_ok() { + capabilities.retain(|capability| *capability != Capability::Instancing); + } + Ok(unsafe { GlesRenderer::with_capabilities(context, capabilities)? }) + })) + .unwrap(); + + let data = UdevData { + dh: display_handle.clone(), + dmabuf_state: None, + syncobj_state: None, + session, + primary_gpu, + gpus, + backends: HashMap::new(), + pointer_image: crate::cursor::Cursor::load(), + pointer_images: Vec::new(), + pointer_element: PointerElement::default(), + #[cfg(feature = "debug")] + fps_texture: None, + debug_flags: DebugFlags::empty(), + keyboards: Vec::new(), + }; + let mut state = AnvilState::init(display, event_loop.handle(), data, true); + + /* + * Initialize the udev backend + */ + let udev_backend = match UdevBackend::new(&state.seat_name) { + Ok(ret) => ret, + Err(err) => { + error!(error = ?err, "Failed to initialize udev backend"); + return; + } + }; + + /* + * Initialize libinput backend + */ + let mut libinput_context = Libinput::new_with_udev::>( + state.backend_data.session.clone().into(), + ); + libinput_context.udev_assign_seat(&state.seat_name).unwrap(); + let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); + + /* + * Bind all our objects that get driven by the event loop + */ + event_loop + .handle() + .insert_source(libinput_backend, move |mut event, _, data| { + let dh = data.backend_data.dh.clone(); + if let InputEvent::DeviceAdded { device } = &mut event { + if device.has_capability(DeviceCapability::Keyboard) { + if let Some(led_state) = data.seat.get_keyboard().map(|keyboard| keyboard.led_state()) { + device.led_update(led_state.into()); + } + data.backend_data.keyboards.push(device.clone()); + } + } else if let InputEvent::DeviceRemoved { ref device } = event { + if device.has_capability(DeviceCapability::Keyboard) { + data.backend_data.keyboards.retain(|item| item != device); + } + } + + data.process_input_event(&dh, event) + }) + .unwrap(); + + event_loop + .handle() + .insert_source(notifier, move |event, &mut (), data| match event { + SessionEvent::PauseSession => { + libinput_context.suspend(); + info!("pausing session"); + + for backend in data.backend_data.backends.values_mut() { + backend.drm_output_manager.pause(); + backend.active_leases.clear(); + if let Some(lease_global) = backend.leasing_global.as_mut() { + lease_global.suspend(); + } + } + } + SessionEvent::ActivateSession => { + info!("resuming session"); + + if let Err(err) = libinput_context.resume() { + error!("Failed to resume libinput context: {:?}", err); + } + for (node, backend) in data + .backend_data + .backends + .iter_mut() + .map(|(handle, backend)| (*handle, backend)) + { + // if we do not care about flicking (caused by modesetting) we could just + // pass true for disable connectors here. this would make sure our drm + // device is in a known state (all connectors and planes disabled). + // but for demonstration we choose a more optimistic path by leaving the + // state as is and assume it will just work. If this assumption fails + // we will try to reset the state when trying to queue a frame. + backend + .drm_output_manager + .lock() + .activate(false) + .expect("failed to activate drm backend"); + if let Some(lease_global) = backend.leasing_global.as_mut() { + lease_global.resume::>(); + } + data.handle + .insert_idle(move |data| data.render(node, None, data.clock.now())); + } + } + }) + .unwrap(); + + // We try to initialize the primary node before others to make sure + // any display only node can fall back to the primary node for rendering + let primary_node = primary_gpu + .node_with_type(NodeType::Primary) + .and_then(|node| node.ok()); + let primary_device = udev_backend.device_list().find(|(device_id, _)| { + primary_node + .map(|primary_node| *device_id == primary_node.dev_id()) + .unwrap_or(false) + || *device_id == primary_gpu.dev_id() + }); + + if let Some((device_id, path)) = primary_device { + let node = DrmNode::from_dev_id(device_id).expect("failed to get primary node"); + state + .device_added(node, path) + .expect("failed to initialize primary node"); + } + + let primary_device_id = primary_device.map(|(device_id, _)| device_id); + for (device_id, path) in udev_backend.device_list() { + if Some(device_id) == primary_device_id { + continue; + } + + if let Err(err) = DrmNode::from_dev_id(device_id) + .map_err(DeviceAddError::DrmNode) + .and_then(|node| state.device_added(node, path)) + { + error!("Skipping device {device_id}: {err}"); + } + } + state.shm_state.update_formats( + state + .backend_data + .gpus + .single_renderer(&primary_gpu) + .unwrap() + .shm_formats(), + ); + + #[cfg_attr(not(feature = "egl"), allow(unused_mut))] + let mut renderer = state.backend_data.gpus.single_renderer(&primary_gpu).unwrap(); + + #[cfg(feature = "debug")] + { + #[allow(deprecated)] + let fps_image = + image::io::Reader::with_format(std::io::Cursor::new(FPS_NUMBERS_PNG), image::ImageFormat::Png) + .decode() + .unwrap(); + let fps_texture = renderer + .import_memory( + &fps_image.to_rgba8(), + Fourcc::Abgr8888, + (fps_image.width() as i32, fps_image.height() as i32).into(), + false, + ) + .expect("Unable to upload FPS texture"); + + for backend in state.backend_data.backends.values_mut() { + for surface in backend.surfaces.values_mut() { + surface.fps_element = Some(FpsElement::new(fps_texture.clone())); + } + } + state.backend_data.fps_texture = Some(fps_texture); + } + + #[cfg(feature = "egl")] + { + info!(?primary_gpu, "Trying to initialize EGL Hardware Acceleration",); + match renderer.bind_wl_display(&display_handle) { + Ok(_) => info!("EGL hardware-acceleration enabled"), + Err(err) => info!(?err, "Failed to initialize EGL hardware-acceleration"), + } + } + + // init dmabuf support with format list from our primary gpu + let dmabuf_formats = renderer.dmabuf_formats(); + let default_feedback = DmabufFeedbackBuilder::new(primary_gpu.dev_id(), dmabuf_formats) + .build() + .unwrap(); + let mut dmabuf_state = DmabufState::new(); + let global = dmabuf_state + .create_global_with_default_feedback::>(&display_handle, &default_feedback); + state.backend_data.dmabuf_state = Some((dmabuf_state, global)); + + let gpus = &mut state.backend_data.gpus; + state + .backend_data + .backends + .iter_mut() + .for_each(|(node, backend_data)| { + // Update the per drm surface dmabuf feedback + backend_data.surfaces.values_mut().for_each(|surface_data| { + surface_data.dmabuf_feedback = surface_data.dmabuf_feedback.take().or_else(|| { + surface_data.drm_output.with_compositor(|compositor| { + get_surface_dmabuf_feedback( + primary_gpu, + surface_data.render_node, + *node, + gpus, + compositor.surface(), + ) + }) + }); + }); + }); + + // Expose syncobj protocol if supported by primary GPU + if let Some(primary_node) = state + .backend_data + .primary_gpu + .node_with_type(NodeType::Primary) + .and_then(|x| x.ok()) + { + if let Some(backend) = state.backend_data.backends.get(&primary_node) { + let import_device = backend.drm_output_manager.device().device_fd().clone(); + if supports_syncobj_eventfd(&import_device) { + let syncobj_state = + DrmSyncobjState::new::>(&display_handle, import_device); + state.backend_data.syncobj_state = Some(syncobj_state); + } + } + } + + event_loop + .handle() + .insert_source(udev_backend, move |event, _, data| match event { + UdevEvent::Added { device_id, path } => { + if let Err(err) = DrmNode::from_dev_id(device_id) + .map_err(DeviceAddError::DrmNode) + .and_then(|node| data.device_added(node, &path)) + { + error!("Skipping device {device_id}: {err}"); + } + } + UdevEvent::Changed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + data.device_changed(node) + } + } + UdevEvent::Removed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + data.device_removed(node) + } + } + }) + .unwrap(); + + /* + * Start XWayland if supported + */ + #[cfg(feature = "xwayland")] + state.start_xwayland(); + + #[cfg(feature = "libei")] + crate::libei::listen_eis(&event_loop.handle()); + + /* + * And run our loop + */ + + while state.running.load(Ordering::SeqCst) { + let result = event_loop.dispatch(Some(Duration::from_millis(16)), &mut state); + if result.is_err() { + state.running.store(false, Ordering::SeqCst); + } else { + state.space.refresh(); + state.popups.cleanup(); + display_handle.flush_clients().unwrap(); + } + } +} + +impl DrmLeaseHandler for AnvilState { + fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState { + self.backend_data + .backends + .get_mut(&node) + .unwrap() + .leasing_global + .as_mut() + .unwrap() + } + + fn lease_request( + &mut self, + node: DrmNode, + request: DrmLeaseRequest, + ) -> Result { + let backend = self + .backend_data + .backends + .get(&node) + .ok_or(LeaseRejected::default())?; + + let drm_device = backend.drm_output_manager.device(); + let mut builder = DrmLeaseBuilder::new(drm_device); + for conn in request.connectors { + if let Some((_, crtc)) = backend + .non_desktop_connectors + .iter() + .find(|(handle, _)| *handle == conn) + { + builder.add_connector(conn); + builder.add_crtc(*crtc); + let planes = drm_device.planes(crtc).map_err(LeaseRejected::with_cause)?; + let (primary_plane, primary_plane_claim) = planes + .primary + .iter() + .find_map(|plane| { + drm_device + .claim_plane(plane.handle, *crtc) + .map(|claim| (plane, claim)) + }) + .ok_or_else(LeaseRejected::default)?; + builder.add_plane(primary_plane.handle, primary_plane_claim); + if let Some((cursor, claim)) = planes.cursor.iter().find_map(|plane| { + drm_device + .claim_plane(plane.handle, *crtc) + .map(|claim| (plane, claim)) + }) { + builder.add_plane(cursor.handle, claim); + } + } else { + tracing::warn!(?conn, "Lease requested for desktop connector, denying request"); + return Err(LeaseRejected::default()); + } + } + + Ok(builder) + } + + fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) { + let backend = self.backend_data.backends.get_mut(&node).unwrap(); + backend.active_leases.push(lease); + } + + fn lease_destroyed(&mut self, node: DrmNode, lease: u32) { + let backend = self.backend_data.backends.get_mut(&node).unwrap(); + backend.active_leases.retain(|l| l.id() != lease); + } +} + +impl DrmSyncobjHandler for AnvilState { + fn drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState> { + self.backend_data.syncobj_state.as_mut() + } +} + +pub type RenderSurface = GbmBufferedSurface, Option>; + +pub type GbmDrmCompositor = DrmCompositor< + GbmAllocator, + GbmDevice, + Option, + DrmDeviceFd, +>; + +struct SurfaceData { + dh: DisplayHandle, + device_id: DrmNode, + render_node: Option, + output: Output, + global: Option, + drm_output: DrmOutput< + GbmAllocator, + GbmFramebufferExporter, + Option, + DrmDeviceFd, + >, + disable_direct_scanout: bool, + #[cfg(feature = "debug")] + fps: fps_ticker::Fps, + #[cfg(feature = "debug")] + fps_element: Option>, + dmabuf_feedback: Option, + last_presentation_time: Option>, + vblank_throttle_timer: Option, +} + +impl Drop for SurfaceData { + fn drop(&mut self) { + self.output.leave_all(); + if let Some(global) = self.global.take() { + self.dh.remove_global::>(global); + } + } +} + +struct BackendData { + surfaces: HashMap, + non_desktop_connectors: Vec<(connector::Handle, crtc::Handle)>, + leasing_global: Option, + active_leases: Vec, + drm_output_manager: DrmOutputManager< + GbmAllocator, + GbmFramebufferExporter, + Option, + DrmDeviceFd, + >, + drm_scanner: DrmScanner, + render_node: Option, + registration_token: RegistrationToken, +} + +#[derive(Debug, thiserror::Error)] +enum DeviceAddError { + #[error("Failed to open device using libseat: {0}")] + DeviceOpen(libseat::Error), + #[error("Failed to initialize drm device: {0}")] + DrmDevice(DrmError), + #[error("Failed to initialize gbm device: {0}")] + GbmDevice(std::io::Error), + #[error("Failed to access drm node: {0}")] + DrmNode(CreateDrmNodeError), + #[error("Failed to add device to GpuManager: {0}")] + AddNode(egl::Error), + #[error("The device has no render node")] + NoRenderNode, + #[error("Primary GPU is missing")] + PrimaryGpuMissing, +} + +fn get_surface_dmabuf_feedback( + primary_gpu: DrmNode, + render_node: Option, + scanout_node: DrmNode, + gpus: &mut GpuManager>, + surface: &DrmSurface, +) -> Option { + let primary_formats = gpus.single_renderer(&primary_gpu).ok()?.dmabuf_formats(); + let render_formats = if let Some(render_node) = render_node { + gpus.single_renderer(&render_node).ok()?.dmabuf_formats() + } else { + FormatSet::default() + }; + + let all_render_formats = primary_formats + .iter() + .chain(render_formats.iter()) + .copied() + .collect::(); + + let planes = surface.planes().clone(); + + // We limit the scan-out tranche to formats we can also render from + // so that there is always a fallback render path available in case + // the supplied buffer can not be scanned out directly + let planes_formats = surface + .plane_info() + .formats + .iter() + .copied() + .chain(planes.overlay.into_iter().flat_map(|p| p.formats)) + .collect::() + .intersection(&all_render_formats) + .copied() + .collect::(); + + let builder = DmabufFeedbackBuilder::new(primary_gpu.dev_id(), primary_formats); + let render_feedback = if let Some(render_node) = render_node { + builder + .clone() + .add_preference_tranche( + render_node.dev_id(), + zwp_linux_dmabuf_feedback_v1::TrancheFlags::Sampling, + render_formats.clone(), + 3u32..=6, + ) + .build() + .unwrap() + } else { + builder.clone().build().unwrap() + }; + + let scanout_feedback = builder + .add_preference_tranche( + surface.device_fd().dev_id().unwrap(), + zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout, + planes_formats, + 4u32..=6, + ) + .add_preference_tranche( + scanout_node.dev_id(), + zwp_linux_dmabuf_feedback_v1::TrancheFlags::Sampling, + render_formats, + 4u32..=6, + ) + .build() + .unwrap(); + + Some(SurfaceDmabufFeedback { + render_feedback, + scanout_feedback, + }) +} + +impl AnvilState { + fn device_added(&mut self, node: DrmNode, path: &Path) -> Result<(), DeviceAddError> { + // Try to open the device + let fd = self + .backend_data + .session + .open( + path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + ) + .map_err(DeviceAddError::DeviceOpen)?; + + let fd = DrmDeviceFd::new(DeviceFd::from(fd)); + + let (drm, notifier) = DrmDevice::new(fd.clone(), true).map_err(DeviceAddError::DrmDevice)?; + let gbm = GbmDevice::new(fd).map_err(DeviceAddError::GbmDevice)?; + + let registration_token = self + .handle + .insert_source( + notifier, + move |event, metadata, data: &mut AnvilState<_>| match event { + DrmEvent::VBlank(crtc) => { + profiling::scope!("vblank", &format!("{crtc:?}")); + data.frame_finish(node, crtc, metadata); + } + DrmEvent::Error(error) => { + error!("{:?}", error); + } + }, + ) + .unwrap(); + + let mut try_initialize_gpu = || { + let display = unsafe { EGLDisplay::new(gbm.clone()).map_err(DeviceAddError::AddNode)? }; + let egl_device = EGLDevice::device_for_display(&display).map_err(DeviceAddError::AddNode)?; + + if egl_device.is_software() { + return Err(DeviceAddError::NoRenderNode); + } + + let render_node = egl_device.try_get_render_node().ok().flatten().unwrap_or(node); + self.backend_data + .gpus + .as_mut() + .add_node(render_node, gbm.clone()) + .map_err(DeviceAddError::AddNode)?; + + std::result::Result::::Ok(render_node) + }; + + let render_node = try_initialize_gpu() + .inspect_err(|err| { + warn!(?err, "failed to initialize gpu"); + }) + .ok(); + + let allocator = render_node + .is_some() + .then(|| GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT)) + .or_else(|| { + self.backend_data + .backends + .get(&self.backend_data.primary_gpu) + .or_else(|| { + self.backend_data + .backends + .values() + .find(|backend| backend.render_node == Some(self.backend_data.primary_gpu)) + }) + .map(|backend| backend.drm_output_manager.allocator().clone()) + }) + .ok_or(DeviceAddError::PrimaryGpuMissing)?; + + let framebuffer_exporter = GbmFramebufferExporter::new(gbm.clone(), render_node.into()); + + let color_formats = if std::env::var("ANVIL_DISABLE_10BIT").is_ok() { + SUPPORTED_FORMATS_8BIT_ONLY + } else { + SUPPORTED_FORMATS + }; + let mut renderer = self + .backend_data + .gpus + .single_renderer(&render_node.unwrap_or(self.backend_data.primary_gpu)) + .unwrap(); + let render_formats = renderer + .as_mut() + .egl_context() + .dmabuf_render_formats() + .iter() + .filter(|format| render_node.is_some() || format.modifier == Modifier::Linear) + .copied() + .collect::(); + + let drm_output_manager = DrmOutputManager::new( + drm, + allocator, + framebuffer_exporter, + Some(gbm), + color_formats.iter().copied(), + render_formats, + ); + + self.backend_data.backends.insert( + node, + BackendData { + registration_token, + drm_output_manager, + drm_scanner: DrmScanner::new(), + non_desktop_connectors: Vec::new(), + render_node, + surfaces: HashMap::new(), + leasing_global: DrmLeaseState::new::>(&self.display_handle, &node) + .inspect_err(|err| { + warn!(?err, "Failed to initialize drm lease global for: {}", node); + }) + .ok(), + active_leases: Vec::new(), + }, + ); + + self.device_changed(node); + + Ok(()) + } + + fn connector_connected(&mut self, node: DrmNode, connector: connector::Info, crtc: crtc::Handle) { + let device = if let Some(device) = self.backend_data.backends.get_mut(&node) { + device + } else { + return; + }; + + let render_node = device.render_node.unwrap_or(self.backend_data.primary_gpu); + let mut renderer = self.backend_data.gpus.single_renderer(&render_node).unwrap(); + + let output_name = format!("{}-{}", connector.interface().as_str(), connector.interface_id()); + info!(?crtc, "Trying to setup connector {}", output_name,); + + let drm_device = device.drm_output_manager.device(); + + let non_desktop = drm_device + .get_properties(connector.handle()) + .ok() + .and_then(|props| { + let (info, value) = props + .into_iter() + .filter_map(|(handle, value)| { + let info = drm_device.get_property(handle).ok()?; + + Some((info, value)) + }) + .find(|(info, _)| info.name().to_str() == Ok("non-desktop"))?; + + info.value_type().convert_value(value).as_boolean() + }) + .unwrap_or(false); + + let display_info = display_info::for_connector(drm_device, connector.handle()); + + let make = display_info + .as_ref() + .and_then(|info| info.make()) + .unwrap_or_else(|| "Unknown".into()); + + let model = display_info + .as_ref() + .and_then(|info| info.model()) + .unwrap_or_else(|| "Unknown".into()); + + let serial_number = display_info + .as_ref() + .and_then(|info| info.serial()) + .unwrap_or_else(|| "Unknown".into()); + + if non_desktop { + info!("Connector {} is non-desktop, setting up for leasing", output_name); + device.non_desktop_connectors.push((connector.handle(), crtc)); + if let Some(lease_state) = device.leasing_global.as_mut() { + lease_state.add_connector::>( + connector.handle(), + output_name, + format!("{make} {model}"), + ); + } + } else { + let mode_id = connector + .modes() + .iter() + .position(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + .unwrap_or(0); + + let drm_mode = connector.modes()[mode_id]; + let wl_mode = WlMode::from(drm_mode); + + let (phys_w, phys_h) = connector.size().unwrap_or((0, 0)); + let output = Output::new( + output_name, + PhysicalProperties { + size: (phys_w as i32, phys_h as i32).into(), + subpixel: connector.subpixel().into(), + make, + model, + serial_number, + }, + ); + let global = output.create_global::>(&self.display_handle); + + let x = self + .space + .outputs() + .fold(0, |acc, o| acc + self.space.output_geometry(o).unwrap().size.w); + let position = (x, 0).into(); + + output.set_preferred(wl_mode); + output.change_current_state(Some(wl_mode), None, None, Some(position)); + self.space.map_output(&output, position); + + output.user_data().insert_if_missing(|| UdevOutputId { + crtc, + device_id: node, + }); + + #[cfg(feature = "debug")] + let fps_element = self.backend_data.fps_texture.clone().map(FpsElement::new); + + let driver = match drm_device.get_driver() { + Ok(driver) => driver, + Err(err) => { + warn!("Failed to query drm driver: {}", err); + return; + } + }; + + let mut planes = match drm_device.planes(&crtc) { + Ok(planes) => planes, + Err(err) => { + warn!("Failed to query crtc planes: {}", err); + return; + } + }; + + // Using an overlay plane on a nvidia card breaks + if driver.name().to_string_lossy().to_lowercase().contains("nvidia") + || driver + .description() + .to_string_lossy() + .to_lowercase() + .contains("nvidia") + { + planes.overlay = vec![]; + } + + let drm_output = match device + .drm_output_manager + .lock() + .initialize_output::<_, OutputRenderElements, WindowRenderElement>>>( + crtc, + drm_mode, + &[connector.handle()], + &output, + Some(planes), + &mut renderer, + &DrmOutputRenderElements::default(), + ) { + Ok(drm_output) => drm_output, + Err(err) => { + warn!("Failed to initialize drm output: {}", err); + return; + } + }; + + let disable_direct_scanout = std::env::var("ANVIL_DISABLE_DIRECT_SCANOUT").is_ok(); + + let dmabuf_feedback = drm_output.with_compositor(|compositor| { + compositor.set_debug_flags(self.backend_data.debug_flags); + + get_surface_dmabuf_feedback( + self.backend_data.primary_gpu, + device.render_node, + node, + &mut self.backend_data.gpus, + compositor.surface(), + ) + }); + + let surface = SurfaceData { + dh: self.display_handle.clone(), + device_id: node, + render_node: device.render_node, + output, + global: Some(global), + drm_output, + disable_direct_scanout, + #[cfg(feature = "debug")] + fps: fps_ticker::Fps::default(), + #[cfg(feature = "debug")] + fps_element, + dmabuf_feedback, + last_presentation_time: None, + vblank_throttle_timer: None, + }; + + device.surfaces.insert(crtc, surface); + + // kick-off rendering + self.handle.insert_idle(move |state| { + state.render_surface(node, crtc, state.clock.now()); + }); + } + } + + fn connector_disconnected(&mut self, node: DrmNode, connector: connector::Info, crtc: crtc::Handle) { + let device = if let Some(device) = self.backend_data.backends.get_mut(&node) { + device + } else { + return; + }; + + if let Some(pos) = device + .non_desktop_connectors + .iter() + .position(|(handle, _)| *handle == connector.handle()) + { + let _ = device.non_desktop_connectors.remove(pos); + if let Some(leasing_state) = device.leasing_global.as_mut() { + leasing_state.withdraw_connector(connector.handle()); + } + } else if let Some(surface) = device.surfaces.remove(&crtc) { + self.space.unmap_output(&surface.output); + self.space.refresh(); + } + + let render_node = device.render_node.unwrap_or(self.backend_data.primary_gpu); + let mut renderer = self.backend_data.gpus.single_renderer(&render_node).unwrap(); + let _ = device.drm_output_manager.lock().try_to_restore_modifiers::<_, OutputRenderElements< + UdevRenderer<'_>, + WindowRenderElement>, + >>( + &mut renderer, + // FIXME: For a flicker free operation we should return the actual elements for this output.. + // Instead we just use black to "simulate" a modeset :) + &DrmOutputRenderElements::default(), + ); + } + + fn device_changed(&mut self, node: DrmNode) { + let device = if let Some(device) = self.backend_data.backends.get_mut(&node) { + device + } else { + return; + }; + + let scan_result = match device + .drm_scanner + .scan_connectors(device.drm_output_manager.device()) + { + Ok(scan_result) => scan_result, + Err(err) => { + tracing::warn!(?err, "Failed to scan connectors"); + return; + } + }; + + for event in scan_result { + match event { + DrmScanEvent::Connected { + connector, + crtc: Some(crtc), + } => { + self.connector_connected(node, connector, crtc); + } + DrmScanEvent::Disconnected { + connector, + crtc: Some(crtc), + } => { + self.connector_disconnected(node, connector, crtc); + } + // The connector's mode list changed while it stayed connected (e.g. EDID + // arrived after the initial probe returned empty/fallback modes). Compositors + // should re-evaluate the output's mode selection and recreate the surface here. + DrmScanEvent::Changed { .. } => {} + _ => {} + } + } + + // fixup window coordinates + crate::shell::fixup_positions(&mut self.space, self.pointer.current_location()); + } + + fn device_removed(&mut self, node: DrmNode) { + let device = if let Some(device) = self.backend_data.backends.get_mut(&node) { + device + } else { + return; + }; + + let crtcs: Vec<_> = device + .drm_scanner + .crtcs() + .map(|(info, crtc)| (info.clone(), crtc)) + .collect(); + + for (connector, crtc) in crtcs { + self.connector_disconnected(node, connector, crtc); + } + + debug!("Surfaces dropped"); + + // drop the backends on this side + if let Some(mut backend_data) = self.backend_data.backends.remove(&node) { + if let Some(mut leasing_global) = backend_data.leasing_global.take() { + leasing_global.disable_global::>(); + } + + if let Some(render_node) = backend_data.render_node { + self.backend_data.gpus.as_mut().remove_node(&render_node); + } + + self.handle.remove(backend_data.registration_token); + + debug!("Dropping device"); + } + + crate::shell::fixup_positions(&mut self.space, self.pointer.current_location()); + } + + fn frame_finish(&mut self, dev_id: DrmNode, crtc: crtc::Handle, metadata: &mut Option) { + profiling::scope!("frame_finish", &format!("{crtc:?}")); + + let device_backend = match self.backend_data.backends.get_mut(&dev_id) { + Some(backend) => backend, + None => { + error!("Trying to finish frame on non-existent backend {}", dev_id); + return; + } + }; + + let surface = match device_backend.surfaces.get_mut(&crtc) { + Some(surface) => surface, + None => { + error!("Trying to finish frame on non-existent crtc {:?}", crtc); + return; + } + }; + + if let Some(timer_token) = surface.vblank_throttle_timer.take() { + self.handle.remove(timer_token); + } + + let output = if let Some(output) = self.space.outputs().find(|o| { + o.user_data().get::() + == Some(&UdevOutputId { + device_id: surface.device_id, + crtc, + }) + }) { + output.clone() + } else { + // somehow we got called with an invalid output + return; + }; + + let Some(frame_duration) = output + .current_mode() + .map(|mode| Duration::from_secs_f64(1_000f64 / mode.refresh as f64)) + else { + return; + }; + + let tp = metadata.as_ref().and_then(|metadata| match metadata.time { + smithay::backend::drm::DrmEventTime::Monotonic(tp) => tp.is_zero().not().then_some(tp), + smithay::backend::drm::DrmEventTime::Realtime(_) => None, + }); + + let seq = metadata.as_ref().map(|metadata| metadata.sequence).unwrap_or(0); + + let (clock, flags) = if let Some(tp) = tp { + ( + tp.into(), + wp_presentation_feedback::Kind::Vsync + | wp_presentation_feedback::Kind::HwClock + | wp_presentation_feedback::Kind::HwCompletion, + ) + } else { + (self.clock.now(), wp_presentation_feedback::Kind::Vsync) + }; + + let vblank_remaining_time = surface.last_presentation_time.map(|last_presentation_time| { + frame_duration.saturating_sub(Time::elapsed(&last_presentation_time, clock)) + }); + + if let Some(vblank_remaining_time) = vblank_remaining_time { + if vblank_remaining_time > frame_duration / 2 { + static WARN_ONCE: Once = Once::new(); + WARN_ONCE.call_once(|| { + warn!("display running faster than expected, throttling vblanks and disabling HwClock") + }); + let throttled_time = tp + .map(|tp| tp.saturating_add(vblank_remaining_time)) + .unwrap_or(Duration::ZERO); + let throttled_metadata = DrmEventMetadata { + sequence: seq, + time: DrmEventTime::Monotonic(throttled_time), + }; + let timer_token = self + .handle + .insert_source(Timer::from_duration(vblank_remaining_time), move |_, _, data| { + data.frame_finish(dev_id, crtc, &mut Some(throttled_metadata)); + TimeoutAction::Drop + }) + .expect("failed to register vblank throttle timer"); + surface.vblank_throttle_timer = Some(timer_token); + return; + } + } + surface.last_presentation_time = Some(clock); + + let submit_result = surface + .drm_output + .frame_submitted() + .map_err(Into::::into); + + let schedule_render = match submit_result { + Ok(user_data) => { + if let Some(mut feedback) = user_data.flatten() { + feedback.presented(clock, Refresh::fixed(frame_duration), seq as u64, flags); + } + + true + } + Err(err) => { + warn!("Error during rendering: {:?}", err); + match err { + SwapBuffersError::AlreadySwapped => true, + // If the device has been deactivated do not reschedule, this will be done + // by session resume + SwapBuffersError::TemporaryFailure(err) + if matches!(err.downcast_ref::(), Some(&DrmError::DeviceInactive)) => + { + false + } + SwapBuffersError::TemporaryFailure(err) => matches!( + err.downcast_ref::(), + Some(DrmError::Access(DrmAccessError { + source, + .. + })) if source.kind() == io::ErrorKind::PermissionDenied + ), + SwapBuffersError::ContextLost(err) => panic!("Rendering loop lost: {err}"), + } + } + }; + + if schedule_render { + let next_frame_target = clock + frame_duration; + + // What are we trying to solve by introducing a delay here: + // + // Basically it is all about latency of client provided buffers. + // A client driven by frame callbacks will wait for a frame callback + // to repaint and submit a new buffer. As we send frame callbacks + // as part of the repaint in the compositor the latency would always + // be approx. 2 frames. By introducing a delay before we repaint in + // the compositor we can reduce the latency to approx. 1 frame + the + // remaining duration from the repaint to the next VBlank. + // + // With the delay it is also possible to further reduce latency if + // the client is driven by presentation feedback. As the presentation + // feedback is directly sent after a VBlank the client can submit a + // new buffer during the repaint delay that can hit the very next + // VBlank, thus reducing the potential latency to below one frame. + // + // Choosing a good delay is a topic on its own so we just implement + // a simple strategy here. We just split the duration between two + // VBlanks into two steps, one for the client repaint and one for the + // compositor repaint. Theoretically the repaint in the compositor should + // be faster so we give the client a bit more time to repaint. On a typical + // modern system the repaint in the compositor should not take more than 2ms + // so this should be safe for refresh rates up to at least 120 Hz. For 120 Hz + // this results in approx. 3.33ms time for repainting in the compositor. + // A too big delay could result in missing the next VBlank in the compositor. + // + // A more complete solution could work on a sliding window analyzing past repaints + // and do some prediction for the next repaint. + let repaint_delay = Duration::from_secs_f64(frame_duration.as_secs_f64() * 0.6f64); + + let timer = if surface + .render_node + .map(|render_node| render_node != self.backend_data.primary_gpu) + .unwrap_or(true) + { + // However, if we need to do a copy, that might not be enough. + // (And without actual comparison to previous frames we cannot really know.) + // So lets ignore that in those cases to avoid thrashing performance. + trace!("scheduling repaint timer immediately on {:?}", crtc); + Timer::immediate() + } else { + trace!( + "scheduling repaint timer with delay {:?} on {:?}", + repaint_delay, crtc + ); + Timer::from_duration(repaint_delay) + }; + + self.handle + .insert_source(timer, move |_, _, data| { + data.render(dev_id, Some(crtc), next_frame_target); + TimeoutAction::Drop + }) + .expect("failed to schedule frame timer"); + } + } + + // If crtc is `Some()`, render it, else render all crtcs + fn render(&mut self, node: DrmNode, crtc: Option, frame_target: Time) { + let device_backend = match self.backend_data.backends.get_mut(&node) { + Some(backend) => backend, + None => { + error!("Trying to render on non-existent backend {}", node); + return; + } + }; + + if let Some(crtc) = crtc { + self.render_surface(node, crtc, frame_target); + } else { + let crtcs: Vec<_> = device_backend.surfaces.keys().copied().collect(); + for crtc in crtcs { + self.render_surface(node, crtc, frame_target); + } + }; + } + + fn render_surface(&mut self, node: DrmNode, crtc: crtc::Handle, frame_target: Time) { + profiling::scope!("render_surface", &format!("{crtc:?}")); + + let output = if let Some(output) = self.space.outputs().find(|o| { + o.user_data().get::() + == Some(&UdevOutputId { + device_id: node, + crtc, + }) + }) { + output.clone() + } else { + // somehow we got called with an invalid output + return; + }; + + self.pre_repaint(&output, frame_target); + + let device = if let Some(device) = self.backend_data.backends.get_mut(&node) { + device + } else { + return; + }; + + let surface = if let Some(surface) = device.surfaces.get_mut(&crtc) { + surface + } else { + return; + }; + + let start = Instant::now(); + + // TODO get scale from the rendersurface when supporting HiDPI + let frame = self + .backend_data + .pointer_image + .get_image(1 /*scale*/, self.clock.now().into()); + + let primary_gpu = self.backend_data.primary_gpu; + let render_node = surface.render_node.unwrap_or(primary_gpu); + let mut renderer = if primary_gpu == render_node { + self.backend_data.gpus.single_renderer(&render_node) + } else { + let format = surface.drm_output.format(); + self.backend_data + .gpus + .renderer(&primary_gpu, &render_node, format) + } + .unwrap(); + + let pointer_images = &mut self.backend_data.pointer_images; + let pointer_image = pointer_images + .iter() + .find_map(|(image, texture)| { + if image == &frame { + Some(texture.clone()) + } else { + None + } + }) + .unwrap_or_else(|| { + let buffer = MemoryRenderBuffer::from_slice( + &frame.pixels_rgba, + Fourcc::Argb8888, + (frame.width as i32, frame.height as i32), + 1, + Transform::Normal, + None, + ); + pointer_images.push((frame, buffer.clone())); + buffer + }); + + let result = render_surface( + surface, + &mut renderer, + &self.space, + &output, + self.pointer.current_location(), + &pointer_image, + &mut self.backend_data.pointer_element, + &self.dnd_icon, + &mut self.cursor_status, + self.show_window_preview, + ); + let reschedule = match result { + Ok((has_rendered, states)) => { + let dmabuf_feedback = surface.dmabuf_feedback.clone(); + self.post_repaint(&output, frame_target, dmabuf_feedback, &states); + !has_rendered + } + Err(err) => { + warn!("Error during rendering: {:#?}", err); + match err { + SwapBuffersError::AlreadySwapped => false, + SwapBuffersError::TemporaryFailure(err) => match err.downcast_ref::() { + Some(DrmError::DeviceInactive) => true, + Some(DrmError::Access(DrmAccessError { source, .. })) => { + source.kind() == io::ErrorKind::PermissionDenied + } + _ => false, + }, + SwapBuffersError::ContextLost(err) => match err.downcast_ref::() { + Some(DrmError::TestFailed(_)) => { + // reset the complete state, disabling all connectors and planes in case we hit a test failed + // most likely we hit this after a tty switch when a foreign master changed CRTC <-> connector bindings + // and we run in a mismatch + device + .drm_output_manager + .device_mut() + .reset_state() + .expect("failed to reset drm device"); + true + } + _ => panic!("Rendering loop lost: {err}"), + }, + } + } + }; + + if reschedule { + let output_refresh = match output.current_mode() { + Some(mode) => mode.refresh, + None => return, + }; + + // If reschedule is true we either hit a temporary failure or more likely rendering + // did not cause any damage on the output. In this case we just re-schedule a repaint + // after approx. one frame to re-test for damage. + let next_frame_target = frame_target + Duration::from_millis(1_000_000 / output_refresh as u64); + let reschedule_timeout = + Duration::from(next_frame_target).saturating_sub(self.clock.now().into()); + trace!( + "reschedule repaint timer with delay {:?} on {:?}", + reschedule_timeout, crtc, + ); + let timer = Timer::from_duration(reschedule_timeout); + self.handle + .insert_source(timer, move |_, _, data| { + data.render(node, Some(crtc), next_frame_target); + TimeoutAction::Drop + }) + .expect("failed to schedule frame timer"); + } else { + let elapsed = start.elapsed(); + tracing::trace!(?elapsed, "rendered surface"); + } + + profiling::finish_frame!(); + } +} + +#[allow(clippy::too_many_arguments)] +#[profiling::function] +fn render_surface<'a>( + surface: &'a mut SurfaceData, + renderer: &mut UdevRenderer<'a>, + space: &Space, + output: &Output, + pointer_location: Point, + pointer_image: &MemoryRenderBuffer, + pointer_element: &mut PointerElement, + dnd_icon: &Option, + cursor_status: &mut CursorImageStatus, + show_window_preview: bool, +) -> Result<(bool, RenderElementStates), SwapBuffersError> { + let output_geometry = space.output_geometry(output).unwrap(); + let scale = Scale::from(output.current_scale().fractional_scale()); + + let mut custom_elements: Vec> = Vec::new(); + + if output_geometry.to_f64().contains(pointer_location) { + let cursor_hotspot = if let CursorImageStatus::Surface(surface) = cursor_status { + compositor::with_states(surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot + }) + } else { + (0, 0).into() + }; + let cursor_pos = pointer_location - output_geometry.loc.to_f64(); + + // set cursor + pointer_element.set_buffer(pointer_image.clone()); + + // draw the cursor as relevant + { + // reset the cursor if the surface is no longer alive + let mut reset = false; + if let CursorImageStatus::Surface(ref surface) = *cursor_status { + reset = !surface.alive(); + } + if reset { + *cursor_status = CursorImageStatus::default_named(); + } + + pointer_element.set_status(cursor_status.clone()); + } + + custom_elements.extend( + pointer_element.render_elements( + renderer, + (cursor_pos - cursor_hotspot.to_f64()) + .to_physical(scale) + .to_i32_round(), + scale, + 1.0, + ), + ); + + // draw the dnd icon if applicable + { + if let Some(icon) = dnd_icon.as_ref() { + let dnd_icon_pos = (cursor_pos + icon.offset.to_f64()) + .to_physical(scale) + .to_i32_round(); + if icon.surface.alive() { + custom_elements.extend(AsRenderElements::>::render_elements( + &SurfaceTree::from_surface(&icon.surface), + renderer, + dnd_icon_pos, + scale, + 1.0, + )); + } + } + } + } + + #[cfg(feature = "debug")] + if let Some(element) = surface.fps_element.as_mut() { + element.update_fps(surface.fps.avg().round() as u32); + surface.fps.tick(); + custom_elements.push(CustomRenderElements::Fps(element.clone())); + } + + let (elements, clear_color) = + output_elements(output, space, custom_elements, renderer, show_window_preview); + + let frame_mode = if surface.disable_direct_scanout { + FrameFlags::empty() + } else { + FrameFlags::DEFAULT + }; + let (rendered, states) = surface + .drm_output + .render_frame(renderer, &elements, clear_color, frame_mode) + .map(|render_frame_result| { + #[cfg(feature = "renderer_sync")] + if let PrimaryPlaneElement::Swapchain(element) = render_frame_result.primary_element { + element.sync.wait(); + } + (!render_frame_result.is_empty, render_frame_result.states) + }) + .map_err(|err| match err { + smithay::backend::drm::compositor::RenderFrameError::PrepareFrame(err) => { + SwapBuffersError::from(err) + } + smithay::backend::drm::compositor::RenderFrameError::RenderFrame( + OutputDamageTrackerError::Rendering(err), + ) => SwapBuffersError::from(err), + _ => unreachable!(), + })?; + + update_primary_scanout_output(space, output, dnd_icon, cursor_status, &states); + + if rendered { + let output_presentation_feedback = take_presentation_feedback(output, space, &states); + surface + .drm_output + .queue_frame(Some(output_presentation_feedback)) + .map_err(Into::::into)?; + } + + Ok((rendered, states)) +} diff --git a/vendor/smithay/anvil/src/winit.rs b/vendor/smithay/anvil/src/winit.rs new file mode 100644 index 0000000..20218ce --- /dev/null +++ b/vendor/smithay/anvil/src/winit.rs @@ -0,0 +1,463 @@ +use std::{ + sync::{Mutex, atomic::Ordering}, + time::Duration, +}; + +#[cfg(feature = "egl")] +use smithay::backend::renderer::ImportEgl; +#[cfg(feature = "debug")] +use smithay::{ + backend::{allocator::Fourcc, renderer::ImportMem}, + reexports::winit::raw_window_handle::{HasWindowHandle, RawWindowHandle}, +}; + +use smithay::{ + backend::{ + SwapBuffersError, + allocator::dmabuf::Dmabuf, + egl::EGLDevice, + renderer::{ + ImportDma, ImportMemWl, + damage::{Error as OutputDamageTrackerError, OutputDamageTracker}, + element::AsRenderElements, + gles::GlesRenderer, + }, + winit::{self, WinitEvent, WinitGraphicsBackend}, + }, + input::{ + keyboard::LedState, + pointer::{CursorImageAttributes, CursorImageStatus}, + }, + output::{Mode, Output, PhysicalProperties, Subpixel}, + reexports::{ + calloop::EventLoop, + wayland_protocols::wp::presentation_time::server::wp_presentation_feedback, + wayland_server::{Display, protocol::wl_surface}, + winit::event_loop::pump_events::PumpStatus, + }, + utils::{IsAlive, Scale, Transform}, + wayland::{ + compositor, + dmabuf::{ + DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, + }, + presentation::Refresh, + }, +}; +use tracing::{error, info, warn}; + +use crate::state::{AnvilState, Backend, take_presentation_feedback, update_primary_scanout_output}; +use crate::{drawing::*, render::*}; + +pub const OUTPUT_NAME: &str = "winit"; + +pub struct WinitData { + backend: WinitGraphicsBackend, + damage_tracker: OutputDamageTracker, + dmabuf_state: (DmabufState, DmabufGlobal, Option), + full_redraw: u8, + #[cfg(feature = "debug")] + pub fps: fps_ticker::Fps, +} + +impl DmabufHandler for AnvilState { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.backend_data.dmabuf_state.0 + } + + fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) { + if self + .backend_data + .backend + .renderer() + .import_dmabuf(&dmabuf, None) + .is_ok() + { + let _ = notifier.successful::>(); + } else { + notifier.failed(); + } + } +} + +impl Backend for WinitData { + const HAS_GESTURES: bool = true; + + fn seat_name(&self) -> String { + String::from("winit") + } + fn reset_buffers(&mut self, _output: &Output) { + self.full_redraw = 4; + } + fn early_import(&mut self, _surface: &wl_surface::WlSurface) {} + fn update_led_state(&mut self, _led_state: LedState) {} +} + +pub fn run_winit() { + let mut event_loop = EventLoop::try_new().unwrap(); + let display = Display::new().unwrap(); + let mut display_handle = display.handle(); + + #[cfg_attr(not(feature = "egl"), allow(unused_mut))] + let (mut backend, mut winit) = match winit::init::() { + Ok(ret) => ret, + Err(err) => { + error!("Failed to initialize Winit backend: {}", err); + return; + } + }; + let size = backend.window_size(); + + let mode = Mode { + size, + refresh: 60_000, + }; + let output = Output::new( + OUTPUT_NAME.to_string(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "Smithay".into(), + model: "Winit".into(), + serial_number: "Unknown".into(), + }, + ); + let _global = output.create_global::>(&display.handle()); + output.change_current_state(Some(mode), Some(Transform::Flipped180), None, Some((0, 0).into())); + output.set_preferred(mode); + + #[cfg(feature = "debug")] + #[allow(deprecated)] + let fps_image = + image::io::Reader::with_format(std::io::Cursor::new(FPS_NUMBERS_PNG), image::ImageFormat::Png) + .decode() + .unwrap(); + #[cfg(feature = "debug")] + let fps_texture = backend + .renderer() + .import_memory( + &fps_image.to_rgba8(), + Fourcc::Abgr8888, + (fps_image.width() as i32, fps_image.height() as i32).into(), + false, + ) + .expect("Unable to upload FPS texture"); + #[cfg(feature = "debug")] + let mut fps_element = FpsElement::new(fps_texture); + + let render_node = EGLDevice::device_for_display(backend.renderer().egl_context().display()) + .and_then(|device| device.try_get_render_node()); + + let dmabuf_default_feedback = match render_node { + Ok(Some(node)) => { + let dmabuf_formats = backend.renderer().dmabuf_formats(); + let dmabuf_default_feedback = DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats) + .build() + .unwrap(); + Some(dmabuf_default_feedback) + } + Ok(None) => { + warn!("failed to query render node, dmabuf will use v3"); + None + } + Err(err) => { + warn!(?err, "failed to egl device for display, dmabuf will use v3"); + None + } + }; + + // if we failed to build dmabuf feedback we fall back to dmabuf v3 + // Note: egl on Mesa requires either v4 or wl_drm (initialized with bind_wl_display) + let dmabuf_state = if let Some(default_feedback) = dmabuf_default_feedback { + let mut dmabuf_state = DmabufState::new(); + let dmabuf_global = dmabuf_state.create_global_with_default_feedback::>( + &display.handle(), + &default_feedback, + ); + (dmabuf_state, dmabuf_global, Some(default_feedback)) + } else { + let dmabuf_formats = backend.renderer().dmabuf_formats(); + let mut dmabuf_state = DmabufState::new(); + let dmabuf_global = + dmabuf_state.create_global::>(&display.handle(), dmabuf_formats); + (dmabuf_state, dmabuf_global, None) + }; + + #[cfg(feature = "egl")] + if backend.renderer().bind_wl_display(&display.handle()).is_ok() { + info!("EGL hardware-acceleration enabled"); + }; + + let data = { + let damage_tracker = OutputDamageTracker::from_output(&output); + + WinitData { + backend, + damage_tracker, + dmabuf_state, + full_redraw: 0, + #[cfg(feature = "debug")] + fps: fps_ticker::Fps::default(), + } + }; + let mut state = AnvilState::init(display, event_loop.handle(), data, true); + state + .shm_state + .update_formats(state.backend_data.backend.renderer().shm_formats()); + state.space.map_output(&output, (0, 0)); + + #[cfg(feature = "xwayland")] + state.start_xwayland(); + + info!("Initialization completed, starting the main loop."); + + let mut pointer_element = PointerElement::default(); + + while state.running.load(Ordering::SeqCst) { + let status = winit.dispatch_new_events(|event| match event { + WinitEvent::Resized { size, .. } => { + // We only have one output + let output = state.space.outputs().next().unwrap().clone(); + state.space.map_output(&output, (0, 0)); + let mode = Mode { + size, + refresh: 60_000, + }; + output.change_current_state(Some(mode), None, None, None); + output.set_preferred(mode); + crate::shell::fixup_positions(&mut state.space, state.pointer.current_location()); + } + WinitEvent::Input(event) => state.process_input_event_windowed(event, OUTPUT_NAME), + _ => (), + }); + + if let PumpStatus::Exit(_) = status { + state.running.store(false, Ordering::SeqCst); + break; + } + + // drawing logic + { + let now = state.clock.now(); + let frame_target = now + + output + .current_mode() + .map(|mode| Duration::from_secs_f64(1_000f64 / mode.refresh as f64)) + .unwrap_or_default(); + state.pre_repaint(&output, frame_target); + + let backend = &mut state.backend_data.backend; + + // draw the cursor as relevant + // reset the cursor if the surface is no longer alive + let mut reset = false; + if let CursorImageStatus::Surface(ref surface) = state.cursor_status { + reset = !surface.alive(); + } + if reset { + state.cursor_status = CursorImageStatus::default_named(); + } + let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_)); + + pointer_element.set_status(state.cursor_status.clone()); + + #[cfg(feature = "debug")] + let fps = state.backend_data.fps.avg().round() as u32; + #[cfg(feature = "debug")] + fps_element.update_fps(fps); + + let full_redraw = &mut state.backend_data.full_redraw; + *full_redraw = full_redraw.saturating_sub(1); + let space = &mut state.space; + let damage_tracker = &mut state.backend_data.damage_tracker; + let show_window_preview = state.show_window_preview; + + let dnd_icon = state.dnd_icon.as_ref(); + + let scale = Scale::from(output.current_scale().fractional_scale()); + let cursor_hotspot = if let CursorImageStatus::Surface(ref surface) = state.cursor_status { + compositor::with_states(surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot + }) + } else { + (0, 0).into() + }; + let cursor_pos = state.pointer.current_location(); + + #[cfg(feature = "debug")] + let mut renderdoc = state.renderdoc.as_mut(); + + let age = if *full_redraw > 0 { + 0 + } else { + backend.buffer_age().unwrap_or(0) + }; + #[cfg(feature = "debug")] + let window_handle = backend + .window() + .window_handle() + .map(|handle| { + if let RawWindowHandle::Wayland(handle) = handle.as_raw() { + handle.surface.as_ptr() + } else { + std::ptr::null_mut() + } + }) + .unwrap_or_else(|_| std::ptr::null_mut()); + let render_res = backend.bind().and_then(|(renderer, mut fb)| { + #[cfg(feature = "debug")] + if let Some(renderdoc) = renderdoc.as_mut() { + renderdoc.start_frame_capture(renderer.egl_context().get_context_handle(), window_handle); + } + + let mut elements = Vec::>::new(); + + elements.extend( + pointer_element.render_elements( + renderer, + (cursor_pos - cursor_hotspot.to_f64()) + .to_physical(scale) + .to_i32_round(), + scale, + 1.0, + ), + ); + + // draw the dnd icon if any + if let Some(icon) = dnd_icon { + let dnd_icon_pos = (cursor_pos + icon.offset.to_f64()) + .to_physical(scale) + .to_i32_round(); + if icon.surface.alive() { + elements.extend(AsRenderElements::::render_elements( + &smithay::desktop::space::SurfaceTree::from_surface(&icon.surface), + renderer, + dnd_icon_pos, + scale, + 1.0, + )); + } + } + + #[cfg(feature = "debug")] + elements.push(CustomRenderElements::Fps(fps_element.clone())); + + render_output( + &output, + space, + elements, + renderer, + &mut fb, + damage_tracker, + age, + show_window_preview, + ) + .map_err(|err| match err { + OutputDamageTrackerError::Rendering(err) => err.into(), + _ => unreachable!(), + }) + }); + + match render_res { + Ok(render_output_result) => { + let has_rendered = render_output_result.damage.is_some(); + if let Some(damage) = render_output_result.damage { + if let Err(err) = backend.submit(Some(damage)) { + warn!("Failed to submit buffer: {}", err); + } + } + + #[cfg(feature = "debug")] + if let Some(renderdoc) = renderdoc.as_mut() { + renderdoc.end_frame_capture( + backend.renderer().egl_context().get_context_handle(), + backend + .window() + .window_handle() + .map(|handle| { + if let RawWindowHandle::Wayland(handle) = handle.as_raw() { + handle.surface.as_ptr() + } else { + std::ptr::null_mut() + } + }) + .unwrap_or_else(|_| std::ptr::null_mut()), + ); + } + + backend.window().set_cursor_visible(cursor_visible); + + let states = render_output_result.states; + + update_primary_scanout_output( + &state.space, + &output, + &state.dnd_icon, + &state.cursor_status, + &states, + ); + + if has_rendered { + let mut output_presentation_feedback = + take_presentation_feedback(&output, &state.space, &states); + output_presentation_feedback.presented( + frame_target, + output + .current_mode() + .map(|mode| { + Refresh::fixed(Duration::from_secs_f64(1_000f64 / mode.refresh as f64)) + }) + .unwrap_or(Refresh::Unknown), + 0, + wp_presentation_feedback::Kind::Vsync, + ) + } + + // Send frame events so that client start drawing their next frame + state.post_repaint(&output, frame_target, None, &states); + } + Err(SwapBuffersError::ContextLost(err)) => { + #[cfg(feature = "debug")] + if let Some(renderdoc) = renderdoc.as_mut() { + renderdoc.discard_frame_capture( + backend.renderer().egl_context().get_context_handle(), + backend + .window() + .window_handle() + .map(|handle| { + if let RawWindowHandle::Wayland(handle) = handle.as_raw() { + handle.surface.as_ptr() + } else { + std::ptr::null_mut() + } + }) + .unwrap_or_else(|_| std::ptr::null_mut()), + ); + } + + error!("Critical Rendering Error: {}", err); + state.running.store(false, Ordering::SeqCst); + } + Err(err) => warn!("Rendering error: {}", err), + } + } + + let result = event_loop.dispatch(Some(Duration::from_millis(1)), &mut state); + if result.is_err() { + state.running.store(false, Ordering::SeqCst); + } else { + state.space.refresh(); + state.popups.cleanup(); + display_handle.flush_clients().unwrap(); + } + + #[cfg(feature = "debug")] + state.backend_data.fps.tick(); + } +} diff --git a/vendor/smithay/anvil/src/x11.rs b/vendor/smithay/anvil/src/x11.rs new file mode 100644 index 0000000..6e0fee7 --- /dev/null +++ b/vendor/smithay/anvil/src/x11.rs @@ -0,0 +1,494 @@ +use std::{ + sync::{Mutex, atomic::Ordering}, + time::Duration, +}; + +use crate::{ + drawing::*, + render::*, + state::{AnvilState, Backend, take_presentation_feedback, update_primary_scanout_output}, +}; +#[cfg(feature = "egl")] +use smithay::backend::renderer::ImportEgl; +#[cfg(feature = "debug")] +use smithay::backend::{allocator::Fourcc, renderer::ImportMem}; + +use smithay::{ + backend::{ + allocator::{ + dmabuf::{Dmabuf, DmabufAllocator}, + gbm::{GbmAllocator, GbmBufferFlags}, + vulkan::{ImageUsageFlags, VulkanAllocator}, + }, + egl::{EGLContext, EGLDisplay}, + renderer::{ + Bind, ImportDma, ImportMemWl, damage::OutputDamageTracker, element::AsRenderElements, + gles::GlesRenderer, + }, + vulkan::{Instance, PhysicalDevice, version::Version}, + x11::{WindowBuilder, X11Backend, X11Event, X11Surface}, + }, + input::{ + keyboard::LedState, + pointer::{CursorImageAttributes, CursorImageStatus}, + }, + output::{Mode, Output, PhysicalProperties, Subpixel}, + reexports::{ + ash::ext, + calloop::EventLoop, + gbm, + wayland_protocols::wp::presentation_time::server::wp_presentation_feedback, + wayland_server::{Display, protocol::wl_surface}, + }, + utils::{DeviceFd, IsAlive, Scale}, + wayland::{ + compositor, + dmabuf::{ + DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, + }, + presentation::Refresh, + }, +}; +use tracing::{error, info, trace, warn}; + +pub const OUTPUT_NAME: &str = "x11"; + +#[derive(Debug)] +pub struct X11Data { + render: bool, + mode: Mode, + // FIXME: If GlesRenderer is dropped before X11Surface, then the MakeCurrent call inside Gles2Renderer will + // fail because the X11Surface is keeping gbm alive. + renderer: GlesRenderer, + damage_tracker: OutputDamageTracker, + surface: X11Surface, + dmabuf_state: DmabufState, + _dmabuf_global: DmabufGlobal, + _dmabuf_default_feedback: DmabufFeedback, + #[cfg(feature = "debug")] + fps: fps_ticker::Fps, +} + +impl DmabufHandler for AnvilState { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.backend_data.dmabuf_state + } + + fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) { + if self.backend_data.renderer.import_dmabuf(&dmabuf, None).is_ok() { + let _ = notifier.successful::>(); + } else { + notifier.failed(); + } + } +} + +impl Backend for X11Data { + fn seat_name(&self) -> String { + "x11".to_owned() + } + fn reset_buffers(&mut self, _output: &Output) { + self.surface.reset_buffers(); + } + fn early_import(&mut self, _surface: &wl_surface::WlSurface) {} + fn update_led_state(&mut self, _led_state: LedState) {} +} + +pub fn run_x11() { + let mut event_loop = EventLoop::try_new().unwrap(); + let display = Display::new().unwrap(); + let mut display_handle = display.handle(); + + let backend = X11Backend::new().expect("Failed to initialize X11 backend"); + let handle = backend.handle(); + + // Obtain the DRM node the X server uses for direct rendering. + let (node, fd) = handle + .drm_node() + .expect("Could not get DRM node used by X server"); + + // Create the gbm device for buffer allocation. + let device = gbm::Device::new(DeviceFd::from(fd)).expect("Failed to create gbm device"); + // Initialize EGL using the GBM device. + let egl = unsafe { EGLDisplay::new(device.clone()).expect("Failed to create EGLDisplay") }; + // Create the OpenGL context + let context = EGLContext::new(&egl).expect("Failed to create EGLContext"); + + let window = WindowBuilder::new() + .title("Anvil") + .build(&handle) + .expect("Failed to create first window"); + + let skip_vulkan = std::env::var("ANVIL_NO_VULKAN") + .map(|x| { + x == "1" || x.to_lowercase() == "true" || x.to_lowercase() == "yes" || x.to_lowercase() == "y" + }) + .unwrap_or(false); + + let vulkan_allocator = if !skip_vulkan { + Instance::new(Version::VERSION_1_2, None) + .ok() + .and_then(|instance| { + PhysicalDevice::enumerate(&instance).ok().and_then(|devices| { + devices + .filter(|phd| phd.has_device_extension(ext::physical_device_drm::NAME)) + .find(|phd| { + phd.primary_node().unwrap() == Some(node) + || phd.render_node().unwrap() == Some(node) + }) + }) + }) + .and_then(|physical_device| { + VulkanAllocator::new( + &physical_device, + ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED, + ) + .ok() + }) + } else { + None + }; + + let surface = match vulkan_allocator { + // Create the surface for the window. + Some(vulkan_allocator) => handle + .create_surface( + &window, + DmabufAllocator(vulkan_allocator), + context + .dmabuf_render_formats() + .iter() + .map(|format| format.modifier), + ) + .expect("Failed to create X11 surface"), + None => handle + .create_surface( + &window, + DmabufAllocator(GbmAllocator::new(device, GbmBufferFlags::RENDERING)), + context + .dmabuf_render_formats() + .iter() + .map(|format| format.modifier), + ) + .expect("Failed to create X11 surface"), + }; + + #[cfg_attr(not(feature = "egl"), allow(unused_mut))] + let mut renderer = unsafe { GlesRenderer::new(context) }.expect("Failed to initialize renderer"); + + #[cfg(feature = "egl")] + if renderer.bind_wl_display(&display.handle()).is_ok() { + info!("EGL hardware-acceleration enabled"); + } + + let dmabuf_formats = renderer.dmabuf_formats(); + let dmabuf_default_feedback = DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats) + .build() + .unwrap(); + let mut dmabuf_state = DmabufState::new(); + let dmabuf_global = dmabuf_state.create_global_with_default_feedback::>( + &display.handle(), + &dmabuf_default_feedback, + ); + + let size = { + let s = window.size(); + + (s.w as i32, s.h as i32).into() + }; + + let mode = Mode { + size, + refresh: 60_000, + }; + + #[cfg(feature = "debug")] + #[allow(deprecated)] + let fps_image = + image::io::Reader::with_format(std::io::Cursor::new(FPS_NUMBERS_PNG), image::ImageFormat::Png) + .decode() + .unwrap(); + #[cfg(feature = "debug")] + let fps_texture = renderer + .import_memory( + &fps_image.to_rgba8(), + Fourcc::Abgr8888, + (fps_image.width() as i32, fps_image.height() as i32).into(), + false, + ) + .expect("Unable to upload FPS texture"); + #[cfg(feature = "debug")] + let mut fps_element = FpsElement::new(fps_texture); + let output = Output::new( + OUTPUT_NAME.to_string(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "Smithay".into(), + model: "X11".into(), + serial_number: "Unknown".into(), + }, + ); + let _global = output.create_global::>(&display.handle()); + output.change_current_state(Some(mode), None, None, Some((0, 0).into())); + output.set_preferred(mode); + + let damage_tracker = OutputDamageTracker::from_output(&output); + + let data = X11Data { + render: true, + mode, + surface, + renderer, + damage_tracker, + dmabuf_state, + _dmabuf_global: dmabuf_global, + _dmabuf_default_feedback: dmabuf_default_feedback, + #[cfg(feature = "debug")] + fps: fps_ticker::Fps::default(), + }; + + let mut state = AnvilState::init(display, event_loop.handle(), data, true); + state + .shm_state + .update_formats(state.backend_data.renderer.shm_formats()); + state.space.map_output(&output, (0, 0)); + + let output_clone = output.clone(); + event_loop + .handle() + .insert_source(backend, move |event, _, data| match event { + X11Event::CloseRequested { .. } => { + data.running.store(false, Ordering::SeqCst); + } + X11Event::Resized { new_size, .. } => { + let output = &output_clone; + let size = { (new_size.w as i32, new_size.h as i32).into() }; + + data.backend_data.mode = Mode { + size, + refresh: 60_000, + }; + output.delete_mode(output.current_mode().unwrap()); + output.change_current_state(Some(data.backend_data.mode), None, None, None); + output.set_preferred(data.backend_data.mode); + crate::shell::fixup_positions(&mut data.space, data.pointer.current_location()); + + data.backend_data.render = true; + } + X11Event::PresentCompleted { .. } | X11Event::Refresh { .. } => { + data.backend_data.render = true; + } + X11Event::Input { event, .. } => data.process_input_event_windowed(event, OUTPUT_NAME), + X11Event::Focus { focused: false, .. } => { + data.release_all_keys(); + } + _ => {} + }) + .expect("Failed to insert X11 Backend into event loop"); + + #[cfg(feature = "xwayland")] + state.start_xwayland(); + + info!("Initialization completed, starting the main loop."); + + let mut pointer_element = PointerElement::default(); + + while state.running.load(Ordering::SeqCst) { + if state.backend_data.render { + profiling::scope!("render_frame"); + + let now = state.clock.now(); + let frame_target = now + + output + .current_mode() + .map(|mode| Duration::from_secs_f64(1_000f64 / mode.refresh as f64)) + .unwrap_or_default(); + state.pre_repaint(&output, frame_target); + + let backend_data = &mut state.backend_data; + // We need to borrow everything we want to refer to inside the renderer callback otherwise rustc is unhappy. + #[cfg(feature = "debug")] + let fps = backend_data.fps.avg().round() as u32; + #[cfg(feature = "debug")] + fps_element.update_fps(fps); + + let (mut buffer, age) = backend_data.surface.buffer().expect("gbm device was destroyed"); + let mut fb = match backend_data.renderer.bind(&mut buffer) { + Ok(fb) => fb, + Err(err) => { + error!("Error while binding buffer: {}", err); + profiling::finish_frame!(); + continue; + } + }; + + #[cfg(feature = "debug")] + if let Some(renderdoc) = state.renderdoc.as_mut() { + renderdoc.start_frame_capture( + backend_data.renderer.egl_context().get_context_handle(), + std::ptr::null(), + ); + } + + let mut elements: Vec> = Vec::new(); + + // draw the cursor as relevant + // reset the cursor if the surface is no longer alive + let mut reset = false; + if let CursorImageStatus::Surface(ref surface) = state.cursor_status { + reset = !surface.alive(); + } + if reset { + state.cursor_status = CursorImageStatus::default_named(); + } + let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_)); + + let scale = Scale::from(output.current_scale().fractional_scale()); + let cursor_hotspot = if let CursorImageStatus::Surface(ref surface) = state.cursor_status { + compositor::with_states(surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot + }) + } else { + (0, 0).into() + }; + let cursor_pos = state.pointer.current_location(); + + pointer_element.set_status(state.cursor_status.clone()); + elements.extend( + pointer_element.render_elements( + &mut backend_data.renderer, + (cursor_pos - cursor_hotspot.to_f64()) + .to_physical(scale) + .to_i32_round(), + scale, + 1.0, + ), + ); + + // draw the dnd icon if any + if let Some(icon) = state.dnd_icon.as_ref() { + let dnd_icon_pos = (cursor_pos + icon.offset.to_f64()) + .to_physical(scale) + .to_i32_round(); + if icon.surface.alive() { + elements.extend(AsRenderElements::::render_elements( + &smithay::desktop::space::SurfaceTree::from_surface(&icon.surface), + &mut backend_data.renderer, + dnd_icon_pos, + scale, + 1.0, + )); + } + } + + #[cfg(feature = "debug")] + elements.push(CustomRenderElements::Fps(fps_element.clone())); + + let render_res = render_output( + &output, + &state.space, + elements, + &mut backend_data.renderer, + &mut fb, + &mut backend_data.damage_tracker, + age.into(), + state.show_window_preview, + ); + + match render_res { + Ok(render_output_result) => { + trace!("Finished rendering"); + let submitted = if let Err(err) = backend_data.surface.submit() { + backend_data.surface.reset_buffers(); + warn!("Failed to submit buffer: {}. Retrying", err); + false + } else { + true + }; + + let states = render_output_result.states; + + update_primary_scanout_output( + &state.space, + &output, + &state.dnd_icon, + &state.cursor_status, + &states, + ); + + #[cfg(feature = "debug")] + let rendered = render_output_result.damage.is_some(); + if render_output_result.damage.is_some() { + let mut output_presentation_feedback = + take_presentation_feedback(&output, &state.space, &states); + output_presentation_feedback.presented( + frame_target, + output + .current_mode() + .map(|mode| { + Refresh::fixed(Duration::from_secs_f64(1_000f64 / mode.refresh as f64)) + }) + .unwrap_or(Refresh::Unknown), + 0, + wp_presentation_feedback::Kind::Vsync, + ) + } + + #[cfg(feature = "debug")] + if rendered { + if let Some(renderdoc) = state.renderdoc.as_mut() { + renderdoc.end_frame_capture( + state.backend_data.renderer.egl_context().get_context_handle(), + std::ptr::null(), + ); + } + } else if let Some(renderdoc) = state.renderdoc.as_mut() { + renderdoc.discard_frame_capture( + state.backend_data.renderer.egl_context().get_context_handle(), + std::ptr::null(), + ); + } + + state.backend_data.render = !submitted; + + // Send frame events so that client start drawing their next frame + state.post_repaint(&output, frame_target, None, &states); + } + Err(err) => { + #[cfg(feature = "debug")] + if let Some(renderdoc) = state.renderdoc.as_mut() { + renderdoc.discard_frame_capture( + backend_data.renderer.egl_context().get_context_handle(), + std::ptr::null(), + ); + } + + backend_data.surface.reset_buffers(); + error!("Rendering error: {}", err); + // TODO: convert RenderError into SwapBuffersError and skip temporary (will retry) and panic on ContextLost or recreate + } + } + + #[cfg(feature = "debug")] + state.backend_data.fps.tick(); + window.set_cursor_visible(cursor_visible); + profiling::finish_frame!(); + } + + let result = event_loop.dispatch(Some(Duration::from_millis(16)), &mut state); + if result.is_err() { + state.running.store(false, Ordering::SeqCst); + } else { + state.space.refresh(); + state.popups.cleanup(); + display_handle.flush_clients().unwrap(); + } + } +} diff --git a/vendor/smithay/benches/benchmark.rs b/vendor/smithay/benches/benchmark.rs new file mode 100644 index 0000000..89bb00d --- /dev/null +++ b/vendor/smithay/benches/benchmark.rs @@ -0,0 +1,18 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use smithay::utils::user_data::UserDataMap; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("UserDataMap::get", |b| { + let udata_map = UserDataMap::new(); + udata_map.insert_if_missing(|| 17i32); + b.iter(|| udata_map.get::()) + }); + c.bench_function("UserDataMap::get threadsafe", |b| { + let udata_map = UserDataMap::new(); + udata_map.insert_if_missing_threadsafe(|| 17i32); + b.iter(|| udata_map.get::()) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/vendor/smithay/benches/geometry.rs b/vendor/smithay/benches/geometry.rs new file mode 100644 index 0000000..e4f686b --- /dev/null +++ b/vendor/smithay/benches/geometry.rs @@ -0,0 +1,50 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use rand::RngExt; +use smithay::utils::{Physical, Rectangle, Size}; + +fn element_visible_size(test_element: Rectangle, opaque_regions: &[Rectangle]) { + let mut workhouse = Vec::with_capacity(2048 * 4); + workhouse.push(test_element); + workhouse = Rectangle::subtract_rects_many_in_place(workhouse, opaque_regions.iter().copied()); + workhouse + .iter() + .fold(0usize, |acc, item| acc + (item.size.w * item.size.h) as usize); +} + +fn criterion_benchmark(c: &mut Criterion) { + let stage: Size = Size::from((800, 600)); + let element_size: Size = Size::from((200, 100)); + let max_x = stage.w - element_size.w; + let max_y = stage.h - element_size.h; + + let mut rand = rand::rng(); + let x = rand.random_range(0..max_x); + let y = rand.random_range(0..max_y); + let test_element = Rectangle::new((x, y).into(), element_size); + + let x_min = (test_element.loc.x - element_size.w) + 1; + let x_max = (test_element.loc.x + element_size.w) - 1; + let y_min = (test_element.loc.y - element_size.h) + 1; + let y_max = (test_element.loc.y + element_size.h) - 1; + // let x_min = 0; + // let x_max = stage.w - element_size.w; + // let y_min = 0; + // let y_max = stage.h - element_size.h; + + let opaque_regions = (0..2048) + .map(|_| { + let x = rand.random_range(x_min..=x_max); + let y = rand.random_range(y_min..=y_max); + Rectangle::new((x, y).into(), element_size) + }) + .collect::>(); + + c.bench_function("element_visible_size", |b| { + b.iter(|| { + element_visible_size(test_element, &opaque_regions); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/vendor/smithay/build.rs b/vendor/smithay/build.rs new file mode 100644 index 0000000..2c6a642 --- /dev/null +++ b/vendor/smithay/build.rs @@ -0,0 +1,139 @@ +#![allow(clippy::disallowed_macros)] +#[cfg(any(feature = "backend_egl", feature = "renderer_gl"))] +fn gl_generate() { + use gl_generator::{Api, Fallbacks, Profile, Registry}; + use std::{env, fs::File, path::PathBuf}; + + let dest = PathBuf::from(&env::var("OUT_DIR").unwrap()); + + if env::var_os("CARGO_FEATURE_BACKEND_EGL").is_some() { + let mut file = File::create(dest.join("egl_bindings.rs")).unwrap(); + Registry::new( + Api::Egl, + (1, 5), + Profile::Core, + Fallbacks::All, + [ + "EGL_KHR_create_context", + "EGL_EXT_create_context_robustness", + "EGL_KHR_create_context_no_error", + "EGL_KHR_no_config_context", + "EGL_EXT_pixel_format_float", + "EGL_EXT_device_base", + "EGL_EXT_device_enumeration", + "EGL_EXT_device_query", + "EGL_EXT_device_drm", + "EGL_EXT_device_drm_render_node", + "EGL_KHR_stream", + "EGL_KHR_stream_producer_eglsurface", + "EGL_EXT_platform_base", + "EGL_KHR_platform_x11", + "EGL_EXT_platform_x11", + "EGL_KHR_platform_wayland", + "EGL_EXT_platform_wayland", + "EGL_KHR_platform_gbm", + "EGL_MESA_platform_gbm", + "EGL_MESA_platform_surfaceless", + "EGL_EXT_platform_device", + "EGL_WL_bind_wayland_display", + "EGL_KHR_image_base", + "EGL_EXT_image_dma_buf_import", + "EGL_EXT_image_dma_buf_import_modifiers", + "EGL_MESA_image_dma_buf_export", + "EGL_KHR_gl_image", + "EGL_EXT_buffer_age", + "EGL_EXT_swap_buffers_with_damage", + "EGL_KHR_swap_buffers_with_damage", + "EGL_KHR_fence_sync", + "EGL_ANDROID_native_fence_sync", + "EGL_IMG_context_priority", + ], + ) + .write_bindings(gl_generator::GlobalGenerator, &mut file) + .unwrap(); + } + + if env::var_os("CARGO_FEATURE_RENDERER_GL").is_some() { + let mut file = File::create(dest.join("gl_bindings.rs")).unwrap(); + Registry::new( + Api::Gles2, + (3, 2), + Profile::Compatibility, + Fallbacks::None, + [ + "GL_OES_EGL_image", + "GL_OES_EGL_image_external", + "GL_EXT_texture_format_BGRA8888", + "GL_EXT_unpack_subimage", + "GL_OES_EGL_sync", + "GL_EXT_disjoint_timer_query", + ], + ) + .write_bindings(gl_generator::StructGenerator, &mut file) + .unwrap(); + } +} + +#[cfg(all(feature = "backend_gbm", not(feature = "backend_gbm_has_fd_for_plane")))] +fn test_gbm_bo_fd_for_plane() { + let gbm = match pkg_config::probe_library("gbm") { + Ok(lib) => lib, + Err(_) => { + println!("cargo:warning=failed to find gbm, assuming gbm_bo_get_fd_for_plane is unavailable"); + return; + } + }; + + let has_gbm_bo_get_fd_for_plane = cc::Build::new() + .file("test_gbm_bo_get_fd_for_plane.c") + .includes(gbm.include_paths) + .warnings_into_errors(true) + .cargo_metadata(false) + .try_compile("test_gbm_bo_get_fd_for_plane") + .is_ok(); + + if has_gbm_bo_get_fd_for_plane { + println!("cargo:rustc-cfg=feature=\"backend_gbm_has_fd_for_plane\""); + } +} + +#[cfg(all( + feature = "backend_gbm", + not(feature = "backend_gbm_has_create_with_modifiers2") +))] +fn test_gbm_bo_create_with_modifiers2() { + let gbm = match pkg_config::probe_library("gbm") { + Ok(lib) => lib, + Err(_) => { + println!( + "cargo:warning=failed to find gbm, assuming gbm_bo_create_with_modifiers2 is unavailable" + ); + return; + } + }; + + let has_gbm_bo_create_with_modifiers2 = cc::Build::new() + .file("test_gbm_bo_create_with_modifiers2.c") + .includes(gbm.include_paths) + .warnings_into_errors(true) + .cargo_metadata(false) + .try_compile("test_gbm_bo_create_with_modifiers2") + .is_ok(); + + if has_gbm_bo_create_with_modifiers2 { + println!("cargo:rustc-cfg=feature=\"backend_gbm_has_create_with_modifiers2\""); + } +} + +fn main() { + #[cfg(any(feature = "backend_egl", feature = "renderer_gl"))] + gl_generate(); + + #[cfg(all(feature = "backend_gbm", not(feature = "backend_gbm_has_fd_for_plane")))] + test_gbm_bo_fd_for_plane(); + #[cfg(all( + feature = "backend_gbm", + not(feature = "backend_gbm_has_create_with_modifiers2") + ))] + test_gbm_bo_create_with_modifiers2(); +} diff --git a/vendor/smithay/clippy.toml b/vendor/smithay/clippy.toml new file mode 100644 index 0000000..c2e3c5a --- /dev/null +++ b/vendor/smithay/clippy.toml @@ -0,0 +1,9 @@ +msrv = "1.87" +type-complexity-threshold = 400 + +disallowed-macros = [ + { path = "std::print", reason = "We use tracing for logging" }, + { path = "std::println", reason = "We use tracing for logging" }, + { path = "std::eprint", reason = "We use tracing for logging" }, + { path = "std::eprintln", reason = "We use tracing for logging" }, +] diff --git a/vendor/smithay/compile_wlcs.sh b/vendor/smithay/compile_wlcs.sh new file mode 100755 index 0000000..81760d8 --- /dev/null +++ b/vendor/smithay/compile_wlcs.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +WLCS_SHA=2b385328366de5db19521b9b3eef81861430d984 + +if [ -f "./wlcs/wlcs" ] && [ "$(cd wlcs; git rev-parse HEAD)" = "${WLCS_SHA}" ] ; then + echo "Using cached WLCS." +else + echo "Compiling WLCS." + git clone https://github.com/MirServer/wlcs.git + cd wlcs || exit + # checkout a specific revision + git reset --hard "${WLCS_SHA}" + cmake -DWLCS_BUILD_ASAN=False -DWLCS_BUILD_TSAN=False -DWLCS_BUILD_UBSAN=False -DCMAKE_EXPORT_COMPILE_COMMANDS=1 . + make +fi diff --git a/vendor/smithay/doc_index.html b/vendor/smithay/doc_index.html new file mode 100644 index 0000000..612db29 --- /dev/null +++ b/vendor/smithay/doc_index.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/vendor/smithay/examples/buffer_test.rs b/vendor/smithay/examples/buffer_test.rs new file mode 100644 index 0000000..c56cecc --- /dev/null +++ b/vendor/smithay/examples/buffer_test.rs @@ -0,0 +1,501 @@ +use std::{fmt, fs::File, os::unix::io::OwnedFd, path::PathBuf}; + +use clap::{Args, Parser, Subcommand, ValueEnum}; +use smithay::{ + backend::{ + allocator::{ + Allocator, Fourcc, Modifier, + dmabuf::{AnyError, Dmabuf, DmabufAllocator}, + dumb::DumbAllocator, + gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, + vulkan::{ImageUsageFlags, VulkanAllocator}, + }, + drm::{DrmDeviceFd, DrmNode}, + egl::{EGLContext, EGLDevice, EGLDisplay}, + renderer::{ + Bind, Color32F, ExportMem, Frame, ImportDma, Offscreen, Renderer, + gles::{GlesRenderbuffer, GlesRenderer}, + }, + vulkan::{Instance, PhysicalDevice, version::Version}, + }, + utils::{DeviceFd, Rectangle, Transform}, +}; +use tracing::info; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + subcommand: Subcommands, +} + +#[derive(Subcommand, Debug)] +enum Subcommands { + /// Intersect formats to find usable combinations + #[command(arg_required_else_help = true)] + Formats { + /// Devices that should be able to render a format + #[arg(short, long)] + render: Vec, + /// Devices that should be able to sample from a format + #[arg(short, long)] + sample: Vec, + }, + /// Test exporting and importing buffers with various apis and devices + Test(TestArgs), +} + +#[derive(Args, Debug)] +struct TestArgs { + /// Allocator used for creating the buffer + #[arg(short, long)] + allocator: AllocatorType, + /// Usage flags for allocating the image, when using the vulkan api + #[arg(short, long, value_parser = usage_flags_from_string, required_if_eq("allocator", "vulkan"))] + usage_flags: Option, + + /// Exporting device node + #[arg(short, long, required_if_eq_any([("allocator", "dumb"), ("allocator", "gbm"), ("allocator", "vulkan")]))] + export: Option, + /// Importing device node (may be equal to export) + #[arg(short, long)] + import: String, + /// Renderer used to draw into buffer before exporting + #[arg(short = 'r', long)] + export_renderer: Option, + #[arg(short = 't', long)] + import_renderer: Option, + #[arg(short, long, requires("export_renderer"), requires("import_renderer"))] + dump: Option, + + #[arg(long, default_value_t = 256)] + width: u32, + #[arg(long, default_value_t = 256)] + height: u32, + #[arg(short, long, default_value = "Abgr8888", value_parser = fourcc_from_string)] + fourcc: Fourcc, + #[arg(short, long, default_value = "Linear", value_parser = modifier_from_string)] + modifier: Modifier, +} + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub enum AllocatorType { + #[value(name = "dumb")] + DumbBuffer, + #[value(name = "gbm")] + Gbm, + #[value(name = "vulkan")] + Vulkan, +} + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub enum RendererType { + #[value(name = "gles")] + Gles, +} + +impl fmt::Display for RendererType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Gles") + } +} + +fn main() { + let args = Cli::parse(); + + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + } else { + tracing_subscriber::fmt().init(); + } + + match args.subcommand { + Subcommands::Formats { render, sample } => format_test(render, sample), + Subcommands::Test(args) => buffer_test(args), + } +} + +fn format_test(render: Vec, sample: Vec) { + for format in render + .iter() + .map(|path| { + let path = PathBuf::from(&path); + let device = EGLDevice::enumerate() + .expect("Failed to enumerate EGL devices") + .find(|device| { + device.drm_device_path().ok().as_ref() == Some(&path) + || device.render_device_path().ok().as_ref() == Some(&path) + }) + .expect("Unable to find egl device"); + let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") }; + display.dmabuf_render_formats().clone() + }) + .chain(sample.iter().map(|path| { + let path = PathBuf::from(&path); + let device = EGLDevice::enumerate() + .expect("Failed to enumerate EGL devices") + .find(|device| { + device.drm_device_path().ok().as_ref() == Some(&path) + || device.render_device_path().ok().as_ref() == Some(&path) + }) + .expect("Unable to find egl device"); + let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") }; + display.dmabuf_texture_formats().clone() + })) + .fold(None, |set, formats| match set { + None => Some(formats), + Some(set) => Some(set.intersection(&formats).copied().collect()), + }) + .unwrap_or_default() + { + println!( + "Format: Fourcc {} / Modifier {}", + format.code, + Into::::into(format.modifier) + ) + } +} + +fn open_device(path: &str) -> DrmDeviceFd { + let file = File::options() + .read(true) + .write(true) + .open(path) + .expect("Failed to open device node"); + DrmDeviceFd::new(DeviceFd::from(Into::::into(file))) +} + +fn buffer_test(args: TestArgs) { + // 1. create allocator + let path = args + .export + .expect("Dumb buffer allocator requires an export device"); + info!("Export device: {}", path); + let mut allocator = match args.allocator { + AllocatorType::DumbBuffer => { + let fd = open_device(&path); + let dumb_allocator = DumbAllocator::new(fd); + Box::new(DmabufAllocator(dumb_allocator)) as Box> + } + AllocatorType::Gbm => { + let fd = open_device(&path); + let gbm = GbmDevice::new(fd).expect("Failed to init gbm device"); + let gbm_allocator = GbmAllocator::new(gbm, GbmBufferFlags::RENDERING); + Box::new(DmabufAllocator(gbm_allocator)) as Box<_> + } + AllocatorType::Vulkan => { + let node = DrmNode::from_path(&path).expect("Failed to find drm node"); + let instance = + Instance::new(Version::VERSION_1_2, None).expect("Unable to create vulkan instance"); + let physical_device = PhysicalDevice::enumerate(&instance) + .expect("Failed to enumerate physical devices") + .filter(|phd| phd.has_device_extension(c"VK_EXT_physical_device_drm")) + .find(|phd| { + phd.primary_node().unwrap() == Some(node) || phd.render_node().unwrap() == Some(node) + }) + .expect("Unable to find physical device"); + Box::new(DmabufAllocator( + VulkanAllocator::new(&physical_device, args.usage_flags.unwrap()) + .expect("Failed to create vulkan allocator"), + )) as Box<_> + } + }; + + // 2. alloc buffer + let mut buffer = allocator + .create_buffer(args.width, args.height, args.fourcc, &[args.modifier]) + .expect("Failed to allocate buffer"); + + // 3. render into buffer on src device + match args.export_renderer { + None => {} + Some(RendererType::Gles) => { + let path = PathBuf::from(&path); + let device = EGLDevice::enumerate() + .expect("Failed to enumerate EGL devices") + .find(|device| { + device.drm_device_path().ok().as_ref() == Some(&path) + || device.render_device_path().ok().as_ref() == Some(&path) + }) + .expect("Unable to find egl device"); + let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") }; + + let context = EGLContext::new(&display).expect("Failed to create EGL context"); + let mut renderer = unsafe { GlesRenderer::new(context).expect("Failed to init GL ES renderer") }; + + render_into(&mut renderer, &mut buffer, args.width as i32, args.height as i32); + } + } + + // 4. import / render buffer on dst device + let path = PathBuf::from(args.import); + info!("Import device: {}", path.display()); + match args.import_renderer { + None => { + let device = EGLDevice::enumerate() + .expect("Failed to enumerate EGL devices") + .find(|device| { + device.drm_device_path().ok().as_ref() == Some(&path) + || device.render_device_path().ok().as_ref() == Some(&path) + }) + .expect("Unable to find egl device"); + let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") }; + + display + .create_image_from_dmabuf(&buffer) + .expect("Failed to import dmabuf"); + } + Some(RendererType::Gles) => { + let device = EGLDevice::enumerate() + .expect("Failed to enumerate EGL devices") + .find(|device| { + device.drm_device_path().ok().as_ref() == Some(&path) + || device.render_device_path().ok().as_ref() == Some(&path) + }) + .expect("Unable to find egl device"); + let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") }; + + let context = EGLContext::new(&display).expect("Failed to create EGL context"); + let mut renderer = unsafe { GlesRenderer::new(context).expect("Failed to init GL ES renderer") }; + + render_from::<_, GlesRenderbuffer>( + &mut renderer, + buffer, + args.width as i32, + args.height as i32, + args.dump, + ); + } + } +} + +fn render_into(renderer: &mut R, buffer: &mut T, w: i32, h: i32) +where + R: Renderer + Bind, +{ + // Bind it as a framebuffer + let mut framebuffer = renderer.bind(buffer).expect("Failed to bind dmabuf"); + + let mut frame = renderer + .render(&mut framebuffer, (w, h).into(), Transform::Normal) + .expect("Failed to create render frame"); + frame + .clear( + Color32F::new(1.0, 0.0, 0.0, 1.0), + &[Rectangle::from_size((w / 2, h / 2).into())], + ) + .expect("Render error"); + frame + .clear( + Color32F::new(0.0, 1.0, 0.0, 1.0), + &[Rectangle::new((w / 2, 0).into(), (w / 2, h / 2).into())], + ) + .expect("Render error"); + frame + .clear( + Color32F::new(0.0, 0.0, 1.0, 1.0), + &[Rectangle::new((0, h / 2).into(), (w / 2, h / 2).into())], + ) + .expect("Render error"); + frame + .clear( + Color32F::new(1.0, 1.0, 0.0, 1.0), + &[Rectangle::new((w / 2, h / 2).into(), (w / 2, h / 2).into())], + ) + .expect("Render error"); + frame + .finish() + .expect("Failed to finish render frame") + .wait() + .expect("Synchronization error"); +} + +fn render_from(renderer: &mut R, buffer: Dmabuf, w: i32, h: i32, dump: Option) +where + R: Renderer + ImportDma + ExportMem + Offscreen, +{ + let texture = renderer + .import_dmabuf(&buffer, None) + .expect("Failed to import dmabuf"); + let mut offscreen = Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, (w, h).into()) + .expect("Failed to create offscreen buffer"); + let mut framebuffer = renderer + .bind(&mut offscreen) + .expect("Failed to bind offscreen buffer"); + let mut frame = renderer + .render(&mut framebuffer, (w, h).into(), Transform::Normal) + .expect("Failed to create render frame"); + frame + .render_texture_at( + &texture, + (0, 0).into(), + 1, + 1., + Transform::Normal, + &[Rectangle::from_size((w, h).into())], + &[], + 1.0, + ) + .expect("Failed to sample dmabuf"); + frame + .finish() + .expect("Failed to finish render frame") + .wait() + .expect("Synchronization error"); + + if let Some(path) = dump { + let mapping = renderer + .copy_framebuffer( + &framebuffer, + Rectangle::from_size((w, h).into()), + Fourcc::Abgr8888, + ) + .expect("Failed to map framebuffer"); + let copy = renderer.map_texture(&mapping).expect("Failed to read mapping"); + image::save_buffer(path, copy, w as u32, h as u32, image::ColorType::Rgba8) + .expect("Failed to save image"); + } +} + +fn fourcc_from_string(name: &str) -> Result { + Ok(match name { + "Abgr1555" => Fourcc::Abgr1555, + "Abgr16161616f" => Fourcc::Abgr16161616f, + "Abgr2101010" => Fourcc::Abgr2101010, + "Abgr4444" => Fourcc::Abgr4444, + "Abgr8888" => Fourcc::Abgr8888, + "Argb1555" => Fourcc::Argb1555, + "Argb16161616f" => Fourcc::Argb16161616f, + "Argb2101010" => Fourcc::Argb2101010, + "Argb4444" => Fourcc::Argb4444, + "Argb8888" => Fourcc::Argb8888, + "Axbxgxrx106106106106" => Fourcc::Axbxgxrx106106106106, + "Ayuv" => Fourcc::Ayuv, + "Bgr233" => Fourcc::Bgr233, + "Bgr565" => Fourcc::Bgr565, + "Bgr565_a8" => Fourcc::Bgr565_a8, + "Bgr888" => Fourcc::Bgr888, + "Bgr888_a8" => Fourcc::Bgr888_a8, + "Bgra1010102" => Fourcc::Bgra1010102, + "Bgra4444" => Fourcc::Bgra4444, + "Bgra5551" => Fourcc::Bgra5551, + "Bgra8888" => Fourcc::Bgra8888, + "Bgrx1010102" => Fourcc::Bgrx1010102, + "Bgrx4444" => Fourcc::Bgrx4444, + "Bgrx5551" => Fourcc::Bgrx5551, + "Bgrx8888" => Fourcc::Bgrx8888, + "Bgrx8888_a8" => Fourcc::Bgrx8888_a8, + "Big_endian" => Fourcc::Big_endian, + "C8" => Fourcc::C8, + "Gr1616" => Fourcc::Gr1616, + "Gr88" => Fourcc::Gr88, + "Nv12" => Fourcc::Nv12, + "Nv15" => Fourcc::Nv15, + "Nv16" => Fourcc::Nv16, + "Nv21" => Fourcc::Nv21, + "Nv24" => Fourcc::Nv24, + "Nv42" => Fourcc::Nv42, + "Nv61" => Fourcc::Nv61, + "P010" => Fourcc::P010, + "P012" => Fourcc::P012, + "P016" => Fourcc::P016, + "P210" => Fourcc::P210, + "Q401" => Fourcc::Q401, + "Q410" => Fourcc::Q410, + "R16" => Fourcc::R16, + "R8" => Fourcc::R8, + "Rg1616" => Fourcc::Rg1616, + "Rg88" => Fourcc::Rg88, + "Rgb332" => Fourcc::Rgb332, + "Rgb565" => Fourcc::Rgb565, + "Rgb565_a8" => Fourcc::Rgb565_a8, + "Rgb888" => Fourcc::Rgb888, + "Rgb888_a8" => Fourcc::Rgb888_a8, + "Rgba1010102" => Fourcc::Rgba1010102, + "Rgba4444" => Fourcc::Rgba4444, + "Rgba5551" => Fourcc::Rgba5551, + "Rgba8888" => Fourcc::Rgba8888, + "Rgbx1010102" => Fourcc::Rgbx1010102, + "Rgbx4444" => Fourcc::Rgbx4444, + "Rgbx5551" => Fourcc::Rgbx5551, + "Rgbx8888" => Fourcc::Rgbx8888, + "Rgbx8888_a8" => Fourcc::Rgbx8888_a8, + "Uyvy" => Fourcc::Uyvy, + "Vuy101010" => Fourcc::Vuy101010, + "Vuy888" => Fourcc::Vuy888, + "Vyuy" => Fourcc::Vyuy, + "X0l0" => Fourcc::X0l0, + "X0l2" => Fourcc::X0l2, + "Xbgr1555" => Fourcc::Xbgr1555, + "Xbgr16161616f" => Fourcc::Xbgr16161616f, + "Xbgr2101010" => Fourcc::Xbgr2101010, + "Xbgr4444" => Fourcc::Xbgr4444, + "Xbgr8888" => Fourcc::Xbgr8888, + "Xbgr8888_a8" => Fourcc::Xbgr8888_a8, + "Xrgb1555" => Fourcc::Xrgb1555, + "Xrgb16161616f" => Fourcc::Xrgb16161616f, + "Xrgb2101010" => Fourcc::Xrgb2101010, + "Xrgb4444" => Fourcc::Xrgb4444, + "Xrgb8888" => Fourcc::Xrgb8888, + "Xrgb8888_a8" => Fourcc::Xrgb8888_a8, + "Xvyu12_16161616" => Fourcc::Xvyu12_16161616, + "Xvyu16161616" => Fourcc::Xvyu16161616, + "Xvyu2101010" => Fourcc::Xvyu2101010, + "Xyuv8888" => Fourcc::Xyuv8888, + "Y0l0" => Fourcc::Y0l0, + "Y0l2" => Fourcc::Y0l2, + "Y210" => Fourcc::Y210, + "Y212" => Fourcc::Y212, + "Y216" => Fourcc::Y216, + "Y410" => Fourcc::Y410, + "Y412" => Fourcc::Y412, + "Y416" => Fourcc::Y416, + "Yuv410" => Fourcc::Yuv410, + "Yuv411" => Fourcc::Yuv411, + "Yuv420" => Fourcc::Yuv420, + "Yuv420_10bit" => Fourcc::Yuv420_10bit, + "Yuv420_8bit" => Fourcc::Yuv420_8bit, + "Yuv422" => Fourcc::Yuv422, + "Yuv444" => Fourcc::Yuv444, + "Yuyv" => Fourcc::Yuyv, + "Yvu410" => Fourcc::Yvu410, + "Yvu411" => Fourcc::Yvu411, + "Yvu420" => Fourcc::Yvu420, + "Yvu422" => Fourcc::Yvu422, + "Yvu444" => Fourcc::Yvu444, + "Yvyu" => Fourcc::Yvyu, + _ => { + return Err("Unknown pixel format"); + } + }) +} + +fn modifier_from_string(modifier: &str) -> Result { + Ok(match modifier { + "Invalid" => Modifier::Invalid, + "Linear" => Modifier::Linear, + x => Modifier::from(str::parse::(x)?), + }) +} + +fn usage_flags_from_string(flags: &str) -> Result { + let flags = flags + .split('+') + .map(|f| { + Ok(match f.to_lowercase().trim() { + "all" => ImageUsageFlags::all(), + "empty" | "none" => ImageUsageFlags::empty(), + "color_attachment" => ImageUsageFlags::COLOR_ATTACHMENT, + "sampled" => ImageUsageFlags::SAMPLED, + "transfer_src" => ImageUsageFlags::TRANSFER_SRC, + "transfer_dst" => ImageUsageFlags::TRANSFER_DST, + x => { + return Err(format!("Unknown Usage Flag: {}", x)); + } + }) + }) + .collect::, String>>()?; + Ok(flags + .into_iter() + .fold(ImageUsageFlags::empty(), |akk, val| akk | val)) +} diff --git a/vendor/smithay/examples/compositor.rs b/vendor/smithay/examples/compositor.rs new file mode 100644 index 0000000..714d4c4 --- /dev/null +++ b/vendor/smithay/examples/compositor.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use smithay::delegate_dispatch2; +use smithay::reexports::wayland_server::Display; + +use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState}; + +use wayland_server::backend::{ClientData, ClientId, DisconnectReason}; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::{Client, ListeningSocket}; + +struct App { + compositor_state: CompositorState, +} + +impl CompositorHandler for App { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + &client.get_data::().unwrap().compositor_state + } + + fn commit(&mut self, surface: &WlSurface) { + dbg!("Commit", surface); + } +} + +fn main() -> Result<(), Box> { + let mut display: Display = Display::new()?; + let dh = display.handle(); + + let compositor_state = CompositorState::new::(&dh); + + let mut state = App { compositor_state }; + + let listener = ListeningSocket::bind("wayland-5").unwrap(); + + let mut clients = Vec::new(); + + loop { + if let Some(stream) = listener.accept().unwrap() { + println!("Got a client: {:?}", stream); + + let client = display + .handle() + .insert_client(stream, Arc::new(ClientState::default())) + .unwrap(); + clients.push(client); + } + + display.dispatch_clients(&mut state)?; + display.flush_clients()?; + } +} + +#[derive(Default)] +struct ClientState { + compositor_state: CompositorClientState, +} +impl ClientData for ClientState { + fn initialized(&self, _client_id: ClientId) { + println!("initialized"); + } + + fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) { + println!("disconnected"); + } +} + +impl AsMut for App { + fn as_mut(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } +} + +delegate_dispatch2!(App); diff --git a/vendor/smithay/examples/minimal.rs b/vendor/smithay/examples/minimal.rs new file mode 100644 index 0000000..d288cb5 --- /dev/null +++ b/vendor/smithay/examples/minimal.rs @@ -0,0 +1,295 @@ +use std::sync::Arc; + +use ::winit::event_loop::pump_events::PumpStatus; +use smithay::{ + backend::{ + input::{InputEvent, InputTime, KeyboardKeyEvent}, + renderer::{ + Color32F, Frame, Renderer, + element::{ + Kind, + surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, + }, + gles::GlesRenderer, + utils::{draw_render_elements, on_commit_buffer_handler}, + }, + winit::{self, WinitEvent}, + }, + input::{Seat, SeatHandler, SeatState, keyboard::FilterResult}, + reexports::wayland_server::{Display, protocol::wl_seat}, + utils::{Rectangle, Serial, Transform}, + wayland::{ + buffer::BufferHandler, + compositor::{ + CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes, TraversalAction, + with_surface_tree_downward, + }, + pointer_constraints::PointerConstraintsHandler, + selection::{ + SelectionHandler, + data_device::{DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler}, + }, + shell::xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState}, + shm::{ShmHandler, ShmState}, + }, +}; +use wayland_protocols::xdg::shell::server::xdg_toplevel; +use wayland_server::{ + Client, ListeningSocket, + backend::{ClientData, ClientId, DisconnectReason}, + protocol::{ + wl_buffer, + wl_surface::{self, WlSurface}, + }, +}; + +impl BufferHandler for App { + fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {} +} + +impl XdgShellHandler for App { + fn xdg_shell_state(&mut self) -> &mut XdgShellState { + &mut self.xdg_shell_state + } + + fn new_toplevel(&mut self, surface: ToplevelSurface) { + surface.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Activated); + }); + surface.send_configure(); + } + + fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) { + // Handle popup creation here + } + + fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { + // Handle popup grab here + } + + fn reposition_request(&mut self, _surface: PopupSurface, _positioner: PositionerState, _token: u32) { + // Handle popup reposition here + } +} + +impl SelectionHandler for App { + type SelectionUserData = (); +} + +impl DataDeviceHandler for App { + fn data_device_state(&mut self) -> &mut DataDeviceState { + &mut self.data_device_state + } +} + +impl WaylandDndGrabHandler for App {} + +impl CompositorHandler for App { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + &client.get_data::().unwrap().compositor_state + } + + fn commit(&mut self, surface: &WlSurface) { + on_commit_buffer_handler::(surface); + } +} + +impl ShmHandler for App { + fn shm_state(&self) -> &ShmState { + &self.shm_state + } +} + +impl SeatHandler for App { + type KeyboardFocus = WlSurface; + type PointerFocus = WlSurface; + type TouchFocus = WlSurface; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&WlSurface>) {} + fn cursor_image(&mut self, _seat: &Seat, _image: smithay::input::pointer::CursorImageStatus) {} +} + +impl PointerConstraintsHandler for App {} + +struct App { + compositor_state: CompositorState, + xdg_shell_state: XdgShellState, + shm_state: ShmState, + seat_state: SeatState, + data_device_state: DataDeviceState, + + seat: Seat, +} + +fn main() -> Result<(), Box> { + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + } else { + tracing_subscriber::fmt().init(); + } + + run_winit() +} + +pub fn run_winit() -> Result<(), Box> { + let mut display: Display = Display::new()?; + let dh = display.handle(); + + let compositor_state = CompositorState::new::(&dh); + let shm_state = ShmState::new::(&dh, vec![]); + let mut seat_state = SeatState::new(); + let seat = seat_state.new_wl_seat(&dh, "winit"); + + let mut state = { + App { + compositor_state, + xdg_shell_state: XdgShellState::new::(&dh), + shm_state, + seat_state, + data_device_state: DataDeviceState::new::(&dh), + seat, + } + }; + + let listener = ListeningSocket::bind("wayland-5").unwrap(); + let mut clients = Vec::new(); + + let (mut backend, mut winit) = winit::init::()?; + + let start_time = std::time::Instant::now(); + + let keyboard = state.seat.add_keyboard(Default::default(), 200, 200).unwrap(); + + unsafe { std::env::set_var("WAYLAND_DISPLAY", "wayland-5") }; + std::process::Command::new("weston-terminal").spawn().ok(); + + loop { + let status = winit.dispatch_new_events(|event| match event { + WinitEvent::Resized { .. } => {} + WinitEvent::Input(event) => match event { + InputEvent::Keyboard { event } => { + keyboard.input::<(), _>( + &mut state, + event.key_code(), + event.state(), + 0.into(), + InputTime::now(), + |_, _, _| { + // + FilterResult::Forward + }, + ); + } + InputEvent::PointerMotionAbsolute { .. } => { + if let Some(surface) = state.xdg_shell_state.toplevel_surfaces().iter().next().cloned() { + let surface = surface.wl_surface().clone(); + keyboard.set_focus(&mut state, Some(surface), 0.into()); + }; + } + _ => {} + }, + _ => (), + }); + + match status { + PumpStatus::Continue => (), + PumpStatus::Exit(_) => return Ok(()), + }; + + let size = backend.window_size(); + let damage = Rectangle::from_size(size); + { + let (renderer, mut framebuffer) = backend.bind().unwrap(); + let elements = state + .xdg_shell_state + .toplevel_surfaces() + .iter() + .flat_map(|surface| { + render_elements_from_surface_tree( + renderer, + surface.wl_surface(), + (0, 0), + 1.0, + 1.0, + Kind::Unspecified, + ) + }) + .collect::>>(); + + let mut frame = renderer + .render(&mut framebuffer, size, Transform::Flipped180) + .unwrap(); + frame.clear(Color32F::new(0.1, 0.0, 0.0, 1.0), &[damage]).unwrap(); + draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap(); + // We rely on the nested compositor to do the sync for us + let _ = frame.finish().unwrap(); + + for surface in state.xdg_shell_state.toplevel_surfaces() { + send_frames_surface_tree(surface.wl_surface(), start_time.elapsed().as_millis() as u32); + } + + if let Some(stream) = listener.accept()? { + println!("Got a client: {:?}", stream); + + let client = display + .handle() + .insert_client(stream, Arc::new(ClientState::default())) + .unwrap(); + clients.push(client); + } + + display.dispatch_clients(&mut state)?; + display.flush_clients()?; + } + + // It is important that all events on the display have been dispatched and flushed to clients before + // swapping buffers because this operation may block. + backend.submit(Some(&[damage])).unwrap(); + } +} + +pub fn send_frames_surface_tree(surface: &wl_surface::WlSurface, time: u32) { + with_surface_tree_downward( + surface, + (), + |_, _, &()| TraversalAction::DoChildren(()), + |_surf, states, &()| { + // the surface may not have any user_data if it is a subsurface and has not + // yet been committed + for callback in states + .cached_state + .get::() + .current() + .frame_callbacks + .drain(..) + { + callback.done(time); + } + }, + |_, _, &()| true, + ); +} + +#[derive(Default)] +struct ClientState { + compositor_state: CompositorClientState, +} +impl ClientData for ClientState { + fn initialized(&self, _client_id: ClientId) { + println!("initialized"); + } + + fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) { + println!("disconnected"); + } +} + +smithay::delegate_dispatch2!(App); diff --git a/vendor/smithay/examples/seat.rs b/vendor/smithay/examples/seat.rs new file mode 100644 index 0000000..0bc2806 --- /dev/null +++ b/vendor/smithay/examples/seat.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use smithay::backend::input::InputTime; +use smithay::input::{Seat, SeatHandler, SeatState, keyboard::FilterResult}; +use smithay::reexports::wayland_server::{ + Display, ListeningSocket, + backend::{ClientData, ClientId, DisconnectReason}, + protocol::wl_surface::WlSurface, +}; +use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState}; +use smithay::wayland::pointer_constraints::PointerConstraintsHandler; + +struct App { + compositor_state: CompositorState, + seat_state: SeatState, + seat: Seat, +} + +impl SeatHandler for App { + type KeyboardFocus = WlSurface; + type PointerFocus = WlSurface; + type TouchFocus = WlSurface; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&WlSurface>) {} + fn cursor_image(&mut self, _seat: &Seat, _image: smithay::input::pointer::CursorImageStatus) {} +} + +impl PointerConstraintsHandler for App {} + +fn main() -> Result<(), Box> { + let mut display: Display = Display::new()?; + let dh = display.handle(); + + let compositor_state = CompositorState::new::(&dh); + let mut seat_state = SeatState::new(); + let seat = seat_state.new_wl_seat(&dh, "Example"); + + let mut state = App { + compositor_state, + seat_state, + seat, + }; + + let keyboard = state.seat.add_keyboard(Default::default(), 25, 600)?; + + let listener = ListeningSocket::bind("wayland-5").unwrap(); + + let mut clients = Vec::new(); + + loop { + if let Some(stream) = listener.accept().unwrap() { + println!("Got a client: {:?}", stream); + + let client = display + .handle() + .insert_client(stream, Arc::new(ClientState(CompositorClientState::default()))) + .unwrap(); + clients.push(client); + } + + keyboard.input( + &mut state, + smithay::backend::input::Keycode::from(9u32), + smithay::backend::input::KeyState::Pressed, + 0.into(), + InputTime::now(), + |_, _, _| { + if false { + FilterResult::Intercept(0) + } else { + FilterResult::Forward + } + }, + ); + + keyboard.set_focus(&mut state, Option::::None, 0.into()); + + display.dispatch_clients(&mut state)?; + display.flush_clients()?; + } +} + +struct ClientState(CompositorClientState); +impl ClientData for ClientState { + fn initialized(&self, _client_id: ClientId) { + println!("initialized"); + } + + fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) { + println!("disconnected"); + } +} + +impl CompositorHandler for App { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { + &client.get_data::().unwrap().0 + } + + fn commit(&mut self, _surface: &WlSurface) {} +} + +smithay::delegate_dispatch2!(App); diff --git a/vendor/smithay/examples/vulkan.rs b/vendor/smithay/examples/vulkan.rs new file mode 100644 index 0000000..3aace0f --- /dev/null +++ b/vendor/smithay/examples/vulkan.rs @@ -0,0 +1,61 @@ +use drm_fourcc::{DrmFourcc, DrmModifier}; +use smithay::backend::{ + allocator::{ + Allocator, Buffer, + dmabuf::AsDmabuf, + vulkan::{ImageUsageFlags, VulkanAllocator}, + }, + vulkan::{Instance, PhysicalDevice, version::Version}, +}; + +fn main() { + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + } else { + tracing_subscriber::fmt().init(); + } + + println!( + "Available instance extensions: {:?}", + Instance::enumerate_extensions().unwrap().collect::>() + ); + println!(); + + let instance = Instance::new(Version::VERSION_1_3, None).unwrap(); + + for (idx, phy) in PhysicalDevice::enumerate(&instance).unwrap().enumerate() { + println!( + "Device #{}: {} v{}, {:?}", + idx, + phy.name(), + phy.api_version(), + phy.driver() + ); + } + + let physical_device = PhysicalDevice::enumerate(&instance) + .unwrap() + .next() + .expect("No physical devices"); + + // The allocator should create buffers that are suitable as render targets. + let mut allocator = VulkanAllocator::new(&physical_device, ImageUsageFlags::COLOR_ATTACHMENT).unwrap(); + + let image = allocator + .create_buffer(100, 200, DrmFourcc::Argb8888, &[DrmModifier::Linear]) + .expect("create"); + + assert_eq!(image.width(), 100); + assert_eq!(image.height(), 200); + + let image_dmabuf = image.export().expect("Export dmabuf"); + + drop(image); + + let _image2 = allocator + .create_buffer(200, 200, DrmFourcc::Argb8888, &[DrmModifier::Linear]) + .expect("create"); + + drop(allocator); + drop(image_dmabuf); +} diff --git a/vendor/smithay/patches/shm-texture-cache-format.patch b/vendor/smithay/patches/shm-texture-cache-format.patch new file mode 100644 index 0000000..30306d2 --- /dev/null +++ b/vendor/smithay/patches/shm-texture-cache-format.patch @@ -0,0 +1,46 @@ +--- a/src/backend/renderer/gles/mod.rs ++++ b/src/backend/renderer/gles/mod.rs +@@ -883,6 +883,8 @@ + // why not store a `GlesTexture`? because the user might do so. + // this is guaranteed a non-public internal type, so we are good. + type CacheMap = HashMap, Arc>; ++ static SHM_FORMAT_CHANGE_LOGS: std::sync::atomic::AtomicUsize = ++ std::sync::atomic::AtomicUsize::new(0); + + let mut surface_lock = surface.as_ref().map(|surface_data| { + surface_data +@@ -937,7 +939,33 @@ + surface_lock + .as_ref() + .and_then(|cache| cache.get(&id).cloned()) +- .filter(|texture| texture.size == (width, height).into()) ++ .filter(|texture| { ++ let format_matches = ++ texture.format == Some(internal_format) && texture.has_alpha == has_alpha; ++ if !format_matches ++ && tracing::enabled!(tracing::Level::DEBUG) ++ && SHM_FORMAT_CHANGE_LOGS ++ .fetch_update( ++ std::sync::atomic::Ordering::Relaxed, ++ std::sync::atomic::Ordering::Relaxed, ++ |count| (count < 16).then_some(count + 1), ++ ) ++ .is_ok() ++ { ++ tracing::debug!( ++ old_width = texture.size.w, ++ old_height = texture.size.h, ++ new_width = width, ++ new_height = height, ++ old_internal_format = texture.format.unwrap_or(0), ++ new_internal_format = internal_format, ++ old_has_alpha = texture.has_alpha, ++ new_has_alpha = has_alpha, ++ "SHM texture cache format changed" ++ ); ++ } ++ texture.size == (width, height).into() && format_matches ++ }) + .unwrap_or_else(|| { + let mut tex = 0; + unsafe { self.gl.GenTextures(1, &mut tex) }; diff --git a/vendor/smithay/smallvil/Cargo.toml b/vendor/smithay/smallvil/Cargo.toml new file mode 100644 index 0000000..be844ac --- /dev/null +++ b/vendor/smithay/smallvil/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "smallvil" +version = "0.1.0" +edition = "2024" + +[dependencies] +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +bitflags = "2.2.1" + +[dependencies.smithay] +path = "../" +default-features = false +features = [ + "backend_winit", + "wayland_frontend", + "desktop", +] diff --git a/vendor/smithay/smallvil/src/grabs/mod.rs b/vendor/smithay/smallvil/src/grabs/mod.rs new file mode 100644 index 0000000..5756e1b --- /dev/null +++ b/vendor/smithay/smallvil/src/grabs/mod.rs @@ -0,0 +1,5 @@ +pub mod move_grab; +pub use move_grab::MoveSurfaceGrab; + +pub mod resize_grab; +pub use resize_grab::ResizeSurfaceGrab; diff --git a/vendor/smithay/smallvil/src/grabs/move_grab.rs b/vendor/smithay/smallvil/src/grabs/move_grab.rs new file mode 100644 index 0000000..61a2963 --- /dev/null +++ b/vendor/smithay/smallvil/src/grabs/move_grab.rs @@ -0,0 +1,160 @@ +//! Move grab is the state of a composer during which the client window is being dragged around. +//! +//! eg. Usually whenever a user clicks on the app's titlebar and starts dragging, the compositors +//! enters a MoveSurfaceGrab state. + +use crate::Smallvil; +use smithay::{ + desktop::Window, + input::pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, + }, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::{Logical, Point}, +}; + +pub struct MoveSurfaceGrab { + pub start_data: PointerGrabStartData, + pub window: Window, + pub initial_window_location: Point, +} + +impl PointerGrab for MoveSurfaceGrab { + fn motion( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + _focus: Option<(WlSurface, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + let delta = event.location - self.start_data.location; + let new_location = self.initial_window_location.to_f64() + delta; + data.space + .map_element(self.window.clone(), new_location.to_i32_round(), true); + } + + fn relative_motion( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + focus: Option<(WlSurface, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &ButtonEvent, + ) { + handle.button(data, event); + + // The button is a button code as defined in the + // Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT. + const BTN_LEFT: u32 = 0x110; + + if !handle.current_pressed().contains(&BTN_LEFT) { + // No more buttons are pressed, release the grab. + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn axis( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame(&mut self, data: &mut Smallvil, handle: &mut PointerInnerHandle<'_, Smallvil>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + &self.start_data + } + + fn unset(&mut self, _data: &mut Smallvil) {} +} diff --git a/vendor/smithay/smallvil/src/grabs/resize_grab.rs b/vendor/smithay/smallvil/src/grabs/resize_grab.rs new file mode 100644 index 0000000..276dc68 --- /dev/null +++ b/vendor/smithay/smallvil/src/grabs/resize_grab.rs @@ -0,0 +1,365 @@ +//! Resize grab is the state of a composer during which the client window is being resized. +//! +//! eg. Usually whenever a user clicks on the app's border and starts dragging, the compositors +//! enters a ResizeSurfaceGrab state. + +use crate::Smallvil; +use smithay::{ + desktop::{Space, Window}, + input::pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, + }, + reexports::{ + wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_server::protocol::wl_surface::WlSurface, + }, + utils::{Logical, Point, Rectangle, Size}, + wayland::{compositor, shell::xdg::SurfaceCachedState}, +}; +use std::cell::RefCell; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct ResizeEdge: u32 { + const TOP = 0b0001; + const BOTTOM = 0b0010; + const LEFT = 0b0100; + const RIGHT = 0b1000; + + const TOP_LEFT = Self::TOP.bits() | Self::LEFT.bits(); + const BOTTOM_LEFT = Self::BOTTOM.bits() | Self::LEFT.bits(); + + const TOP_RIGHT = Self::TOP.bits() | Self::RIGHT.bits(); + const BOTTOM_RIGHT = Self::BOTTOM.bits() | Self::RIGHT.bits(); + } +} + +impl From for ResizeEdge { + #[inline] + fn from(x: xdg_toplevel::ResizeEdge) -> Self { + Self::from_bits(x as u32).unwrap() + } +} + +pub struct ResizeSurfaceGrab { + start_data: PointerGrabStartData, + window: Window, + + edges: ResizeEdge, + + initial_rect: Rectangle, + last_window_size: Size, +} + +impl ResizeSurfaceGrab { + pub fn start( + start_data: PointerGrabStartData, + window: Window, + edges: ResizeEdge, + initial_window_rect: Rectangle, + ) -> Self { + let initial_rect = initial_window_rect; + + ResizeSurfaceState::with(window.toplevel().unwrap().wl_surface(), |state| { + *state = ResizeSurfaceState::Resizing { edges, initial_rect }; + }); + + Self { + start_data, + window, + edges, + initial_rect, + last_window_size: initial_rect.size, + } + } +} + +impl PointerGrab for ResizeSurfaceGrab { + fn motion( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + _focus: Option<(WlSurface, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + let mut delta = event.location - self.start_data.location; + + let mut new_window_width = self.initial_rect.size.w; + let mut new_window_height = self.initial_rect.size.h; + + if self.edges.intersects(ResizeEdge::LEFT | ResizeEdge::RIGHT) { + if self.edges.intersects(ResizeEdge::LEFT) { + delta.x = -delta.x; + } + + new_window_width = (self.initial_rect.size.w as f64 + delta.x) as i32; + } + + if self.edges.intersects(ResizeEdge::TOP | ResizeEdge::BOTTOM) { + if self.edges.intersects(ResizeEdge::TOP) { + delta.y = -delta.y; + } + + new_window_height = (self.initial_rect.size.h as f64 + delta.y) as i32; + } + + let (min_size, max_size) = + compositor::with_states(self.window.toplevel().unwrap().wl_surface(), |states| { + let mut guard = states.cached_state.get::(); + let data = guard.current(); + (data.min_size, data.max_size) + }); + + let min_width = min_size.w.max(1); + let min_height = min_size.h.max(1); + + let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w }; + let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h }; + + self.last_window_size = Size::from(( + new_window_width.max(min_width).min(max_width), + new_window_height.max(min_height).min(max_height), + )); + + let xdg = self.window.toplevel().unwrap(); + xdg.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + + xdg.send_pending_configure(); + } + + fn relative_motion( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + focus: Option<(WlSurface, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &ButtonEvent, + ) { + handle.button(data, event); + + // The button is a button code as defined in the + // Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT. + const BTN_LEFT: u32 = 0x110; + + if !handle.current_pressed().contains(&BTN_LEFT) { + // No more buttons are pressed, release the grab. + handle.unset_grab(self, data, event.serial, event.time, true); + + let xdg = self.window.toplevel().unwrap(); + xdg.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + + xdg.send_pending_configure(); + + ResizeSurfaceState::with(xdg.wl_surface(), |state| { + *state = ResizeSurfaceState::WaitingForLastCommit { + edges: self.edges, + initial_rect: self.initial_rect, + }; + }); + } + } + + fn axis( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame(&mut self, data: &mut Smallvil, handle: &mut PointerInnerHandle<'_, Smallvil>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut Smallvil, + handle: &mut PointerInnerHandle<'_, Smallvil>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + &self.start_data + } + + fn unset(&mut self, _data: &mut Smallvil) {} +} + +/// State of the resize operation. +/// +/// It is stored inside of WlSurface, +/// and can be accessed using [`ResizeSurfaceState::with`] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +enum ResizeSurfaceState { + #[default] + Idle, + Resizing { + edges: ResizeEdge, + /// The initial window size and location. + initial_rect: Rectangle, + }, + /// Resize is done, we are now waiting for last commit, to do the final move + WaitingForLastCommit { + edges: ResizeEdge, + /// The initial window size and location. + initial_rect: Rectangle, + }, +} + +impl ResizeSurfaceState { + fn with(surface: &WlSurface, cb: F) -> T + where + F: FnOnce(&mut Self) -> T, + { + compositor::with_states(surface, |states| { + states.data_map.insert_if_missing(RefCell::::default); + let state = states.data_map.get::>().unwrap(); + + cb(&mut state.borrow_mut()) + }) + } + + fn commit(&mut self) -> Option<(ResizeEdge, Rectangle)> { + match *self { + Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)), + Self::WaitingForLastCommit { edges, initial_rect } => { + // The resize is done, let's go back to idle + *self = Self::Idle; + + Some((edges, initial_rect)) + } + Self::Idle => None, + } + } +} + +/// Should be called on `WlSurface::commit` +pub fn handle_commit(space: &mut Space, surface: &WlSurface) -> Option<()> { + let window = space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == surface) + .cloned()?; + + let mut window_loc = space.element_location(&window)?; + let geometry = window.geometry(); + + let new_loc: Point, Logical> = ResizeSurfaceState::with(surface, |state| { + state + .commit() + .and_then(|(edges, initial_rect)| { + // If the window is being resized by top or left, its location must be adjusted + // accordingly. + edges.intersects(ResizeEdge::TOP_LEFT).then(|| { + let new_x = edges + .intersects(ResizeEdge::LEFT) + .then_some(initial_rect.loc.x + (initial_rect.size.w - geometry.size.w)); + + let new_y = edges + .intersects(ResizeEdge::TOP) + .then_some(initial_rect.loc.y + (initial_rect.size.h - geometry.size.h)); + + (new_x, new_y).into() + }) + }) + .unwrap_or_default() + }); + + if let Some(new_x) = new_loc.x { + window_loc.x = new_x; + } + if let Some(new_y) = new_loc.y { + window_loc.y = new_y; + } + + if new_loc.x.is_some() || new_loc.y.is_some() { + // If TOP or LEFT side of the window got resized, we have to move it + space.map_element(window, window_loc, false); + } + + Some(()) +} diff --git a/vendor/smithay/smallvil/src/handlers/compositor.rs b/vendor/smithay/smallvil/src/handlers/compositor.rs new file mode 100644 index 0000000..188b06e --- /dev/null +++ b/vendor/smithay/smallvil/src/handlers/compositor.rs @@ -0,0 +1,57 @@ +use crate::{Smallvil, grabs::resize_grab, state::ClientState}; +use smithay::{ + backend::renderer::utils::on_commit_buffer_handler, + reexports::wayland_server::{ + Client, + protocol::{wl_buffer, wl_surface::WlSurface}, + }, + wayland::{ + buffer::BufferHandler, + compositor::{ + CompositorClientState, CompositorHandler, CompositorState, get_parent, is_sync_subsurface, + }, + shm::{ShmHandler, ShmState}, + }, +}; + +use super::xdg_shell; + +impl CompositorHandler for Smallvil { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + &client.get_data::().unwrap().compositor_state + } + + fn commit(&mut self, surface: &WlSurface) { + on_commit_buffer_handler::(surface); + if !is_sync_subsurface(surface) { + let mut root = surface.clone(); + while let Some(parent) = get_parent(&root) { + root = parent; + } + if let Some(window) = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == &root) + { + window.on_commit(); + } + }; + + xdg_shell::handle_commit(&mut self.popups, &self.space, surface); + resize_grab::handle_commit(&mut self.space, surface); + } +} + +impl BufferHandler for Smallvil { + fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {} +} + +impl ShmHandler for Smallvil { + fn shm_state(&self) -> &ShmState { + &self.shm_state + } +} diff --git a/vendor/smithay/smallvil/src/handlers/mod.rs b/vendor/smithay/smallvil/src/handlers/mod.rs new file mode 100644 index 0000000..fafba61 --- /dev/null +++ b/vendor/smithay/smallvil/src/handlers/mod.rs @@ -0,0 +1,90 @@ +mod compositor; +mod xdg_shell; + +use crate::Smallvil; + +// +// Wl Seat +// + +use smithay::input::dnd::{DnDGrab, DndGrabHandler, GrabType, Source}; +use smithay::input::pointer::Focus; +use smithay::input::{Seat, SeatHandler, SeatState}; +use smithay::reexports::wayland_server::Resource; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::Serial; +use smithay::wayland::output::OutputHandler; +use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +use smithay::wayland::selection::SelectionHandler; +use smithay::wayland::selection::data_device::{ + DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus, +}; + +impl SeatHandler for Smallvil { + type KeyboardFocus = WlSurface; + type PointerFocus = WlSurface; + type TouchFocus = WlSurface; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn cursor_image(&mut self, _seat: &Seat, _image: smithay::input::pointer::CursorImageStatus) {} + + fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { + let dh = &self.display_handle; + let client = focused.and_then(|s| dh.get_client(s.id()).ok()); + set_data_device_focus(dh, seat, client); + } +} + +impl PointerConstraintsHandler for Smallvil {} + +// +// Wl Data Device +// + +impl SelectionHandler for Smallvil { + type SelectionUserData = (); +} + +impl DataDeviceHandler for Smallvil { + fn data_device_state(&mut self) -> &mut DataDeviceState { + &mut self.data_device_state + } +} + +impl DndGrabHandler for Smallvil {} +impl WaylandDndGrabHandler for Smallvil { + fn dnd_requested( + &mut self, + source: S, + _icon: Option, + seat: Seat, + serial: Serial, + type_: GrabType, + ) { + match type_ { + GrabType::Pointer => { + let ptr = seat.get_pointer().unwrap(); + let start_data = ptr.grab_start_data().unwrap(); + + // create a dnd grab to start the operation + let grab = DnDGrab::new_pointer(&self.display_handle, start_data, source, seat); + ptr.set_grab(self, grab, serial, Focus::Keep); + } + GrabType::Touch => { + // smallvil lacks touch handling + source.cancel(); + } + } + } +} + +// +// Wl Output & Xdg Output +// + +impl OutputHandler for Smallvil {} + +smithay::delegate_dispatch2!(Smallvil); diff --git a/vendor/smithay/smallvil/src/handlers/xdg_shell.rs b/vendor/smithay/smallvil/src/handlers/xdg_shell.rs new file mode 100644 index 0000000..be3a02f --- /dev/null +++ b/vendor/smithay/smallvil/src/handlers/xdg_shell.rs @@ -0,0 +1,214 @@ +use smithay::{ + desktop::{PopupKind, PopupManager, Space, Window, find_popup_root_surface, get_popup_toplevel_coords}, + input::{ + Seat, + pointer::{Focus, GrabStartData as PointerGrabStartData}, + }, + reexports::{ + wayland_protocols::xdg::shell::server::xdg_toplevel, + wayland_server::{ + Resource, + protocol::{wl_seat, wl_surface::WlSurface}, + }, + }, + utils::{Rectangle, Serial}, + wayland::{ + compositor::with_states, + shell::xdg::{ + PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, + XdgToplevelSurfaceData, + }, + }, +}; + +use crate::{ + Smallvil, + grabs::{MoveSurfaceGrab, ResizeSurfaceGrab}, +}; + +impl XdgShellHandler for Smallvil { + fn xdg_shell_state(&mut self) -> &mut XdgShellState { + &mut self.xdg_shell_state + } + + fn new_toplevel(&mut self, surface: ToplevelSurface) { + let window = Window::new_wayland_window(surface); + self.space.map_element(window, (0, 0), false); + } + + fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { + self.unconstrain_popup(&surface); + let _ = self.popups.track_popup(PopupKind::Xdg(surface)); + } + + fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) { + surface.with_pending_state(|state| { + let geometry = positioner.get_geometry(); + state.geometry = geometry; + state.positioner = positioner; + }); + self.unconstrain_popup(&surface); + surface.send_repositioned(token); + } + + fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) { + let seat = Seat::from_resource(&seat).unwrap(); + + let wl_surface = surface.wl_surface(); + + if let Some(start_data) = check_grab(&seat, wl_surface, serial) { + let pointer = seat.get_pointer().unwrap(); + + let window = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == wl_surface) + .unwrap() + .clone(); + let initial_window_location = self.space.element_location(&window).unwrap(); + + let grab = MoveSurfaceGrab { + start_data, + window, + initial_window_location, + }; + + pointer.set_grab(self, grab, serial, Focus::Clear); + } + } + + fn resize_request( + &mut self, + surface: ToplevelSurface, + seat: wl_seat::WlSeat, + serial: Serial, + edges: xdg_toplevel::ResizeEdge, + ) { + let seat = Seat::from_resource(&seat).unwrap(); + + let wl_surface = surface.wl_surface(); + + if let Some(start_data) = check_grab(&seat, wl_surface, serial) { + let pointer = seat.get_pointer().unwrap(); + + let window = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == wl_surface) + .unwrap() + .clone(); + let initial_window_location = self.space.element_location(&window).unwrap(); + let initial_window_size = window.geometry().size; + + surface.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + }); + + surface.send_pending_configure(); + + let grab = ResizeSurfaceGrab::start( + start_data, + window, + edges.into(), + Rectangle::new(initial_window_location, initial_window_size), + ); + + pointer.set_grab(self, grab, serial, Focus::Clear); + } + } + + fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { + // TODO popup grabs + } +} + +fn check_grab( + seat: &Seat, + surface: &WlSurface, + serial: Serial, +) -> Option> { + let pointer = seat.get_pointer()?; + + // Check that this surface has a click grab. + if !pointer.has_grab(serial) { + return None; + } + + let start_data = pointer.grab_start_data()?; + + let (focus, _) = start_data.focus.as_ref()?; + // If the focus was for a different surface, ignore the request. + if !focus.id().same_client_as(&surface.id()) { + return None; + } + + Some(start_data) +} + +/// Should be called on `WlSurface::commit` +pub fn handle_commit(popups: &mut PopupManager, space: &Space, surface: &WlSurface) { + // Handle toplevel commits. + if let Some(window) = space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == surface) + .cloned() + { + let initial_configure_sent = with_states(surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }); + + if !initial_configure_sent { + window.toplevel().unwrap().send_configure(); + } + } + + // Handle popup commits. + popups.commit(surface); + if let Some(popup) = popups.find_popup(surface) { + match popup { + PopupKind::Xdg(ref xdg) => { + if !xdg.is_initial_configure_sent() { + // NOTE: This should never fail as the initial configure is always + // allowed. + xdg.send_configure().expect("initial configure failed"); + } + } + PopupKind::InputMethod(ref _input_method) => {} + } + } +} + +impl Smallvil { + fn unconstrain_popup(&self, popup: &PopupSurface) { + let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else { + return; + }; + let Some(window) = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == &root) + else { + return; + }; + + let output = self.space.outputs().next().unwrap(); + let output_geo = self.space.output_geometry(output).unwrap(); + let window_geo = self.space.element_geometry(window).unwrap(); + + // The target geometry for the positioner should be relative to its parent's geometry, so + // we will compute that here. + let mut target = output_geo; + target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())); + target.loc -= window_geo.loc; + + popup.with_pending_state(|state| { + state.geometry = state.positioner.get_unconstrained_geometry(target); + }); + } +} diff --git a/vendor/smithay/smallvil/src/input.rs b/vendor/smithay/smallvil/src/input.rs new file mode 100644 index 0000000..cb90962 --- /dev/null +++ b/vendor/smithay/smallvil/src/input.rs @@ -0,0 +1,144 @@ +use smithay::{ + backend::input::{ + AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, + KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, + }, + input::{ + keyboard::FilterResult, + pointer::{AxisFrame, ButtonEvent, MotionEvent}, + }, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::SERIAL_COUNTER, +}; + +use crate::state::Smallvil; + +impl Smallvil { + pub fn process_input_event(&mut self, event: InputEvent) { + match event { + InputEvent::Keyboard { event, .. } => { + let serial = SERIAL_COUNTER.next_serial(); + let time = Event::time(&event); + + self.seat.get_keyboard().unwrap().input::<(), _>( + self, + event.key_code(), + event.state(), + serial, + time, + |_, _, _| FilterResult::Forward, + ); + } + InputEvent::PointerMotion { .. } => {} + InputEvent::PointerMotionAbsolute { event, .. } => { + let output = self.space.outputs().next().unwrap(); + + let output_geo = self.space.output_geometry(output).unwrap(); + + let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64(); + + let serial = SERIAL_COUNTER.next_serial(); + + let pointer = self.seat.get_pointer().unwrap(); + + let under = self.surface_under(pos); + + pointer.motion( + self, + under, + &MotionEvent { + location: pos, + serial, + time: event.time(), + }, + ); + pointer.frame(self); + } + InputEvent::PointerButton { event, .. } => { + let pointer = self.seat.get_pointer().unwrap(); + let keyboard = self.seat.get_keyboard().unwrap(); + + let serial = SERIAL_COUNTER.next_serial(); + + let button = event.button_code(); + + let button_state = event.state(); + + if ButtonState::Pressed == button_state && !pointer.is_grabbed() { + if let Some((window, _loc)) = self + .space + .element_under(pointer.current_location()) + .map(|(w, l)| (w.clone(), l)) + { + self.space.raise_element(&window, true); + keyboard.set_focus( + self, + Some(window.toplevel().unwrap().wl_surface().clone()), + serial, + ); + self.space.elements().for_each(|window| { + window.toplevel().unwrap().send_pending_configure(); + }); + } else { + self.space.elements().for_each(|window| { + window.set_activated(false); + window.toplevel().unwrap().send_pending_configure(); + }); + keyboard.set_focus(self, Option::::None, serial); + } + }; + + pointer.button( + self, + &ButtonEvent { + button, + state: button_state, + serial, + time: event.time(), + }, + ); + pointer.frame(self); + } + InputEvent::PointerAxis { event, .. } => { + let source = event.source(); + + let horizontal_amount = event + .amount(Axis::Horizontal) + .unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.); + let vertical_amount = event + .amount(Axis::Vertical) + .unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.); + let horizontal_amount_discrete = event.amount_v120(Axis::Horizontal); + let vertical_amount_discrete = event.amount_v120(Axis::Vertical); + + let mut frame = AxisFrame::new(event.time()).source(source); + if horizontal_amount != 0.0 { + frame = frame.value(Axis::Horizontal, horizontal_amount); + if let Some(discrete) = horizontal_amount_discrete { + frame = frame.v120(Axis::Horizontal, discrete as i32); + } + } + if vertical_amount != 0.0 { + frame = frame.value(Axis::Vertical, vertical_amount); + if let Some(discrete) = vertical_amount_discrete { + frame = frame.v120(Axis::Vertical, discrete as i32); + } + } + + if source == AxisSource::Finger { + if event.amount(Axis::Horizontal) == Some(0.0) { + frame = frame.stop(Axis::Horizontal); + } + if event.amount(Axis::Vertical) == Some(0.0) { + frame = frame.stop(Axis::Vertical); + } + } + + let pointer = self.seat.get_pointer().unwrap(); + pointer.axis(self, frame); + pointer.frame(self); + } + _ => {} + } + } +} diff --git a/vendor/smithay/smallvil/src/main.rs b/vendor/smithay/smallvil/src/main.rs new file mode 100644 index 0000000..27832bc --- /dev/null +++ b/vendor/smithay/smallvil/src/main.rs @@ -0,0 +1,60 @@ +#![allow(irrefutable_let_patterns)] + +mod handlers; + +mod grabs; +mod input; +mod state; +mod winit; + +use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; +pub use state::Smallvil; + +fn main() -> Result<(), Box> { + init_logging(); + + let mut event_loop: EventLoop = EventLoop::try_new()?; + + let display: Display = Display::new()?; + + let mut state = Smallvil::new(&mut event_loop, display); + + // Open a Wayland/X11 window for our nested compositor + crate::winit::init_winit(&mut event_loop, &mut state)?; + + // Set WAYLAND_DISPLAY to our socket name, so child processes connect to Smallvil rather + // than the host compositor + unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; + + // Spawn a test client, that will run under Smallvil + spawn_client(); + + event_loop.run(None, &mut state, move |_| { + // Smallvil is running + })?; + + Ok(()) +} + +fn init_logging() { + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + } else { + tracing_subscriber::fmt().init(); + } +} + +fn spawn_client() { + let mut args = std::env::args().skip(1); + let flag = args.next(); + let arg = args.next(); + + match (flag.as_deref(), arg) { + (Some("-c") | Some("--command"), Some(command)) => { + std::process::Command::new(command).spawn().ok(); + } + _ => { + std::process::Command::new("weston-terminal").spawn().ok(); + } + } +} diff --git a/vendor/smithay/smallvil/src/state.rs b/vendor/smithay/smallvil/src/state.rs new file mode 100644 index 0000000..42c68ad --- /dev/null +++ b/vendor/smithay/smallvil/src/state.rs @@ -0,0 +1,168 @@ +use std::{ffi::OsString, sync::Arc}; + +use smithay::{ + desktop::{PopupManager, Space, Window, WindowSurfaceType}, + input::{Seat, SeatState}, + reexports::{ + calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic}, + wayland_server::{ + Display, DisplayHandle, + backend::{ClientData, ClientId, DisconnectReason}, + protocol::wl_surface::WlSurface, + }, + }, + utils::{Logical, Point}, + wayland::{ + compositor::{CompositorClientState, CompositorState}, + output::OutputManagerState, + selection::data_device::DataDeviceState, + shell::xdg::XdgShellState, + shm::ShmState, + socket::ListeningSocketSource, + }, +}; + +pub struct Smallvil { + pub start_time: std::time::Instant, + pub socket_name: OsString, + pub display_handle: DisplayHandle, + + pub space: Space, + pub loop_signal: LoopSignal, + + // Smithay State + pub compositor_state: CompositorState, + pub xdg_shell_state: XdgShellState, + pub shm_state: ShmState, + pub output_manager_state: OutputManagerState, + pub seat_state: SeatState, + pub data_device_state: DataDeviceState, + pub popups: PopupManager, + + pub seat: Seat, +} + +impl Smallvil { + pub fn new(event_loop: &mut EventLoop, display: Display) -> Self { + let start_time = std::time::Instant::now(); + + let dh = display.handle(); + + // Here we initialize implementations of some wayland protocols + // Some of them require us to implement traits on the Smallvil state, + // you can find those implementations in the `crate::handlers` module + + // Initialize protocols needed for displaying windows + let compositor_state = CompositorState::new::(&dh); + let xdg_shell_state = XdgShellState::new::(&dh); + let shm_state = ShmState::new::(&dh, vec![]); + let popups = PopupManager::default(); + + let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); + + // Data device is responsible for clipboard and drag-and-drop + let data_device_state = DataDeviceState::new::(&dh); + + // A seat is a group of keyboards, pointer and touch devices. + // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. + let mut seat_state = SeatState::new(); + let mut seat: Seat = seat_state.new_wl_seat(&dh, "winit"); + + // Notify clients that we have a keyboard, for the sake of the example we assume that keyboard is always present. + // You may want to track keyboard hot-plug in real compositor. + seat.add_keyboard(Default::default(), 200, 25).unwrap(); + + // Notify clients that we have a pointer (mouse) + // Here we assume that there is always pointer plugged in + seat.add_pointer(); + + // A space represents a two-dimensional plane. Windows and Outputs can be mapped onto it. + // + // Windows get a position and stacking order through mapping. + // Outputs become views of a part of the Space and can be rendered via Space::render_output. + let space = Space::default(); + + // Setup a wayland socket that will be used to accept clients + let socket_name = Self::init_wayland_listener(display, event_loop); + + // Get the loop signal, used to stop the event loop + let loop_signal = event_loop.get_signal(); + + Self { + start_time, + display_handle: dh, + + space, + loop_signal, + socket_name, + + compositor_state, + xdg_shell_state, + shm_state, + output_manager_state, + seat_state, + data_device_state, + popups, + seat, + } + } + + fn init_wayland_listener(display: Display, event_loop: &mut EventLoop) -> OsString { + // Creates a new listening socket, automatically choosing the next available `wayland` socket name. + let listening_socket = ListeningSocketSource::new_auto().unwrap(); + + // Get the name of the listening socket. + // Clients will connect to this socket. + let socket_name = listening_socket.socket_name().to_os_string(); + + let loop_handle = event_loop.handle(); + + loop_handle + .insert_source(listening_socket, move |client_stream, _, state| { + // Inside the callback, you should insert the client into the display. + // + // You may also associate some data with the client when inserting the client. + state + .display_handle + .insert_client(client_stream, Arc::new(ClientState::default())) + .unwrap(); + }) + .expect("Failed to init the wayland event source."); + + // You also need to add the display itself to the event loop, so that client events will be processed by wayland-server. + loop_handle + .insert_source( + Generic::new(display, Interest::READ, Mode::Level), + |_, display, state| { + // Safety: we don't drop the display + unsafe { + display.get_mut().dispatch_clients(state).unwrap(); + } + Ok(PostAction::Continue) + }, + ) + .unwrap(); + + socket_name + } + + pub fn surface_under(&self, pos: Point) -> Option<(WlSurface, Point)> { + self.space.element_under(pos).and_then(|(window, location)| { + window + .surface_under(pos - location.to_f64(), WindowSurfaceType::ALL) + .map(|(s, p)| (s, (p + location).to_f64())) + }) + } +} + +/// Data associated with a wayland client that connects to Smallvil. +/// One instance of this type per client. +#[derive(Default)] +pub struct ClientState { + pub compositor_state: CompositorClientState, +} + +impl ClientData for ClientState { + fn initialized(&self, _client_id: ClientId) {} + fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} +} diff --git a/vendor/smithay/smallvil/src/winit.rs b/vendor/smithay/smallvil/src/winit.rs new file mode 100644 index 0000000..190c155 --- /dev/null +++ b/vendor/smithay/smallvil/src/winit.rs @@ -0,0 +1,110 @@ +use std::time::Duration; + +use smithay::{ + backend::{ + renderer::{ + damage::OutputDamageTracker, element::surface::WaylandSurfaceRenderElement, gles::GlesRenderer, + }, + winit::{self, WinitEvent}, + }, + output::{Mode, Output, PhysicalProperties, Subpixel}, + reexports::calloop::EventLoop, + utils::{Rectangle, Transform}, +}; + +use crate::Smallvil; + +pub fn init_winit( + event_loop: &mut EventLoop, + state: &mut Smallvil, +) -> Result<(), Box> { + let (mut backend, winit) = winit::init()?; + + let mode = Mode { + size: backend.window_size(), + refresh: 60_000, + }; + + let output = Output::new( + "winit".to_string(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "Smithay".into(), + model: "Winit".into(), + serial_number: "Unknown".into(), + }, + ); + let _global = output.create_global::(&state.display_handle); + output.change_current_state(Some(mode), Some(Transform::Flipped180), None, Some((0, 0).into())); + output.set_preferred(mode); + + state.space.map_output(&output, (0, 0)); + + let mut damage_tracker = OutputDamageTracker::from_output(&output); + + event_loop.handle().insert_source(winit, move |event, _, state| { + match event { + WinitEvent::Resized { size, .. } => { + output.change_current_state( + Some(Mode { + size, + refresh: 60_000, + }), + None, + None, + None, + ); + } + WinitEvent::Input(event) => state.process_input_event(event), + WinitEvent::Redraw => { + let size = backend.window_size(); + let damage = Rectangle::from_size(size); + + { + let (renderer, mut framebuffer) = backend.bind().unwrap(); + smithay::desktop::space::render_output::< + _, + WaylandSurfaceRenderElement, + _, + _, + >( + &output, + renderer, + &mut framebuffer, + 1.0, + 0, + [&state.space], + &[], + &mut damage_tracker, + [0.1, 0.1, 0.1, 1.0], + ) + .unwrap(); + } + backend.submit(Some(&[damage])).unwrap(); + + state.space.elements().for_each(|window| { + window.send_frame( + &output, + state.start_time.elapsed(), + Some(Duration::ZERO), + |_, _| Some(output.clone()), + ) + }); + + state.space.refresh(); + state.popups.cleanup(); + let _ = state.display_handle.flush_clients(); + + // Ask for redraw to schedule new frame. + backend.window().request_redraw(); + } + WinitEvent::CloseRequested => { + state.loop_signal.stop(); + } + _ => (), + }; + })?; + + Ok(()) +} diff --git a/vendor/smithay/smithay-drm-extras/Cargo.toml b/vendor/smithay/smithay-drm-extras/Cargo.toml new file mode 100644 index 0000000..7f393fc --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "smithay-drm-extras" +version = "0.1.0" +edition = "2024" +license = "MIT" +authors = ["Bartłomiej Maryńczak "] + +[dependencies] +libdisplay-info = { version = "0.3.0", optional = true } +drm = { version = "0.14.0" } + +[features] +default = ["display-info"] +display-info = ["libdisplay-info"] + +[dev-dependencies.smithay] +path = "../" diff --git a/vendor/smithay/smithay-drm-extras/LICENSE b/vendor/smithay/smithay-drm-extras/LICENSE new file mode 100644 index 0000000..4040496 --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Bartłomiej Maryńczak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/smithay/smithay-drm-extras/README.md b/vendor/smithay/smithay-drm-extras/README.md new file mode 100644 index 0000000..07b9d23 --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/README.md @@ -0,0 +1,9 @@ +# Smithay DRM Extras + +This crate contains some extra abstractions and helpers over DRM + +- `display_info` module is responsible for extraction of information from DRM connectors (`model` and `manufacturer`) +- `drm_scanner` module contains helpers for detecting connector connected and disconnected events as well as mapping crtc to them. + - `ConnectorScanner` is responsible for tracking connected/disconnected events. + - `CrtcMapper` trait and `SimpleCrtcMapper` are meant for mapping crtc to connector. + - `DrmScanner` combines two above into single abstraction. If it does not fit your needs you can always drop down to using `ConnectScanner` alone. \ No newline at end of file diff --git a/vendor/smithay/smithay-drm-extras/examples/simple.rs b/vendor/smithay/smithay-drm-extras/examples/simple.rs new file mode 100644 index 0000000..9379ccc --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/examples/simple.rs @@ -0,0 +1,220 @@ +use std::{collections::HashMap, path::PathBuf, time::Duration}; + +use smithay::reexports::drm::control::{connector, crtc}; +use smithay_drm_extras::{ + display_info, + drm_scanner::{self, DrmScanEvent}, +}; + +use smithay::{ + backend::{ + drm::{self, DrmDeviceFd, DrmNode}, + session::{Session, libseat::LibSeatSession}, + udev::{UdevBackend, UdevEvent}, + }, + reexports::{ + calloop::{EventLoop, LoopHandle, timer::Timer}, + rustix::fs::OFlags, + }, + utils::DeviceFd, +}; + +struct State { + handle: LoopHandle<'static, Self>, + session: LibSeatSession, + devices: HashMap, +} + +struct Device { + drm: drm::DrmDevice, + drm_scanner: drm_scanner::DrmScanner, + surfaces: HashMap, +} + +#[derive(Clone)] +struct Surface { + // Your gbm surface stuff goes here +} + +fn main() -> Result<(), Box> { + let mut event_loop = EventLoop::::try_new()?; + + let (session, notify) = LibSeatSession::new().unwrap(); + + event_loop.handle().insert_source(notify, |_, _, _| {}).unwrap(); + + let mut state = State { + handle: event_loop.handle(), + session, + devices: Default::default(), + }; + + init_udev(&mut state); + + event_loop + .handle() + .insert_source(Timer::from_duration(Duration::from_secs(5)), |_, _, _| { + panic!("Aborted"); + }) + .unwrap(); + + event_loop.run(None, &mut state, |_data| {})?; + + Ok(()) +} + +fn init_udev(state: &mut State) { + let backend = UdevBackend::new(state.session.seat()).unwrap(); + for (device_id, path) in backend.device_list() { + state.on_udev_event(UdevEvent::Added { + device_id, + path: path.to_owned(), + }); + } + + state + .handle + .insert_source(backend, |event, _, state| state.on_udev_event(event)) + .unwrap(); +} + +impl State { + fn on_udev_event(&mut self, event: UdevEvent) { + match event { + UdevEvent::Added { device_id, path } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + self.device_added(node, path); + } + } + UdevEvent::Changed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + self.device_changed(node); + } + } + UdevEvent::Removed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + self.device_removed(node); + } + } + } + } + + fn device_added(&mut self, node: DrmNode, path: PathBuf) { + let fd = self + .session + .open( + &path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + ) + .unwrap(); + + let fd = DrmDeviceFd::new(DeviceFd::from(fd)); + + let (drm, drm_notifier) = drm::DrmDevice::new(fd, false).unwrap(); + + self.handle + .insert_source(drm_notifier, move |event, _, _| match event { + drm::DrmEvent::VBlank(_) => {} + drm::DrmEvent::Error(_) => {} + }) + .unwrap(); + + self.devices.insert( + node, + Device { + drm, + drm_scanner: Default::default(), + surfaces: Default::default(), + }, + ); + + self.device_changed(node); + } + + fn connector_connected(&mut self, node: DrmNode, connector: connector::Info, crtc: crtc::Handle) { + if let Some(device) = self.devices.get_mut(&node) { + let name = format!("{}-{}", connector.interface().as_str(), connector.interface_id()); + + let display_info = display_info::for_connector(&device.drm, connector.handle()); + + let manufacturer = display_info + .as_ref() + .and_then(|info| info.make()) + .unwrap_or_else(|| "Unknown".into()); + + let model = display_info + .as_ref() + .and_then(|info| info.model()) + .unwrap_or_else(|| "Unknown".into()); + + println!("Connected:"); + dbg!(name); + dbg!(manufacturer); + dbg!(model); + + device.surfaces.insert(crtc, Surface {}); + } + } + + fn connector_disconnected(&mut self, node: DrmNode, connector: connector::Info, crtc: crtc::Handle) { + let name = format!("{}-{}", connector.interface().as_str(), connector.interface_id()); + + println!("Disconnected:"); + dbg!(name); + + if let Some(device) = self.devices.get_mut(&node) { + device.surfaces.remove(&crtc); + } + } + + fn device_changed(&mut self, node: DrmNode) { + let device = if let Some(device) = self.devices.get_mut(&node) { + device + } else { + return; + }; + + for event in device + .drm_scanner + .scan_connectors(&device.drm) + .expect("failed to scan connectors") + { + match event { + DrmScanEvent::Connected { + connector, + crtc: Some(crtc), + } => { + self.connector_connected(node, connector, crtc); + } + DrmScanEvent::Disconnected { + connector, + crtc: Some(crtc), + } => { + self.connector_disconnected(node, connector, crtc); + } + DrmScanEvent::Changed { .. } => {} + _ => {} + } + } + } + + fn device_removed(&mut self, node: DrmNode) { + let device = if let Some(device) = self.devices.get_mut(&node) { + device + } else { + return; + }; + + let crtcs: Vec<_> = device + .drm_scanner + .crtcs() + .map(|(info, crtc)| (info.clone(), crtc)) + .collect(); + + for (connector, crtc) in crtcs { + self.connector_disconnected(node, connector, crtc); + } + + self.devices.remove(&node); + } +} diff --git a/vendor/smithay/smithay-drm-extras/src/display_info.rs b/vendor/smithay/smithay-drm-extras/src/display_info.rs new file mode 100644 index 0000000..ea76f87 --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/src/display_info.rs @@ -0,0 +1,37 @@ +//! # EDID - Extended Display Identification Data +//! +//! This module is meant to help with extraction of EDID data from connectors +//! +//! ```no_run +//! # mod helpers { include!("./docs/doctest_helpers.rs"); }; +//! # let drm_device: helpers::FakeDevice = todo!(); +//! # let connector = todo!(); +//! use smithay_drm_extras::display_info; +//! +//! let info = display_info::for_connector(&drm_device, connector).unwrap(); +//! +//! println!("Monitor name: {:?}", info.model()); +//! println!("Manufacturer name: {:?}", info.make()); +//! ``` + +use drm::control::{Device as ControlDevice, connector}; +use libdisplay_info::info::Info; + +/// Try to read the [`Info`] from the connector EDID property +pub fn for_connector(device: &impl ControlDevice, connector: connector::Handle) -> Option { + let props = device.get_properties(connector).ok()?; + + let (info, value) = props + .into_iter() + .filter_map(|(handle, value)| { + let info = device.get_property(handle).ok()?; + + Some((info, value)) + }) + .find(|(info, _)| info.name().to_str() == Ok("EDID"))?; + + let blob = info.value_type().convert_value(value).as_blob()?; + let data = device.get_property_blob(blob).ok()?; + + Info::parse_edid(&data).ok() +} diff --git a/vendor/smithay/smithay-drm-extras/src/docs/doctest_helpers.rs b/vendor/smithay/smithay-drm-extras/src/docs/doctest_helpers.rs new file mode 100644 index 0000000..a0717fd --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/src/docs/doctest_helpers.rs @@ -0,0 +1,8 @@ +pub struct FakeDevice; +impl std::os::unix::prelude::AsFd for FakeDevice { + fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> { + unimplemented!() + } +} +impl drm::Device for FakeDevice {} +impl drm::control::Device for FakeDevice {} diff --git a/vendor/smithay/smithay-drm-extras/src/drm_scanner.rs b/vendor/smithay/smithay-drm-extras/src/drm_scanner.rs new file mode 100644 index 0000000..5c01c59 --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/src/drm_scanner.rs @@ -0,0 +1,244 @@ +//! # Drm Scanner +//! +//! - [`ConnectorScanner`] is responsible for tracking connected/disconnected events. +//! - [`CrtcMapper`] trait and [`SimpleCrtcMapper`] are meant for mapping crtc to connector. +//! - [`DrmScanner`] combines two above into single abstraction. +//! If it does not fit your needs you can always drop down to using [`ConnectScanner`] alone. +//! +//! ### Example +//! ```no_run +//! # mod helpers { include!("./docs/doctest_helpers.rs"); }; +//! # let drm_device: helpers::FakeDevice = todo!(); +//! use smithay_drm_extras::drm_scanner::{DrmScanner, DrmScanEvent}; +//! +//! let mut scanner: DrmScanner = DrmScanner::new(); +//! +//! for event in scanner.scan_connectors(&drm_device).expect("failed to scan connectors") { +//! match event { +//! DrmScanEvent::Connected { .. } => {}, +//! DrmScanEvent::Disconnected { .. } => {}, +//! DrmScanEvent::Changed { .. } => {}, +//! } +//! } +//! ``` +use std::collections::HashMap; + +use drm::control::{Device as ControlDevice, connector, crtc}; + +mod connector_scanner; +pub use connector_scanner::{ConnectorScanEvent, ConnectorScanResult, ConnectorScanner}; + +mod crtc_mapper; +pub use crtc_mapper::{CrtcMapper, SimpleCrtcMapper}; + +/// Drm Scanner +/// +/// Wrapper over [`ConnectorScanner`] and [`CrtcMapper`] +#[derive(Debug, Default)] +pub struct DrmScanner +where + Mapper: CrtcMapper, +{ + connectors: ConnectorScanner, + crtc_mapper: Mapper, +} + +impl DrmScanner +where + M: CrtcMapper + Default, +{ + /// Create new DrmScanner with default CRTC mapper. + pub fn new() -> Self { + Self::new_with_mapper(Default::default()) + } +} + +impl DrmScanner +where + M: CrtcMapper, +{ + /// Create new DrmScanner with custom CRTC mapper + pub fn new_with_mapper(mapper: M) -> Self { + Self { + connectors: Default::default(), + crtc_mapper: mapper, + } + } + + /// [`CrtcMapper`] getter + pub fn crtc_mapper(&self) -> &M { + &self.crtc_mapper + } + + /// Muttable [`CrtcMapper`] getter + pub fn crtc_mapper_mut(&mut self) -> &mut M { + &mut self.crtc_mapper + } + + /// Scan connectors to find out what has changed since last call to this method. + /// + /// Returns [`DrmScanResult`] that contains added and removed connectors, + /// and CRTCs that got assigned to them. + /// + /// Should be called on every device changed event + /// + /// ```no_run + /// # mod helpers { include!("./docs/doctest_helpers.rs"); }; + /// # let drm_device: helpers::FakeDevice = todo!(); + /// use smithay_drm_extras::drm_scanner::{DrmScanner, DrmScanEvent}; + /// + /// let mut scanner: DrmScanner = DrmScanner::new(); + /// let res = scanner.scan_connectors(&drm_device).expect("failed to scan connectors"); + /// + /// // You can extract scan info manually + /// println!("Plugged {} connectors", res.connected.len()); + /// println!("Unplugged {} connectors", res.disconnected.len()); + /// + /// // Or simply iterate over it + /// for event in res { + /// match event { + /// DrmScanEvent::Connected { .. } => {}, + /// DrmScanEvent::Disconnected { .. } => {}, + /// DrmScanEvent::Changed { .. } => {}, + /// } + /// } + /// ``` + pub fn scan_connectors(&mut self, drm: &impl ControlDevice) -> std::io::Result { + let scan = self.connectors.scan(drm)?; + + let removed = scan + .disconnected + .into_iter() + .map(|info| { + let crtc = self.crtc_mapper.crtc_for_connector(&info.handle()); + (info, crtc) + }) + .collect(); + + self.crtc_mapper.map(drm, self.connectors.connectors().values()); + + let added = scan + .connected + .into_iter() + .map(|info| { + let crtc = self.crtc_mapper.crtc_for_connector(&info.handle()); + (info, crtc) + }) + .collect(); + + let changed = scan + .changed + .into_iter() + .map(|info| { + let crtc = self.crtc_mapper.crtc_for_connector(&info.handle()); + (info, crtc) + }) + .collect(); + + Ok(DrmScanResult { + disconnected: removed, + connected: added, + changed, + }) + } + + /// Get map of all connectors, connected and disconnected ones. + pub fn connectors(&self) -> &HashMap { + self.connectors.connectors() + } + + /// Get CRTC that is mapped to supplied connector + /// + /// This will query underlying [`CrtcMapper`] + pub fn crtc_for_connector(&self, connector: &connector::Handle) -> Option { + self.crtc_mapper.crtc_for_connector(connector) + } + + /// Get iterator over all `connector -> CRTC` mappings + pub fn crtcs(&self) -> impl Iterator { + self.connectors() + .iter() + .filter_map(|(handle, info)| Some((info, self.crtc_for_connector(handle)?))) + } +} + +type DrmScanItem = (connector::Info, Option); + +/// Result of [`DrmScanner::scan_connectors`] +/// +/// You can use `added` and `removed` fields of this result manually, +/// or you can just iterate (using [`IntoIterator`] or [`DrmScanResult::iter`]) +/// over this result to get [`DrmScanEvent`]. +#[derive(Debug, Default, Clone)] +pub struct DrmScanResult { + /// Connectors that got plugged in since last scan + pub connected: Vec, + /// Connectors that got unplugged since last scan + pub disconnected: Vec, + /// Connectors whose mode list changed while staying connected + pub changed: Vec, +} + +impl DrmScanResult { + /// Creates event iterator for this result + /// + /// Internally this clones the data so it is equivalent to [`IntoIterator`] + pub fn iter(&self) -> impl Iterator { + self.clone().into_iter() + } +} + +/// Created from [`DrmScanResult`], informs about connector events. +#[derive(Debug, Clone)] +pub enum DrmScanEvent { + /// A new connector got plugged in since last scan + Connected { + /// Info about connected connector + connector: connector::Info, + /// Crtc that got mapped to this connector + crtc: Option, + }, + /// A connector got unplugged in since last scan + Disconnected { + /// Info about disconnected connector + connector: connector::Info, + /// Crtc that is no longer mapped to this connector + crtc: Option, + }, + /// The connector's mode list changed while staying connected + Changed { + /// Info about the connector whose modes changed + connector: connector::Info, + /// Crtc that is mapped to this connector + crtc: Option, + }, +} + +impl DrmScanEvent { + fn connected((connector, crtc): (connector::Info, Option)) -> Self { + DrmScanEvent::Connected { connector, crtc } + } + + fn disconnected((connector, crtc): (connector::Info, Option)) -> Self { + DrmScanEvent::Disconnected { connector, crtc } + } + + fn changed((connector, crtc): (connector::Info, Option)) -> Self { + DrmScanEvent::Changed { connector, crtc } + } +} + +impl IntoIterator for DrmScanResult { + type Item = DrmScanEvent; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.disconnected + .into_iter() + .map(DrmScanEvent::disconnected) + .chain(self.connected.into_iter().map(DrmScanEvent::connected)) + .chain(self.changed.into_iter().map(DrmScanEvent::changed)) + .collect::>() + .into_iter() + } +} diff --git a/vendor/smithay/smithay-drm-extras/src/drm_scanner/connector_scanner.rs b/vendor/smithay/smithay-drm-extras/src/drm_scanner/connector_scanner.rs new file mode 100644 index 0000000..17cf2d7 --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/src/drm_scanner/connector_scanner.rs @@ -0,0 +1,153 @@ +use std::collections::{HashMap, HashSet}; + +use drm::control::{Device as ControlDevice, connector}; + +/// Responsible for tracking connected/disconnected events. +/// +/// ### Example +/// ```no_run +/// # mod helpers { include!("../docs/doctest_helpers.rs"); }; +/// # let drm_device: helpers::FakeDevice = todo!(); +/// use smithay_drm_extras::drm_scanner::{ConnectorScanner, ConnectorScanEvent}; +/// +/// let mut scanner = ConnectorScanner::new(); +/// +/// for event in scanner.scan(&drm_device).expect("failed to scan connectors") { +/// match event { +/// ConnectorScanEvent::Connected(conn) => {}, +/// ConnectorScanEvent::Disconnected(conn) => {}, +/// ConnectorScanEvent::Changed(conn) => {}, +/// } +/// } +#[derive(Debug, Default)] +pub struct ConnectorScanner { + connectors: HashMap, +} + +impl ConnectorScanner { + /// Create new [`ConnectorScanner`] + pub fn new() -> Self { + Default::default() + } + + /// Should be called on every device changed event + pub fn scan(&mut self, drm: &impl ControlDevice) -> std::io::Result { + let res_handles = drm.resource_handles()?; + let connector_handles = res_handles.connectors(); + + let mut added = Vec::new(); + let mut removed = Vec::new(); + let mut changed = Vec::new(); + + for conn in connector_handles + .iter() + .filter_map(|conn| drm.get_connector(*conn, true).ok()) + { + let curr_state = conn.state(); + + use connector::State; + if let Some(old) = self.connectors.insert(conn.handle(), conn.clone()) { + match (old.state(), curr_state) { + (State::Connected, State::Disconnected) => removed.push(conn), + (State::Disconnected | State::Unknown, State::Connected) => added.push(conn), + // + // Emit Changed when the mode list changes while staying + // connected. This covers the EDID race where the kernel + // initially reports a connector as Connected with an + // empty or fallback mode list (EDID not yet read) and a + // later rescan finds the real modes populated. + (State::Connected, State::Connected) => { + if old.modes() != conn.modes() { + changed.push(conn); + } + } + (State::Disconnected, State::Disconnected) => {} + // + (State::Unknown, _) => {} + (_, State::Unknown) => {} + } + } else if curr_state == State::Connected { + added.push(conn) + } + } + + // Check for connectors that disappeared entirely (e.g., USB-C dock disconnected while + // suspended). These won't be in resource_handles() anymore, so handle them specially. + let connector_handles_set: HashSet<_> = connector_handles.iter().copied().collect(); + let to_remove: Vec<_> = self + .connectors + .keys() + .filter(|handle| !connector_handles_set.contains(handle)) + .copied() + .collect(); + + for handle in to_remove { + if let Some(info) = self.connectors.remove(&handle) { + if info.state() == connector::State::Connected { + removed.push(info); + } + } + } + + Ok(ConnectorScanResult { + connected: added, + disconnected: removed, + changed, + }) + } + + /// Get map of all connectors, connected and disconnected ones. + pub fn connectors(&self) -> &HashMap { + &self.connectors + } +} + +/// Result of [`ConnectorScanner::scan`] +/// +/// You can use `added` and `removed` fields of this result manually, +/// or you can just iterate (using [`IntoIterator`] or [`ConnectorScanResult::iter`]) +/// over this result to get [`ConnectorScanEvent`]. +#[derive(Debug, Default, Clone)] +pub struct ConnectorScanResult { + /// Connectors that got plugged in since last scan + pub connected: Vec, + /// Connectors that got unplugged since last scan + pub disconnected: Vec, + /// Connectors whose mode list changed while staying connected + pub changed: Vec, +} + +/// Created from [`ConnectorScanResult`], informs about connector events. +#[derive(Debug, Clone)] +pub enum ConnectorScanEvent { + /// A new connector got plugged in since last scan + Connected(connector::Info), + /// A connector got unplugged in since last scan + Disconnected(connector::Info), + /// The connector's mode list changed while staying connected + Changed(connector::Info), +} + +impl ConnectorScanResult { + /// Creates event iterator for this result + /// + /// Internally this clones the data so it is equivalent to [`IntoIterator`] + pub fn iter(&self) -> impl Iterator { + self.clone().into_iter() + } +} + +impl IntoIterator for ConnectorScanResult { + type Item = ConnectorScanEvent; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.disconnected + .into_iter() + .map(ConnectorScanEvent::Disconnected) + .chain(self.connected.into_iter().map(ConnectorScanEvent::Connected)) + .chain(self.changed.into_iter().map(ConnectorScanEvent::Changed)) + .collect::>() + .into_iter() + } +} diff --git a/vendor/smithay/smithay-drm-extras/src/drm_scanner/crtc_mapper.rs b/vendor/smithay/smithay-drm-extras/src/drm_scanner/crtc_mapper.rs new file mode 100644 index 0000000..af8bc73 --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/src/drm_scanner/crtc_mapper.rs @@ -0,0 +1,126 @@ +use std::collections::{HashMap, HashSet}; + +use drm::control::{Device as ControlDevice, connector, crtc}; + +/// CRTC Mapper trait +/// +/// It exists to allow custom mappers in [`super::DrmScanner`]. +/// +/// It is responsible for mapping CRTCs to connectors. +/// For each connector it has to pick suitable CRTC. +pub trait CrtcMapper { + /// Request mapping of CRTCs to supplied connectors + /// + /// Usually called in response to udev device changed event, + /// or on device init. + fn map<'a>( + &mut self, + drm: &impl ControlDevice, + connectors: impl Iterator + Clone, + ); + + /// Query CRTC mapped to supplied connector + fn crtc_for_connector(&self, connector: &connector::Handle) -> Option; +} + +/// Simple CRTC Mapper +/// +/// This is basic mapper that simply chooses one CRTC for every connector. +/// +/// It is also capable of recovering mappings that were used by display manger or tty +/// before the compositor was started up. +#[derive(Debug, Default)] +pub struct SimpleCrtcMapper { + crtcs: HashMap, +} + +impl SimpleCrtcMapper { + /// Create new [`SimpleCrtcMapper`] + pub fn new() -> Self { + Self::default() + } + + fn is_taken(&self, crtc: &crtc::Handle) -> bool { + self.crtcs.values().any(|v| v == crtc) + } + + fn is_available(&self, crtc: &crtc::Handle) -> bool { + !self.is_taken(crtc) + } + + fn restored_for_connector( + &self, + drm: &impl ControlDevice, + connector: &connector::Info, + ) -> Option { + let encoder = connector.current_encoder()?; + let encoder = drm.get_encoder(encoder).ok()?; + let crtc = encoder.crtc()?; + + self.is_available(&crtc).then_some(crtc) + } + + fn pick_next_available_for_connector( + &self, + drm: &impl ControlDevice, + connector: &connector::Info, + ) -> Option { + let res_handles = drm.resource_handles().ok()?; + + connector + .encoders() + .iter() + .flat_map(|encoder_handle| drm.get_encoder(*encoder_handle)) + .find_map(|encoder_info| { + res_handles + .filter_crtcs(encoder_info.possible_crtcs()) + .into_iter() + .find(|crtc| self.is_available(crtc)) + }) + } +} + +impl super::CrtcMapper for SimpleCrtcMapper { + fn map<'a>( + &mut self, + drm: &impl ControlDevice, + connectors: impl Iterator + Clone, + ) { + // Release CRTC reservations for every connector that is not currently connected. This + // covers connectors that are present but disconnected, as well as connectors that have + // vanished from the resource list entirely (e.g. DP-MST sink connectors when a dock is + // unplugged or the system is suspended). + let connected: HashSet = connectors + .clone() + .filter(|conn| conn.state() == connector::State::Connected) + .map(|conn| conn.handle()) + .collect(); + self.crtcs.retain(|handle, _| connected.contains(handle)); + + let mut needs_crtc: Vec<&connector::Info> = connectors + .filter(|conn| conn.state() == connector::State::Connected) + .filter(|conn| !self.crtcs.contains_key(&conn.handle())) + .collect(); + + needs_crtc.retain(|connector| { + if let Some(crtc) = self.restored_for_connector(drm, connector) { + self.crtcs.insert(connector.handle(), crtc); + + // This connector no longer needs crtc so let's remove it + false + } else { + true + } + }); + + for connector in needs_crtc { + if let Some(crtc) = self.pick_next_available_for_connector(drm, connector) { + self.crtcs.insert(connector.handle(), crtc); + } + } + } + + fn crtc_for_connector(&self, connector: &connector::Handle) -> Option { + self.crtcs.get(connector).copied() + } +} diff --git a/vendor/smithay/smithay-drm-extras/src/lib.rs b/vendor/smithay/smithay-drm-extras/src/lib.rs new file mode 100644 index 0000000..3cea25a --- /dev/null +++ b/vendor/smithay/smithay-drm-extras/src/lib.rs @@ -0,0 +1,16 @@ +//! # Smithay DRM Extras +//! +//! This crate contains some extra abstractions and helpers over DRM +//! +//! - [`display_info`] is responsible for extraction of information from DRM connectors +//! - [`drm_scanner`] is responsible for detecting connector connected and +//! disconnected events, as well as mapping CRTC to them. +//! +//! ### Features +//! - `display_info` - If enabled `display_info` functionality is enabled through `libdisplay-info` integration + +#![warn(missing_docs, missing_debug_implementations)] + +#[cfg(feature = "display-info")] +pub mod display_info; +pub mod drm_scanner; diff --git a/vendor/smithay/src/backend/allocator/dmabuf.rs b/vendor/smithay/src/backend/allocator/dmabuf.rs new file mode 100644 index 0000000..405e544 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/dmabuf.rs @@ -0,0 +1,715 @@ +//! Module for [dmabuf](https://docs.kernel.org/driver-api/dma-buf.html) buffers. +//! +//! `Dmabuf`s act alike to smart pointers and can be freely cloned and passed around. +//! Once the last `Dmabuf` reference is dropped, its file descriptor is closed and +//! underlying resources are freed. +//! +//! If you want to hold on to a potentially alive dmabuf without blocking the free up +//! of the underlying resources, you may `downgrade` a `Dmabuf` reference to a `WeakDmabuf`. +//! +//! This can be especially useful in resources where other parts of the stack should decide upon +//! the lifetime of the buffer. E.g. when you are only caching associated resources for a dmabuf. + +use calloop::generic::Generic; +use calloop::{EventSource, Interest, Mode, PostAction}; +use rustix::ioctl::Setter; + +use super::{Allocator, Buffer, Format, Fourcc, Modifier}; +#[cfg(feature = "backend_drm")] +use crate::backend::drm::DrmNode; +use crate::utils::{Buffer as BufferCoords, Size}; +#[cfg(feature = "wayland_frontend")] +use crate::wayland::compositor::{Blocker, BlockerState}; +use std::hash::{Hash, Hasher}; +use std::os::unix::io::{AsFd, BorrowedFd, OwnedFd}; +#[cfg(feature = "backend_drm")] +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; +use std::{error, fmt}; + +/// Maximum amount of planes this implementation supports +pub const MAX_PLANES: usize = 4; + +#[derive(Debug)] +pub(crate) struct DmabufInternal { + /// The submitted planes + pub planes: Vec, + /// The size of this buffer + pub size: Size, + /// The format in use + pub format: Fourcc, + /// Format modifier + pub modifier: Modifier, + /// The flags applied to it + /// + /// This is a bitflag, to be compared with the `Flags` enum re-exported by this module. + pub flags: DmabufFlags, + /// Presumably compatible device for buffer import + /// + /// This is inferred from client apis, however there is no kernel api or guarantee this is correct + #[cfg(feature = "backend_drm")] + node: Mutex>, +} + +#[derive(Debug)] +pub(crate) struct Plane { + pub fd: Arc, + /// The plane index + #[allow(dead_code)] + pub plane_idx: u32, + /// Offset from the start of the Fd + pub offset: u32, + /// Stride for this plane + pub stride: u32, +} + +bitflags::bitflags! { + /// Possible flags for a DMA buffer + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct DmabufFlags: u32 { + /// The buffer content is Y-inverted + const Y_INVERT = 1; + /// The buffer content is interlaced + const INTERLACED = 2; + /// The buffer content if interlaced is bottom-field first + const BOTTOM_FIRST = 4; + } +} + +#[derive(Debug, Clone)] +/// Strong reference to a dmabuf handle +pub struct Dmabuf(pub(crate) Arc); + +#[derive(Debug, Clone)] +/// Weak reference to a dmabuf handle +pub struct WeakDmabuf(pub(crate) Weak); + +impl PartialEq for Dmabuf { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} +impl Eq for Dmabuf {} + +impl PartialEq for WeakDmabuf { + #[inline] + fn eq(&self, other: &Self) -> bool { + Weak::ptr_eq(&self.0, &other.0) + } +} +impl Eq for WeakDmabuf {} + +impl Hash for Dmabuf { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.0).hash(state) + } +} +impl Hash for WeakDmabuf { + #[inline] + fn hash(&self, state: &mut H) { + self.0.as_ptr().hash(state) + } +} + +impl Buffer for Dmabuf { + #[inline] + fn size(&self) -> Size { + self.0.size + } + + #[inline] + fn format(&self) -> Format { + Format { + code: self.0.format, + modifier: self.0.modifier, + } + } +} + +/// Builder for Dmabufs +#[derive(Debug)] +pub struct DmabufBuilder { + internal: DmabufInternal, +} + +impl DmabufBuilder { + /// Add a plane to the constructed Dmabuf + /// + /// *Note*: Each Dmabuf needs at least one plane. + /// MAX_PLANES notes the maximum amount of planes any format may use with this implementation. + pub fn add_plane(&mut self, fd: impl Into>, offset: u32, stride: u32) -> bool { + if self.internal.planes.len() == MAX_PLANES { + return false; + } + self.internal.planes.push(Plane { + fd: fd.into(), + plane_idx: self.internal.planes.len() as u32, + offset, + stride, + }); + + true + } + + /// Sets a known good node to import the dmabuf with. + /// + /// While this is only a strong hint and no guarantee, implementations + /// should avoid setting this at all, if they can't be reasonably certain. + #[cfg(feature = "backend_drm")] + pub fn set_node(&mut self, node: DrmNode) { + self.internal.node = Mutex::new(Some(node)); + } + + /// Build a `Dmabuf` out of the provided parameters and planes + /// + /// Returns `None` if the builder has no planes attached. + pub fn build(self) -> Option { + if self.internal.planes.is_empty() { + return None; + } + + Some(Dmabuf(Arc::new(self.internal))) + } +} + +impl Dmabuf { + /// Create a new Dmabuf by initializing with values from an existing buffer + /// + // Note: the `src` Buffer is only used a reference for size and format. + // The contents are determined by the provided file descriptors, which + // do not need to refer to the same buffer `src` does. + pub fn builder_from_buffer(src: &impl Buffer, flags: DmabufFlags) -> DmabufBuilder { + Self::builder(src.size(), src.format().code, src.format().modifier, flags) + } + + /// Create a new Dmabuf builder + pub fn builder( + size: impl Into>, + format: Fourcc, + modifier: Modifier, + flags: DmabufFlags, + ) -> DmabufBuilder { + DmabufBuilder { + internal: DmabufInternal { + planes: Vec::with_capacity(MAX_PLANES), + size: size.into(), + format, + modifier, + flags, + #[cfg(feature = "backend_drm")] + node: Mutex::new(None), + }, + } + } + + /// The amount of planes this Dmabuf has + pub fn num_planes(&self) -> usize { + self.0.planes.len() + } + + /// Returns raw handles of the planes of this buffer + pub fn handles(&self) -> impl Iterator> + '_ { + self.0.planes.iter().map(|p| p.fd.as_fd()) + } + + /// Returns offsets for the planes of this buffer + pub fn offsets(&self) -> impl Iterator + '_ { + self.0.planes.iter().map(|p| p.offset) + } + + /// Returns strides for the planes of this buffer + pub fn strides(&self) -> impl Iterator + '_ { + self.0.planes.iter().map(|p| p.stride) + } + + /// Returns if this buffer format has any vendor-specific modifiers set or is implicit/linear + pub fn has_modifier(&self) -> bool { + self.0.modifier != Modifier::Invalid && self.0.modifier != Modifier::Linear + } + + /// Returns the flags stored on this buffer without interpreting them. + /// + /// Bits unknown to this Smithay version are retained. + pub fn flags(&self) -> DmabufFlags { + self.0.flags + } + + /// Returns if the buffer is stored inverted on the y-axis + pub fn y_inverted(&self) -> bool { + self.0.flags.contains(DmabufFlags::Y_INVERT) + } + + /// Create a weak reference to this dmabuf + pub fn weak(&self) -> WeakDmabuf { + WeakDmabuf(Arc::downgrade(&self.0)) + } + + /// Presumably compatible device for buffer import + /// + /// This is inferred from client apis, as there is no kernel api or other guarantee this is correct, + /// so it should only be treated as a hint. + #[cfg(feature = "backend_drm")] + pub fn node(&self) -> Option { + *self.0.node.lock().unwrap() + } + + /// Sets or unsets any node set for this dmabuf (see [`Dmabuf::node`]). + /// + /// May alter behavior of other parts of smithay using this as a hint. + #[cfg(feature = "backend_drm")] + pub fn set_node(&self, node: impl Into>) { + *self.0.node.lock().unwrap() = node.into(); + } + + /// Create an [`calloop::EventSource`] and [`Blocker`] for this [`Dmabuf`]. + /// + /// Usually used to block applying surface state on the readiness of an attached dmabuf. + #[cfg(feature = "wayland_frontend")] + #[profiling::function] + pub fn generate_blocker( + &self, + interest: Interest, + ) -> Result<(DmabufBlocker, DmabufSource), AlreadyReady> { + let source = DmabufSource::new(self.clone(), interest)?; + let blocker = DmabufBlocker(source.signal.clone()); + Ok((blocker, source)) + } + + /// Map the plane at specified index with the specified mode + /// + /// Returns `Err` if the plane with the specified index does not exist or + /// mmap failed + pub fn map_plane( + &self, + idx: usize, + mode: DmabufMappingMode, + ) -> Result { + let plane = self + .0 + .planes + .get(idx) + .ok_or(DmabufMappingFailed::PlaneIndexOutOfBound)?; + + let size = rustix::fs::seek(&plane.fd, rustix::fs::SeekFrom::End(0)).map_err(std::io::Error::from)?; + rustix::fs::seek(&plane.fd, rustix::fs::SeekFrom::Start(0)).map_err(std::io::Error::from)?; + + let len = (size - plane.offset as u64) as usize; + let ptr = unsafe { + rustix::mm::mmap( + std::ptr::null_mut(), + len, + mode.into(), + rustix::mm::MapFlags::SHARED, + &plane.fd, + plane.offset as u64, + ) + } + .map_err(std::io::Error::from)?; + Ok(DmabufMapping { len, ptr }) + } + + /// Synchronize access for the plane at the specified index + /// + /// Returns `Err` if the plane with the specified index does not exist or + /// the dmabuf_sync ioctl failed + pub fn sync_plane(&self, idx: usize, flags: DmabufSyncFlags) -> Result<(), DmabufSyncFailed> { + let plane = self + .0 + .planes + .get(idx) + .ok_or(DmabufSyncFailed::PlaneIndexOutOfBound)?; + unsafe { rustix::ioctl::ioctl(&plane.fd, Setter::::new(dma_buf_sync { flags })) } + .map_err(std::io::Error::from)?; + Ok(()) + } +} + +bitflags::bitflags! { + /// Modes of mapping a dmabuf plane + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct DmabufMappingMode: u32 { + /// Map the dmabuf readable + const READ = 0b00000001; + /// Map the dmabuf writable + const WRITE = 0b00000010; + } +} + +impl From for rustix::mm::ProtFlags { + #[inline] + fn from(mode: DmabufMappingMode) -> Self { + let mut flags = rustix::mm::ProtFlags::empty(); + + if mode.contains(DmabufMappingMode::READ) { + flags |= rustix::mm::ProtFlags::READ; + } + + if mode.contains(DmabufMappingMode::WRITE) { + flags |= rustix::mm::ProtFlags::WRITE; + } + + flags + } +} + +/// Dmabuf mapping errors +#[derive(Debug, thiserror::Error)] +#[error("Mapping the dmabuf failed")] +pub enum DmabufMappingFailed { + /// The supplied index for the plane is out of bounds + #[error("The supplied index for the plane is out of bounds")] + PlaneIndexOutOfBound, + /// Io error during map operation + Io(#[from] std::io::Error), +} + +/// Dmabuf sync errors +#[derive(Debug, thiserror::Error)] +#[error("Sync of the dmabuf failed")] +pub enum DmabufSyncFailed { + /// The supplied index for the plane is out of bounds + #[error("The supplied index for the plane is out of bounds")] + PlaneIndexOutOfBound, + /// Io error during sync operation + Io(#[from] std::io::Error), +} + +bitflags::bitflags! { + /// Flags for the [`Dmabuf::sync_plane`](Dmabuf::sync_plane) operation + #[derive(Copy, Clone)] + pub struct DmabufSyncFlags: std::ffi::c_ulonglong { + /// Read from the dmabuf + const READ = 1 << 0; + /// Write to the dmabuf + #[allow(clippy::identity_op)] + const WRITE = 2 << 0; + /// Start of read/write + const START = 0 << 2; + /// End of read/write + const END = 1 << 2; + } +} + +#[repr(C)] +#[allow(non_camel_case_types)] +struct dma_buf_sync { + flags: DmabufSyncFlags, +} + +const DMA_BUF_SYNC: rustix::ioctl::Opcode = rustix::ioctl::opcode::write::(b'b', 0); + +/// A mapping into a [`Dmabuf`] +#[derive(Debug)] +pub struct DmabufMapping { + ptr: *mut std::ffi::c_void, + len: usize, +} + +impl DmabufMapping { + /// Access the raw pointer of the mapping + pub fn ptr(&self) -> *mut std::ffi::c_void { + self.ptr + } + + /// Access the length of the mapping + pub fn length(&self) -> usize { + self.len + } +} + +// SAFETY: The caller is responsible for accessing the data without assuming +// another process isn't mutating it, regardless of how many threads this is +// referenced in. +unsafe impl Send for DmabufMapping {} +unsafe impl Sync for DmabufMapping {} + +impl Drop for DmabufMapping { + fn drop(&mut self) { + let _ = unsafe { rustix::mm::munmap(self.ptr, self.len) }; + } +} + +impl WeakDmabuf { + /// Try to upgrade to a strong reference of this buffer. + /// + /// Fails if no strong references exist anymore and the handle was already closed. + #[inline] + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(Dmabuf) + } + + /// Returns true if there are not any strong references remaining + #[inline] + pub fn is_gone(&self) -> bool { + self.0.strong_count() == 0 + } +} + +/// Buffer that can be exported as Dmabufs +pub trait AsDmabuf { + /// Error type returned, if exporting fails + type Error: std::error::Error; + + /// Export this buffer as a new Dmabuf + fn export(&self) -> Result; +} + +impl AsDmabuf for Dmabuf { + type Error = std::convert::Infallible; + + #[inline] + fn export(&self) -> Result { + Ok(self.clone()) + } +} + +/// Type erased error +#[derive(Debug)] +pub struct AnyError(Box); + +impl fmt::Display for AnyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl error::Error for AnyError { + #[inline] + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(&*self.0) + } +} + +/// Wrapper for Allocators, whose buffer types implement [`AsDmabuf`]. +/// +/// Implements `Allocator` +#[derive(Debug)] +pub struct DmabufAllocator(pub A) +where + A: Allocator, + ::Buffer: AsDmabuf + 'static, + ::Error: 'static; + +impl Allocator for DmabufAllocator +where + A: Allocator, + ::Buffer: AsDmabuf + 'static, + ::Error: Send + Sync + 'static, + <::Buffer as AsDmabuf>::Error: Send + Sync + 'static, +{ + type Buffer = Dmabuf; + type Error = AnyError; + + #[profiling::function] + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result { + self.0 + .create_buffer(width, height, fourcc, modifiers) + .map_err(|err| AnyError(err.into())) + .and_then(|b| AsDmabuf::export(&b).map_err(|err| AnyError(err.into()))) + } +} + +/// [`Blocker`] implementation for an accompaning [`DmabufSource`] +#[cfg(feature = "wayland_frontend")] +#[derive(Debug)] +pub struct DmabufBlocker(Arc); + +#[cfg(feature = "wayland_frontend")] +impl Blocker for DmabufBlocker { + fn state(&self) -> BlockerState { + if self.0.load(Ordering::SeqCst) { + BlockerState::Released + } else { + BlockerState::Pending + } + } +} + +#[derive(Debug)] +enum Subsource { + Active(Generic, std::io::Error>), + Done(Generic, std::io::Error>), + Empty, +} + +impl Subsource { + fn done(&mut self) { + let mut this = Subsource::Empty; + std::mem::swap(self, &mut this); + match this { + Subsource::Done(source) | Subsource::Active(source) => { + *self = Subsource::Done(source); + } + _ => {} + } + } +} + +/// [`Dmabuf`]-based event source. Can be used to monitor implicit fences of a dmabuf. +#[derive(Debug)] +pub struct DmabufSource { + dmabuf: Dmabuf, + signal: Arc, + sources: [Subsource; 4], +} + +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[error("Dmabuf is already ready for the given interest")] +/// Dmabuf is already ready for the given interest +pub struct AlreadyReady; + +impl DmabufSource { + /// Creates a new [`DmabufSource`] from a [`Dmabuf`] and interest. + /// + /// This event source will monitor the implicit fences of the given dmabuf. + /// Monitoring for READ-access will monitor the state of the most recent write or exclusive fence. + /// Monitoring for WRITE-access, will monitor state of all attached fences, shared and exclusive ones. + /// + /// The event source is a one shot event source and will remove itself from the event loop after being triggered once. + /// To monitor for new fences added at a later time a new DmabufSource needs to be created. + /// + /// Returns `AlreadyReady` if all corresponding fences are already signalled or if `interest` is empty. + #[profiling::function] + pub fn new(dmabuf: Dmabuf, interest: Interest) -> Result { + if !interest.readable && !interest.writable { + return Err(AlreadyReady); + } + + let mut sources = [ + Subsource::Empty, + Subsource::Empty, + Subsource::Empty, + Subsource::Empty, + ]; + for (idx, handle) in dmabuf.handles().enumerate() { + if matches!( + rustix::event::poll( + &mut [rustix::event::PollFd::new( + &handle, + if interest.writable { + rustix::event::PollFlags::OUT + } else { + rustix::event::PollFlags::IN + }, + )], + Some(&rustix::time::Timespec { + tv_sec: 0, + tv_nsec: 0 + }) + ), + Ok(1) + ) { + continue; + } + let fd = dmabuf.0.planes[idx].fd.clone(); + sources[idx] = Subsource::Active(Generic::new(fd, interest, Mode::OneShot)); + } + if sources + .iter() + .all(|x| matches!(x, Subsource::Done(_) | Subsource::Empty)) + { + Err(AlreadyReady) + } else { + Ok(DmabufSource { + dmabuf, + sources, + signal: Arc::new(AtomicBool::new(false)), + }) + } + } +} + +impl EventSource for DmabufSource { + type Event = (); + type Metadata = Dmabuf; + type Ret = Result<(), std::io::Error>; + + type Error = std::io::Error; + + #[profiling::function] + fn process_events( + &mut self, + readiness: calloop::Readiness, + token: calloop::Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + for i in 0..4 { + if let Ok(PostAction::Remove) = if let Subsource::Active(source) = &mut self.sources[i] { + // luckily Generic skips events for other tokens + source.process_events(readiness, token, |_, _| Ok(PostAction::Remove)) + } else { + Ok(PostAction::Continue) + } { + self.sources[i].done(); + } + } + + if self + .sources + .iter() + .all(|x| matches!(x, Subsource::Done(_) | Subsource::Empty)) + { + self.signal.store(true, Ordering::SeqCst); + callback((), &mut self.dmabuf)?; + Ok(PostAction::Remove) + } else { + Ok(PostAction::Reregister) + } + } + + fn register( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + for source in self.sources.iter_mut().filter_map(|source| match source { + Subsource::Active(source) => Some(source), + _ => None, + }) { + source.register(poll, token_factory)?; + } + Ok(()) + } + + fn reregister( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + for source in self.sources.iter_mut() { + match source { + Subsource::Active(source) => source.reregister(poll, token_factory)?, + Subsource::Done(source) => { + let _ = source.unregister(poll); + } + _ => {} + } + } + Ok(()) + } + + fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { + for source in self.sources.iter_mut() { + match source { + Subsource::Active(source) => source.unregister(poll)?, + Subsource::Done(source) => { + let _ = source.unregister(poll); + } + _ => {} + } + } + Ok(()) + } +} diff --git a/vendor/smithay/src/backend/allocator/dumb.rs b/vendor/smithay/src/backend/allocator/dumb.rs new file mode 100644 index 0000000..8239705 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/dumb.rs @@ -0,0 +1,130 @@ +//! Module for [DumbBuffer](https://docs.kernel.org/gpu/drm-kms.html#dumb-buffer-objects) buffers + +use std::fmt; +use std::io; + +use drm::buffer::Buffer as DrmBuffer; +use drm::control::{Device as ControlDevice, dumbbuffer::DumbBuffer as Handle}; +use tracing::instrument; + +use super::dmabuf::{AsDmabuf, Dmabuf, DmabufFlags}; +use super::{Allocator, Buffer, Format, Fourcc, Modifier, format::get_bpp}; +use crate::backend::drm::DrmDeviceFd; +use crate::backend::drm::DrmNode; +use crate::utils::{Buffer as BufferCoords, Size}; + +/// Wrapper around raw DumbBuffer handles. +pub struct DumbBuffer { + fd: DrmDeviceFd, + handle: Handle, + format: Format, +} + +impl fmt::Debug for DumbBuffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DumbBuffer") + .field("handle", &self.handle) + .field("format", &self.format) + .finish() + } +} + +/// Light wrapper around an [`DrmDeviceFd`] to implement the [`Allocator`]-trait +#[derive(Debug, Clone)] +pub struct DumbAllocator { + fd: DrmDeviceFd, +} + +impl DumbAllocator { + /// Create a new [`DumbAllocator`] from a [`DrmDeviceFd`]. + pub fn new(fd: DrmDeviceFd) -> Self { + DumbAllocator { fd } + } +} + +impl Allocator for DumbAllocator { + type Buffer = DumbBuffer; + type Error = io::Error; + + #[instrument(level = "trace", err)] + #[profiling::function] + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result { + // dumb buffers are always linear + if modifiers + .iter() + .all(|&x| x != Modifier::Invalid && x != Modifier::Linear) + { + return Err(rustix::io::Errno::INVAL.into()); + } + + let handle = self.fd.create_dumb_buffer( + (width, height), + fourcc, + get_bpp(fourcc).ok_or(rustix::io::Errno::INVAL)? as u32, + )?; + + Ok(DumbBuffer { + fd: self.fd.clone(), + handle, + format: Format { + code: fourcc, + modifier: Modifier::Linear, + }, + }) + } +} + +impl Buffer for DumbBuffer { + fn size(&self) -> Size { + let (w, h) = self.handle.size(); + (w as i32, h as i32).into() + } + + fn format(&self) -> Format { + self.format + } +} + +impl DumbBuffer { + /// Raw handle to the underlying buffer. + /// + /// Note: This handle will become invalid, once the `DumbBuffer` wrapper is dropped + /// or the device used to create is closed. Do not copy this handle and assume it keeps being valid. + pub fn handle(&self) -> &Handle { + &self.handle + } +} + +impl AsDmabuf for DumbBuffer { + type Error = io::Error; + + #[profiling::function] + fn export(&self) -> Result { + let fd = self + .fd + .buffer_to_prime_fd(self.handle.handle(), drm::CLOEXEC | drm::RDWR)?; + let mut builder = Dmabuf::builder( + self.size(), + self.format.code, + self.format.modifier, + DmabufFlags::empty(), + ); + builder.add_plane(fd, 0, self.handle.pitch()); + if let Ok(node) = DrmNode::from_file(&self.fd) { + builder.set_node(node); + } + builder.build().ok_or(rustix::io::Errno::INVAL.into()) + } +} + +impl Drop for DumbBuffer { + fn drop(&mut self) { + let _ = self.fd.destroy_dumb_buffer(self.handle); + } +} diff --git a/vendor/smithay/src/backend/allocator/format.rs b/vendor/smithay/src/backend/allocator/format.rs new file mode 100644 index 0000000..dba4356 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/format.rs @@ -0,0 +1,611 @@ +//! Format info tables for DRM formats. +//! +//! This module provides three functions, [`get_opaque`], [`has_alpha`] and [`get_bpp`]. +//! +//! [`get_opaque`] returns the opaque alternative of a DRM format with an alpha channel. +//! +//! ``` +//! # use smithay::backend::allocator::Fourcc; +//! # use smithay::backend::allocator::format::get_opaque; +//! assert_eq!(Some(Fourcc::Xrgb8888), get_opaque(Fourcc::Argb8888)); +//! ``` +//! +//! [`has_alpha`] returns true if the format has an alpha channel. +//! +//! ``` +//! # use smithay::backend::allocator::Fourcc; +//! # use smithay::backend::allocator::format::has_alpha; +//! assert!(has_alpha(Fourcc::Argb8888)); +//! assert!(!has_alpha(Fourcc::Xrgb8888)); +//! ``` +//! +//! [`get_bpp`] returns the number of bits per pixel of a format. +//! +//! ``` +//! # use smithay::backend::allocator::Fourcc; +//! # use smithay::backend::allocator::format::get_bpp; +//! assert_eq!(get_bpp(Fourcc::Argb8888), Some(32)); +//! ``` +//! +//! [`get_depth`] returns the number of used bits per pixel of a format +//! (excluding padding or non-alpha "X" parts of the format). +//! +//! ``` +//! # use smithay::backend::allocator::Fourcc; +//! # use smithay::backend::allocator::format::get_depth; +//! assert_eq!(get_depth(Fourcc::Argb8888), Some(32)); +//! assert_eq!(get_depth(Fourcc::Xrgb8888), Some(24)); +//! ``` + +use std::sync::Arc; + +use super::Format; +use indexmap::IndexSet; + +/// Macro to generate table lookup functions for formats. +/// +/// See the module documentation for usage details. +macro_rules! format_tables { + ( + $($fourcc: ident { + $(opaque: $opaque: ident,)? + alpha: $alpha: expr, + bpp: $bpp: expr, + depth: $depth: expr $(,)? + }),* + ) => { + /// Returns the opaque alternative of the specified format. + /// + /// If the format has an alpha channel, this may return the corresponding opaque format. + /// + /// Unknown formats will always return [`None`]. + pub const fn get_opaque( + fourcc: $crate::backend::allocator::Fourcc, + ) -> Option<$crate::backend::allocator::Fourcc> { + match fourcc { + $($( + $crate::backend::allocator::Fourcc::$fourcc + => Some($crate::backend::allocator::Fourcc::$opaque), + )?)* + _ => None, + } + } + + /// Returns the transparent alternative of the specified format. + /// + /// If the format has an unused alpha channel, this may return the corresponding non-opaque format. + /// + /// Unknown formats will always return [`None`]. + pub const fn get_transparent( + fourcc: $crate::backend::allocator::Fourcc, + ) -> Option<$crate::backend::allocator::Fourcc> { + match fourcc { + $($( + $crate::backend::allocator::Fourcc::$opaque + => Some($crate::backend::allocator::Fourcc::$fourcc), + )?)* + _ => None, + } + } + + /// Returns true if the format has an alpha channel. + /// + /// This function may be useful to know if the alpha channel may need to be swizzled when rendering + /// with some graphics apis. + /// + /// Unknown formats will always return `false`. + pub const fn has_alpha(fourcc: $crate::backend::allocator::Fourcc) -> bool { + match fourcc { + $( + $crate::backend::allocator::Fourcc::$fourcc => $alpha, + )* + _ => false, + } + } + + /// Returns the bits per pixel of the specified format. + /// + /// Unknown formats will always return [`None`]. + pub const fn get_bpp( + fourcc: $crate::backend::allocator::Fourcc, + ) -> Option { + match fourcc { + $($crate::backend::allocator::Fourcc::$fourcc => Some($bpp),)* + _ => None, + } + } + + /// Returns the depth of the specified format. + /// + /// Unknown formats will always return [`None`]. + pub const fn get_depth( + fourcc: $crate::backend::allocator::Fourcc, + ) -> Option { + match fourcc { + $($crate::backend::allocator::Fourcc::$fourcc => Some($depth),)* + _ => None, + } + } + + fn _impl_formats() -> &'static [$crate::backend::allocator::Fourcc] { + &[ + $( + $crate::backend::allocator::Fourcc::$fourcc, + )* + ] + } + }; +} + +format_tables! { + // 8-bit bpp Red + R8 { alpha: false, bpp: 8, depth: 8 }, + + // TODO: Update drm-fourcc + // 16-bit bpp Red with padding (x:R) + // R10 { bpp: 16, depth: 10 }, + // R12 { bpp: 16, depth: 12 }, + + // 16-bit bpp Red + R16 { alpha: false, bpp: 16, depth: 16 }, + + // 16-bit bpp RG + Rg88 { alpha: false, bpp: 16, depth: 16 }, + + Gr88 { alpha: false, bpp: 16, depth: 16 }, + + // 32-bit bpp RG + Rg1616 { alpha: false, bpp: 32, depth: 32 }, + + Gr1616 { alpha: false, bpp: 32, depth: 32 }, + + // 8-bit bpp RGB + Rgb332 { alpha: false, bpp: 8, depth: 8 }, + + Bgr233 { alpha: false, bpp: 8, depth: 8 }, + + // 16-bit bpp RGB, 4 bits per channel + Argb4444 { + opaque: Xrgb4444, + alpha: true, + bpp: 16, + depth: 16, + }, + + Xrgb4444 { alpha: false, bpp: 16, depth: 12 }, + + Abgr4444 { + opaque: Xbgr4444, + alpha: true, + bpp: 16, + depth: 16, + }, + + Xbgr4444 { alpha: false, bpp: 16, depth: 12 }, + + Rgba4444 { + opaque: Rgbx4444, + alpha: true, + bpp: 16, + depth: 16, + }, + + Rgbx4444 { alpha: false, bpp: 16, depth: 12 }, + + Bgra4444 { + opaque: Bgrx4444, + alpha: true, + bpp: 16, + depth: 16, + }, + + Bgrx4444 { alpha: false, bpp: 16, depth: 12 }, + + // 16-bit bpp RGB, 5 bits per color channel, 1 bit for alpha channel + Argb1555 { + opaque: Xrgb1555, + alpha: true, + bpp: 16, + depth: 16 + }, + + Xrgb1555 { alpha: false, bpp: 16, depth: 15 }, + + Abgr1555 { + opaque: Xbgr1555, + alpha: true, + bpp: 16, + depth: 16, + }, + + Xbgr1555 { alpha: false, bpp: 16, depth: 15 }, + + Rgba5551 { + opaque: Rgbx5551, + alpha: true, + bpp: 16, + depth: 16, + }, + + Rgbx5551 { alpha: false, bpp: 16, depth: 15 }, + + Bgra5551 { + opaque: Bgrx5551, + alpha: true, + bpp: 16, + depth: 16, + }, + + Bgrx5551 { alpha: false, bpp: 16, depth: 15 }, + + // 16-bit bpp RGB, no alpha, 6 bits for green channel and 5 bits for blue and red + Rgb565 { alpha: false, bpp: 16, depth: 16 }, + + Bgr565 { alpha: false, bpp: 16, depth: 16 }, + + // 24-bit bpp RGB + Rgb888 { alpha: false, bpp: 24, depth: 24 }, + + Bgr888 { alpha: false, bpp: 24, depth: 24 }, + + // 32-bit bpp RGB, 8 bits per channel + Argb8888 { + opaque: Xrgb8888, + alpha: true, + bpp: 32, + depth: 32, + }, + + Xrgb8888 { alpha: false, bpp: 32, depth: 24 }, + + Abgr8888 { + opaque: Xbgr8888, + alpha: true, + bpp: 32, + depth: 32, + }, + + Xbgr8888 { alpha: false, bpp: 32, depth: 24 }, + + Rgba8888 { + opaque: Rgbx8888, + alpha: true, + bpp: 32, + depth: 32, + }, + + Rgbx8888 { alpha: false, bpp: 32, depth: 24 }, + + Bgra8888 { + opaque: Bgrx8888, + alpha: true, + bpp: 32, + depth: 32, + }, + + Bgrx8888 { alpha: false, bpp: 32, depth: 24 }, + + // 32-bit bpp RGB with 10-bits per color channel + + Argb2101010 { + opaque: Xrgb2101010, + alpha: true, + bpp: 32, + depth: 32, + }, + + Xrgb2101010 { alpha: false, bpp: 32, depth: 30 }, + + Abgr2101010 { + opaque: Xbgr2101010, + alpha: true, + bpp: 32, + depth: 32, + }, + + Xbgr2101010 { alpha: false, bpp: 32, depth: 30 }, + + Rgba1010102 { + opaque: Rgbx1010102, + alpha: true, + bpp: 32, + depth: 32, + }, + + Rgbx1010102 { alpha: false, bpp: 32, depth: 30 }, + + Bgra1010102 { + opaque: Bgrx1010102, + alpha: true, + bpp: 32, + depth: 32, + }, + + Bgrx1010102 { alpha: false, bpp: 32, depth: 30 }, + + // 64-bit RGB, 16-bits per channel + // TODO: Update drm-fourcc + // Argb16161616 { + // opaque: Xrgb16161616, + // alpha: true, + // bpp: 64, + // depth: 64, + // }, + // Xrgb16161616 { alpha: false, bpp: 64, depth: 48 }, + // Abgr16161616 { + // opaque: Xbgr16161616, + // alpha: true, + // bpp: 64, + // depth: 64, + // }, + // Xbgr16161616 { alpha: false, bpp: 64, depth: 48 }, + + // Floating point 64bpp RGB + // IEEE 754-2008 binary16 half-precision float + Argb16161616f { + opaque: Xrgb16161616f, + alpha: true, + bpp: 64, + depth: 64 + }, + + Xrgb16161616f { alpha: false, bpp: 64, depth: 48 }, + + Abgr16161616f { + opaque: Xbgr16161616f, + alpha: true, + bpp: 64, + depth: 64, + }, + + Xbgr16161616f { alpha: false, bpp: 64, depth: 48 }, + + // RGBA with 10-bit components packed in 64-bits per pixel, with 6-bits of unused padding per component + + // Axbxgxrx106106106106 has no direct non-alpha alternative. + Axbxgxrx106106106106 { alpha: true, bpp: 64, depth: 40 }, + + Yuv420 {alpha: false, bpp: 12, depth: 12 }, + Nv12 {alpha: false, bpp: 12, depth: 12 } + // TODO: YUV and other formats +} + +/// A set of [`Format`]s +#[derive(Debug, Default, Clone)] +pub struct FormatSet { + formats: Arc>, +} + +impl FormatSet { + #[cfg(any(feature = "backend_egl", feature = "backend_drm"))] + pub(crate) fn from_formats(formats: IndexSet) -> Self { + FormatSet { + formats: Arc::new(formats), + } + } +} + +impl FormatSet { + /// Return an iterator over the values of the set, in their order + pub fn iter(&self) -> FormatSetIter<'_> { + FormatSetIter { + inner: self.formats.iter(), + } + } + + /// Return `true` if an equivalent to `value` exists in the set. + pub fn contains(&self, format: &Format) -> bool { + self.formats.contains(format) + } + + /// Return an iterator over the values that are in both `self` and `other`. + pub fn intersection<'a>(&'a self, other: &'a FormatSet) -> FormatSetIntersection<'a> { + FormatSetIntersection { + inner: self.formats.intersection(&other.formats), + } + } + + /// Get access to the underlying storage. + pub fn indexset(&self) -> &IndexSet { + &self.formats + } +} + +/// A lazy iterator producing elements in the intersection of [`FormatSet`]s. +#[derive(Debug)] +pub struct FormatSetIntersection<'a> { + inner: indexmap::set::Intersection<'a, Format, std::collections::hash_map::RandomState>, +} + +impl<'a> Iterator for FormatSetIntersection<'a> { + type Item = &'a Format; + + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl IntoIterator for FormatSet { + type Item = Format; + + type IntoIter = FormatSetIntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + FormatSetIntoIter { + inner: (*self.formats).clone().into_iter(), + } + } +} + +impl FromIterator for FormatSet { + #[inline] + fn from_iter>(iter: T) -> Self { + Self { + formats: Arc::new(IndexSet::from_iter(iter)), + } + } +} + +/// An iterator over the items of an [`FormatSet`]. +#[derive(Debug)] +pub struct FormatSetIter<'a> { + inner: indexmap::set::Iter<'a, Format>, +} + +impl<'a> Iterator for FormatSetIter<'a> { + type Item = &'a Format; + + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +/// An owning iterator over the items of an [`FormatSet`]. +#[derive(Debug)] +pub struct FormatSetIntoIter { + inner: indexmap::set::IntoIter, +} + +impl Iterator for FormatSetIntoIter { + type Item = Format; + + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +#[cfg(test)] +mod tests { + use super::{_impl_formats, get_bpp, get_depth, get_opaque, get_transparent, has_alpha}; + + /// Tests that opaque alternatives are not the same as the variant with alpha. + #[test] + fn opaque_neq() { + for &format in _impl_formats() { + if let Some(opaque) = get_opaque(format) { + assert_ne!( + format, opaque, + "{}'s opaque alternative is the same format", + format + ); + } + } + } + + /// Tests that opaque alternatives are cleanly converting back with get_transparent. + #[test] + fn opaque_inverse() { + for &format in _impl_formats() { + if let Some(opaque) = get_opaque(format) { + let transparent = get_transparent(opaque); + assert_eq!( + Some(format), + transparent, + "{}'s opaque alternative {} doesn't cleanly convert back, got: {:?}", + format, + opaque, + transparent + ); + } + } + } + + /// Tests that opaque alternatives for formats do not have opaque alternatives themselves. + /// + /// For example, `Argb8888` should have an opaque alternative of `Xrgb8888` and `Xrgb8888` should have no + /// opaque alternative. + #[test] + fn opaque_alternatives() { + for &format in _impl_formats() { + if let Some(opaque) = get_opaque(format) { + // If a format is considered to be an opaque alternative, the format should not have an opaque + // alternative. + let result = get_opaque(opaque); + + assert!( + result.is_none(), + "Format {format} has an opaque alternative, {opaque}. However {opaque} reports an opaque alternative {opaque_opaque:?} which is incorrect", + format = format, + opaque = opaque, + opaque_opaque = result, + ); + } + } + } + + /// Tests that a format and it's opaque alternative have the same number of bits per pixel. + #[test] + fn opaque_has_same_bpp() { + for &format in _impl_formats() { + if let Some(opaque) = get_opaque(format) { + let format_bpp = get_bpp(format); + let opaque_bpp = get_bpp(opaque); + + assert_eq!( + format_bpp, + opaque_bpp, + "Format {format} has a bpp of {format_bpp:?}. However the opaque alternative {opaque} has a different bpp of {opaque_bpp:?}", + format_bpp = get_bpp(format), + opaque = opaque, + opaque_bpp = get_bpp(opaque), + ); + } + } + } + + /// A format with an opaque alternative should have alpha. + /// + /// The opaque alternative should not have alpha. + #[test] + fn format_with_opaque_has_alpha() { + for &format in _impl_formats() { + if let Some(opaque) = get_opaque(format) { + // Since the format has an opaque alternative, verify the opaque alternative does not have alpha. + assert!( + has_alpha(format), + "{} has an opaque alternative but does not state it has an alpha component", + format + ); + + // The opaque alternative should not have alpha. + assert!( + !has_alpha(opaque), + "opaque alternative to {} ({}) has an alpha channel", + format, + opaque + ); + } + } + } + + // A format's depth should always be equal or small to it's bits-per-pixel + #[test] + fn format_bpp_greater_or_equal_than_depth() { + for &format in _impl_formats() { + assert!( + get_depth(format) <= get_bpp(format), + "{} has a depth higher than its bpp", + format + ); + } + } +} diff --git a/vendor/smithay/src/backend/allocator/gbm.rs b/vendor/smithay/src/backend/allocator/gbm.rs new file mode 100644 index 0000000..573aa2d --- /dev/null +++ b/vendor/smithay/src/backend/allocator/gbm.rs @@ -0,0 +1,384 @@ +//! Module for Buffers created using [libgbm](gbm). +//! +//! The re-exported [`GbmDevice`](gbm::Device) implements the [`Allocator`] trait +//! and [`GbmBuffer`](gbm::BufferObject) satisfies the [`Buffer`] trait while also allowing +//! conversions to and from [dmabufs](super::dmabuf). + +use super::{ + Allocator, Buffer, Format, Fourcc, Modifier, + dmabuf::{AsDmabuf, Dmabuf, DmabufFlags, MAX_PLANES}, +}; +use crate::backend::drm::DrmNode; +use crate::utils::{Buffer as BufferCoords, Size}; +use drm::buffer::PlanarBuffer; +use gbm::BufferObject; +pub use gbm::{BufferObjectFlags as GbmBufferFlags, Device as GbmDevice}; +use std::os::unix::io::{AsFd, BorrowedFd}; +use tracing::instrument; + +/// A GBM buffer object +#[derive(Debug)] +pub struct GbmBuffer { + drm_node: Option, + bo: BufferObject<()>, + size: Size, + format: Format, +} + +impl PlanarBuffer for GbmBuffer { + #[inline] + fn size(&self) -> (u32, u32) { + (self.size.w as u32, self.size.h as u32) + } + + #[inline] + fn format(&self) -> Fourcc { + self.format.code + } + + #[inline] + fn modifier(&self) -> Option { + match self.format.modifier { + Modifier::Invalid => None, + x => Some(x), + } + } + + #[inline] + fn pitches(&self) -> [u32; 4] { + self.bo.pitches() + } + + #[inline] + fn handles(&self) -> [Option; 4] { + self.bo.handles() + } + + #[inline] + fn offsets(&self) -> [u32; 4] { + self.bo.offsets() + } +} + +impl drm::buffer::Buffer for GbmBuffer { + #[inline] + fn size(&self) -> (u32, u32) { + (self.size.w as u32, self.size.h as u32) + } + + #[inline] + fn format(&self) -> Fourcc { + self.format.code + } + + #[inline] + fn pitch(&self) -> u32 { + self.bo.pitch() + } + + #[inline] + fn handle(&self) -> drm::buffer::Handle { + drm::buffer::Buffer::handle(&self.bo) + } +} + +impl GbmBuffer { + /// Create a [`GbmBuffer`] from an existing [`BufferObject`] + /// + /// `implicit` forces the object to assume the modifier is `Invalid` for cases, + /// where the buffer was allocated with an older api, that doesn't support modifiers. + /// + /// Gbm might otherwise give us the underlying or a non-sensical modifier, + /// which can fail in various other apis. + pub fn from_bo(bo: BufferObject<()>, implicit: bool) -> Self { + let drm_node = DrmNode::from_file(bo.device_fd()).ok(); + Self::from_bo_with_node(bo, implicit, drm_node) + } + + /// Create a [`GbmBuffer`] from an existing [`BufferObject`] explicitly defining the device node + /// + /// `implicit` forces the object to assume the modifier is `Invalid` for cases, + /// where the buffer was allocated with an older api, that doesn't support modifiers. + /// + /// Gbm might otherwise give us the underlying or a non-sensical modifier, + /// which can fail in various other apis. + pub fn from_bo_with_node(bo: BufferObject<()>, implicit: bool, drm_node: Option) -> Self { + let size = (bo.width() as i32, bo.height() as i32).into(); + let format = Format { + code: bo.format(), + modifier: if implicit { + Modifier::Invalid + } else { + bo.modifier() + }, + }; + Self { + drm_node, + bo, + size, + format, + } + } + + /// Get the [`DrmNode`] of the device the buffer was allocated when available + pub fn device_node(&self) -> Option { + self.drm_node + } +} + +impl std::ops::Deref for GbmBuffer { + type Target = BufferObject<()>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.bo + } +} + +impl std::ops::DerefMut for GbmBuffer { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.bo + } +} + +/// Light wrapper around an [`GbmDevice`] to implement the [`Allocator`]-trait +#[derive(Clone, Debug)] +pub struct GbmAllocator { + drm_node: Option, + device: GbmDevice, + default_flags: GbmBufferFlags, +} + +impl AsRef> for GbmAllocator { + #[inline] + fn as_ref(&self) -> &GbmDevice { + &self.device + } +} + +impl AsMut> for GbmAllocator { + #[inline] + fn as_mut(&mut self) -> &mut GbmDevice { + &mut self.device + } +} + +impl AsFd for GbmAllocator { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.device.as_fd() + } +} + +impl GbmAllocator { + /// Create a new [`GbmAllocator`] from a [`GbmDevice`] with some default usage flags, + /// to be used when [`Allocator::create_buffer`] is invoked. + pub fn new(device: GbmDevice, default_flags: GbmBufferFlags) -> GbmAllocator { + let drm_node = DrmNode::from_file(device.as_fd()).ok(); + GbmAllocator { + drm_node, + device, + default_flags, + } + } + + /// Alternative to [`Allocator::create_buffer`], if you need a one-off buffer with + /// a different set of usage flags. + #[instrument(level = "trace", skip(self), fields(self.device = ?self.device, err))] + #[profiling::function] + pub fn create_buffer_with_flags( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + flags: GbmBufferFlags, + ) -> Result { + #[cfg(feature = "backend_gbm_has_create_with_modifiers2")] + let result = self + .device + .create_buffer_object_with_modifiers2(width, height, fourcc, modifiers.iter().copied(), flags) + .map(|bo| GbmBuffer::from_bo_with_node(bo, false, self.drm_node)); + + #[cfg(not(feature = "backend_gbm_has_create_with_modifiers2"))] + let result = if (flags & !(GbmBufferFlags::SCANOUT | GbmBufferFlags::RENDERING)).is_empty() { + self.device + .create_buffer_object_with_modifiers(width, height, fourcc, modifiers.iter().copied()) + .map(|bo| GbmBuffer::from_bo_with_node(bo, false, self.drm_node)) + } else if modifiers.contains(&Modifier::Invalid) || modifiers.contains(&Modifier::Linear) { + return self + .device + .create_buffer_object(width, height, fourcc, flags) + .map(|bo| GbmBuffer::from_bo_with_node(bo, true, self.drm_node)); + } else { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "unsupported combination of flags and modifiers", + )); + }; + + match result { + Ok(bo) => Ok(bo), + Err(err) => { + if modifiers.contains(&Modifier::Invalid) || modifiers.contains(&Modifier::Linear) { + self.device + .create_buffer_object(width, height, fourcc, flags) + .map(|bo| GbmBuffer::from_bo_with_node(bo, true, self.drm_node)) + } else { + Err(err) + } + } + } + } +} + +impl Allocator for GbmAllocator { + type Buffer = GbmBuffer; + type Error = std::io::Error; + + #[profiling::function] + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result { + self.create_buffer_with_flags(width, height, fourcc, modifiers, self.default_flags) + } +} + +impl Buffer for GbmBuffer { + #[inline] + fn size(&self) -> Size { + self.size + } + + #[inline] + fn format(&self) -> Format { + self.format + } +} + +/// Errors during conversion to a dmabuf handle from a gbm buffer object +#[derive(thiserror::Error, Debug)] +pub enum GbmConvertError { + /// The buffer consists out of multiple file descriptions, which is currently unsupported + #[error("Buffer consists out of multiple file descriptors, which is currently unsupported")] + UnsupportedBuffer, + /// The conversion returned an invalid file descriptor + #[error("Buffer returned invalid file descriptor")] + InvalidFD(#[from] gbm::InvalidFdError), +} + +impl AsDmabuf for GbmBuffer { + type Error = GbmConvertError; + + #[cfg(feature = "backend_gbm_has_fd_for_plane")] + #[profiling::function] + fn export(&self) -> Result { + let planes = self.plane_count() as i32; + + let mut builder = Dmabuf::builder_from_buffer(self, DmabufFlags::empty()); + for idx in 0..planes { + let fd = self.fd_for_plane(idx)?; + + builder.add_plane(fd, self.offset(idx), self.stride_for_plane(idx)); + } + + if let Some(node) = self.device_node() { + builder.set_node(node); + } + + Ok(builder.build().unwrap()) + } + + #[cfg(not(feature = "backend_gbm_has_fd_for_plane"))] + #[profiling::function] + fn export(&self) -> Result { + let planes = self.plane_count() as i32; + + let mut iter = (0i32..planes).map(|i| self.handle_for_plane(i)); + let first = iter.next().expect("Encountered a buffer with zero planes"); + // check that all handles are the same + let handle = iter.try_fold(first, |first, next| { + if unsafe { next.u64_ == first.u64_ } { + return Some(first); + } + None + }); + if handle.is_none() { + // GBM is lacking a function to get a FD for a given plane. Instead, + // check all planes have the same handle. We can't use + // drmPrimeHandleToFD because that messes up handle ref'counting in + // the user-space driver. + return Err(GbmConvertError::UnsupportedBuffer); + } + + let mut builder = Dmabuf::builder_from_buffer(self, DmabufFlags::empty()); + for idx in 0..planes { + let fd = self.fd()?; + + builder.add_plane(fd, self.offset(idx), self.stride_for_plane(idx)); + } + + if let Some(node) = self.device_node() { + builder.set_node(node); + } + + Ok(builder.build().unwrap()) + } +} + +impl Dmabuf { + /// Import a Dmabuf using libgbm, creating a gbm Buffer Object to the same underlying data. + #[profiling::function] + pub fn import_to( + &self, + gbm: &GbmDevice, + usage: GbmBufferFlags, + ) -> std::io::Result { + let mut handles = [None; MAX_PLANES]; + for (i, handle) in self.handles().take(MAX_PLANES).enumerate() { + handles[i] = Some(handle); + } + let mut strides = [0i32; MAX_PLANES]; + for (i, stride) in self.strides().take(MAX_PLANES).enumerate() { + strides[i] = stride as i32; + } + let mut offsets = [0i32; MAX_PLANES]; + for (i, offset) in self.offsets().take(MAX_PLANES).enumerate() { + offsets[i] = offset as i32; + } + + if self.has_modifier() || self.num_planes() > 1 || self.offsets().next().unwrap() != 0 { + gbm.import_buffer_object_from_dma_buf_with_modifiers( + self.num_planes() as u32, + handles, + self.width(), + self.height(), + self.format().code, + usage, + strides, + offsets, + self.format().modifier, + ) + .map(|bo| GbmBuffer::from_bo(bo, false)) + } else { + gbm.import_buffer_object_from_dma_buf( + handles[0].unwrap(), + self.width(), + self.height(), + strides[0] as u32, + self.format().code, + if self.format().modifier == Modifier::Linear { + usage | GbmBufferFlags::LINEAR + } else { + usage + }, + ) + .map(|bo| GbmBuffer::from_bo(bo, true)) + } + } +} diff --git a/vendor/smithay/src/backend/allocator/mod.rs b/vendor/smithay/src/backend/allocator/mod.rs new file mode 100644 index 0000000..c1cc100 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/mod.rs @@ -0,0 +1,122 @@ +//! Buffer allocation and management. +//! +//! Collection of common traits and implementations around +//! buffer creation and handling from various sources. +//! +//! Allocators provided: +//! - Dumb Buffers through [`crate::backend::drm::DrmDevice`] +//! - Gbm Buffers through [`::gbm::Device`] +//! +//! Buffer types supported: +//! - [DumbBuffers](dumb::DumbBuffer) +//! - [GbmBuffers](::gbm::BufferObject) +//! - [DmaBufs](dmabuf::Dmabuf) +//! +//! Helpers: +//! - [`Swapchain`] to help with buffer management for framebuffers + +pub mod dmabuf; +#[cfg(feature = "backend_drm")] +pub mod dumb; +pub mod format; +#[cfg(feature = "backend_gbm")] +pub mod gbm; +pub mod udmabuf; +#[cfg(feature = "backend_vulkan")] +pub mod vulkan; + +mod swapchain; +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, Mutex}, +}; + +use crate::utils::{Buffer as BufferCoords, Size}; +pub use swapchain::{Slot, Swapchain}; + +pub use drm_fourcc::{ + DrmFormat as Format, DrmFourcc as Fourcc, DrmModifier as Modifier, DrmVendor as Vendor, + UnrecognizedFourcc, UnrecognizedVendor, +}; + +/// Common trait describing common properties of most types of buffers. +pub trait Buffer { + /// Width of the two-dimensional buffer + fn width(&self) -> u32 { + self.size().w as u32 + } + /// Height of the two-dimensional buffer + fn height(&self) -> u32 { + self.size().h as u32 + } + /// Size of the two-dimensional buffer + fn size(&self) -> Size; + /// Pixel format of the buffer + fn format(&self) -> Format; +} + +/// Interface to create Buffers +pub trait Allocator { + /// Buffer type produced by this allocator + type Buffer: Buffer; + /// Error type thrown if allocations fail + type Error: std::error::Error; + + /// Try to create a buffer with the given dimensions and pixel format + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result; +} + +// General implementations for interior mutability. + +impl Allocator for Arc> { + type Buffer = A::Buffer; + type Error = A::Error; + + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result { + let mut guard = self.lock().unwrap(); + guard.create_buffer(width, height, fourcc, modifiers) + } +} + +impl Allocator for Rc> { + type Buffer = A::Buffer; + type Error = A::Error; + + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result { + self.borrow_mut().create_buffer(width, height, fourcc, modifiers) + } +} + +impl Allocator for Box + 'static> { + type Buffer = B; + type Error = E; + + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result { + (**self).create_buffer(width, height, fourcc, modifiers) + } +} diff --git a/vendor/smithay/src/backend/allocator/swapchain.rs b/vendor/smithay/src/backend/allocator/swapchain.rs new file mode 100644 index 0000000..5dfb277 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/swapchain.rs @@ -0,0 +1,263 @@ +use std::{ + fmt, + ops::Deref, + sync::{ + Arc, + atomic::{AtomicBool, AtomicU8, Ordering}, + }, +}; + +use tracing::instrument; + +use crate::backend::allocator::{Allocator, Buffer, Fourcc, Modifier}; +use crate::utils::user_data::UserDataMap; + +use super::dmabuf::{AsDmabuf, Dmabuf}; + +pub const SLOT_CAP: usize = 4; + +/// Swapchain handling a fixed set of re-usable buffers e.g. for scan-out. +/// +/// ## How am I supposed to use this? +/// +/// To do proper buffer management, most compositors do so called double-buffering. +/// Which means you use two buffers, one that is currently presented (the front buffer) +/// and one that is currently rendered to (the back buffer). After each rendering operation +/// you swap the buffers around, the old front buffer becomes the new back buffer, while +/// the new front buffer is displayed to the user. This avoids showing the user rendering +/// artifacts doing rendering. +/// +/// There are also reasons to do triple-buffering, e.g. if you swap operation takes a +/// unspecified amount of time. In that case you have one buffer, that is currently +/// displayed, one that is done drawing and about to be swapped in and another one, +/// which you can use to render currently. +/// +/// Re-using and managing these buffers becomes increasingly complex the more buffers you +/// introduce, which is where `Swapchain` comes into play. +/// +/// `Swapchain` allocates buffers for you and transparently re-created them, e.g. when resizing. +/// All you tell the swapchain is: *"Give me the next free buffer"* (by calling [`acquire`](Swapchain::acquire)). +/// You then hold on to the returned buffer during rendering and swapping and free it once it is displayed. +/// Efficient re-use of the buffers is done by the swapchain. +/// +/// If you have associated resources for each buffer that can be reused (e.g. framebuffer `Handle`s for a `DrmDevice`), +/// you can store then in the `Slot`s userdata field. If a buffer is re-used, its userdata is preserved for the next time +/// it is returned by `acquire()`. +pub struct Swapchain { + /// Allocator used by the swapchain + pub allocator: A, + + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: Vec, + + slots: [Arc>; SLOT_CAP], +} + +impl fmt::Debug for Swapchain { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("Swapchain") + .field("width", &self.width) + .field("height", &self.height) + .field("fourcc", &self.fourcc) + .field("modifiers", &self.modifiers) + .finish_non_exhaustive() + } +} + +/// Slot of a swapchain containing an allocated buffer and its userdata. +/// +/// The buffer is marked for re-use once all copies are dropped. +/// Holding on to this struct will block the buffer in the swapchain. +#[derive(Debug)] +pub struct Slot(Arc>); + +#[derive(Debug)] +struct InternalSlot { + buffer: Option, + acquired: AtomicBool, + age: AtomicU8, + userdata: UserDataMap, +} + +impl Slot { + /// Retrieve userdata for this slot. + pub fn userdata(&self) -> &UserDataMap { + &self.0.userdata + } + + /// Retrieve the age of the buffer + pub fn age(&self) -> u8 { + self.0.age.load(Ordering::SeqCst) + } +} + +impl Default for InternalSlot { + fn default() -> Self { + InternalSlot { + buffer: None, + acquired: AtomicBool::new(false), + age: AtomicU8::new(0), + userdata: UserDataMap::new(), + } + } +} + +impl Deref for Slot { + type Target = B; + fn deref(&self) -> &B { + Option::as_ref(&self.0.buffer).unwrap() + } +} + +impl AsDmabuf for Slot { + type Error = ::Error; + + fn export(&self) -> Result { + let maybe_dmabuf = self.userdata().get::(); + if maybe_dmabuf.is_none() { + let dmabuf = (**self).export()?; + self.userdata().insert_if_missing_threadsafe(|| dmabuf); + } + + Ok(self.userdata().get::().cloned().unwrap()) + } +} + +impl Drop for Slot { + fn drop(&mut self) { + self.0.acquired.store(false, Ordering::SeqCst); + } +} + +impl Swapchain +where + A: Allocator, +{ + /// Create a new swapchain with the desired allocator, dimensions and pixel format for the created buffers. + pub fn new( + allocator: A, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: Vec, + ) -> Swapchain { + Swapchain { + allocator, + width, + height, + fourcc, + modifiers, + slots: Default::default(), + } + } + + /// Acquire a new slot from the swapchain, if one is still free. + /// + /// The swapchain has an internal maximum of four re-usable buffers. + /// This function returns the first free one. + #[instrument(level = "trace", skip_all, err)] + #[profiling::function] + pub fn acquire(&mut self) -> Result>, A::Error> { + if let Some(free_slot) = self + .slots + .iter_mut() + .find(|s| !s.acquired.swap(true, Ordering::SeqCst)) + { + if free_slot.buffer.is_none() { + let free_slot = Arc::get_mut(free_slot).expect("Acquired was false, but Arc is not unique?"); + match self + .allocator + .create_buffer(self.width, self.height, self.fourcc, &self.modifiers) + { + Ok(buffer) => free_slot.buffer = Some(buffer), + Err(err) => { + free_slot.acquired.store(false, Ordering::SeqCst); + return Err(err); + } + } + } + assert!(free_slot.buffer.is_some()); + return Ok(Some(Slot(free_slot.clone()))); + } + + // no free slots + Ok(None) + } + + /// Mark a given buffer as submitted. + /// + /// This might effect internal data (e.g. buffer age) and may only be called, + /// the buffer may not be used for rendering anymore. + /// You may hold on to it, if you require keeping it alive. + /// + /// Buffers can always just be safely discarded by dropping them, but not + /// calling this function before may affect performance characteristics + /// (e.g. by not tracking the buffer age). + pub fn submitted(&mut self, slot: &Slot) { + // don't mess up the state, if the user submitted and old buffer, after e.g. a resize + if !self.slots.iter().any(|other| Arc::ptr_eq(&slot.0, other)) { + return; + } + + slot.0.age.store(1, Ordering::SeqCst); + for other_slot in &mut self.slots { + if !Arc::ptr_eq(other_slot, &slot.0) && other_slot.buffer.is_some() { + #[allow(deprecated, reason = "our MSRV is 1.87 but try_update() was added in 1.95")] + let res = other_slot + .age + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |age| { + if age > 0 { age.checked_add(1) } else { Some(0) } + }); + // If the age overflows the slot was not used for a long time. Lets clear it + if res.is_err() { + *other_slot = Default::default(); + } + } + } + } + + /// Change the dimensions of newly returned buffers. + /// + /// Already obtained buffers are unaffected and will be cleaned up on drop. + pub fn resize(&mut self, width: u32, height: u32) { + if self.width == width && self.height == height { + return; + } + + self.width = width; + self.height = height; + self.slots = Default::default(); + } + + /// Remove all internally cached buffers. + pub fn reset_buffers(&mut self) { + for slot in &mut self.slots { + *slot = Default::default(); + } + } + + /// Reset the age for each buffer. + /// + /// Resetting the buffer age will discard all damage information and force a + /// full redraw for the next frame. + pub fn reset_buffer_ages(&mut self) { + for slot in &mut self.slots { + match Arc::get_mut(slot) { + Some(slot) => slot.age = AtomicU8::new(0), + None => *slot = Default::default(), + } + } + } + + /// Get set format + pub fn format(&self) -> Fourcc { + self.fourcc + } + + /// Get allowed modifiers + pub fn modifiers(&self) -> &[Modifier] { + &self.modifiers + } +} diff --git a/vendor/smithay/src/backend/allocator/udmabuf.rs b/vendor/smithay/src/backend/allocator/udmabuf.rs new file mode 100644 index 0000000..f253b39 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/udmabuf.rs @@ -0,0 +1,169 @@ +//! Module containing an implementation of the [`Allocator`]-trait using linux `udmabuf` api. + +#![cfg(target_os = "linux")] + +use std::{ + io, + os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}, + sync::LazyLock, +}; + +use super::{ + Allocator, Fourcc, Modifier, + dmabuf::{Dmabuf, DmabufFlags}, + format::get_bpp, +}; +use crate::utils::Size; + +use rustix::{ + fs::{MemfdFlags, Mode, OFlags, SealFlags, fcntl_add_seals, ftruncate, memfd_create}, + ioctl::{Ioctl, Opcode, ioctl, opcode}, + param::page_size, +}; + +static PAGE_SIZE: LazyLock = LazyLock::new(page_size); + +/// [`Allocator`] creating [`Dmabuf`]s for system memory via the `udmabuf` uapi. +/// +/// Note: [`Allocator::create_buffer`] will always return linear buffers for a +/// [`UdmabufAllocator`] and fail if [`Modifier::Linear`] is not in the list +/// of passed modifiers. +#[derive(Debug)] +pub struct UdmabufAllocator { + dev: OwnedFd, +} + +/// Minimum alignment requirement for the stride for `radv` (which seems to be the highest among drivers). +pub const STRIDE_ALIGN: usize = 256; + +impl UdmabufAllocator { + /// Tries to open `/dev/udmabuf` and create a [`UdmabufAllocator`]. + pub fn new() -> io::Result { + let fd = rustix::fs::open("/dev/udmabuf", OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?; + Ok(UdmabufAllocator { dev: fd }) + } + + /// Creates a [`Dmabuf`] from an existing allocation represented by a memfd. + /// + /// For importing to succeed a few additional requirements need to be satisfied: + /// - the width and height cannot exceed 65535. + /// - the stride needs to be aligned to [`STRIDE_ALIGN`]. + /// - size and offset need to be aligned to the platforms page size. + /// + /// Note: dmabufs created through this interface will always be interpreted as linear. + #[allow(clippy::too_many_arguments)] + pub fn create_buffer_from_memfd( + &self, + mem_fd: impl AsFd, + offset: usize, + size: usize, + format: Fourcc, + width: u32, + height: u32, + stride: u32, + ) -> Result { + if width >= u16::MAX as u32 || height >= u16::MAX as u32 { + return Err(io::ErrorKind::InvalidData.into()); + } + if !(stride as usize).is_multiple_of(STRIDE_ALIGN) { + return Err(io::ErrorKind::InvalidData.into()); + } + + if !size.is_multiple_of(*PAGE_SIZE) || size < (height * stride) as usize { + return Err(io::ErrorKind::InvalidData.into()); + } + + let dma_fd = udmabuf_from_memfd(self.dev.as_fd(), mem_fd.as_fd(), offset as u64, size as u64)?; + let mut dmabuf = Dmabuf::builder( + Size::new(width as i32, height as i32), + format, + Modifier::Linear, + DmabufFlags::empty(), + ); + dmabuf.add_plane(dma_fd, 0, stride); + dmabuf.build().ok_or_else(|| io::ErrorKind::Unsupported.into()) + } +} + +impl Allocator for UdmabufAllocator { + type Buffer = Dmabuf; + type Error = io::Error; + + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: Fourcc, + modifiers: &[Modifier], + ) -> Result { + if width >= u16::MAX as u32 || height >= u16::MAX as u32 { + return Err(io::ErrorKind::InvalidData.into()); + } + if !modifiers.contains(&Modifier::Linear) { + return Err(io::ErrorKind::InvalidData.into()); + } + let stride = (width as usize * get_bpp(fourcc).ok_or(io::ErrorKind::InvalidData)?) + .next_multiple_of(STRIDE_ALIGN); + let size = (height as usize * stride).next_multiple_of(*PAGE_SIZE); + + let mem_fd = memfd_create("udmabuf", MemfdFlags::ALLOW_SEALING | MemfdFlags::CLOEXEC)?; + ftruncate(&mem_fd, size as u64)?; + fcntl_add_seals(&mem_fd, SealFlags::SHRINK)?; + self.create_buffer_from_memfd(mem_fd, 0, size, fourcc, width, height, stride as u32) + } +} + +fn udmabuf_from_memfd( + fd: BorrowedFd<'_>, + mem_fd: BorrowedFd<'_>, + offset: u64, + size: u64, +) -> io::Result { + #[repr(C)] + #[derive(Debug)] + struct udmabuf_create { + memfd: u32, + flags: u32, + offset: u64, + size: u64, + } + const UDMABUF_FLAGS_CLOEXEC: u32 = 0x01; + + unsafe impl Ioctl for udmabuf_create { + type Output = RawFd; + + const IS_MUTATING: bool = false; + + fn opcode(&self) -> Opcode { + opcode::write::(b'u', 0x42) + } + + fn as_ptr(&mut self) -> *mut rustix::ffi::c_void { + self as *mut Self as *mut _ + } + + unsafe fn output_from_ptr( + out: rustix::ioctl::IoctlOutput, + _extract_output: *mut rustix::ffi::c_void, + ) -> rustix::io::Result { + if out < 0 { + Err(rustix::io::Errno::from_raw_os_error(!out)) + } else { + Ok(out) + } + } + } + + let args = udmabuf_create { + memfd: mem_fd.as_raw_fd() as u32, + flags: UDMABUF_FLAGS_CLOEXEC, + offset, + size, + }; + + unsafe { + ioctl(fd, args) + .map(|fd| OwnedFd::from_raw_fd(fd)) + .map_err(|err| io::Error::from_raw_os_error(err.raw_os_error())) + } +} diff --git a/vendor/smithay/src/backend/allocator/vulkan/format.rs b/vendor/smithay/src/backend/allocator/vulkan/format.rs new file mode 100644 index 0000000..c0f7538 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/vulkan/format.rs @@ -0,0 +1,83 @@ +//! Format conversions between Vulkan and DRM formats. + +/// Macro to generate format conversions between Vulkan and FourCC format codes. +/// +/// Any entry in this table may have attributes associated with a conversion. This is needed for `PACK` Vulkan +/// formats which may only have an alternative given a specific host endian. +/// +/// See the module documentation for usage details. +macro_rules! vk_format_table { + ( + $( + // This meta specifier is used for format conversions for PACK formats. + $(#[$conv_meta:meta])* + $fourcc: ident => $vk: ident + ),* $(,)? + ) => { + /// Converts a FourCC format code to a Vulkan format code. + /// + /// This will return [`None`] if the format is not known. + /// + /// These format conversions will return all known FourCC and Vulkan format conversions. However a + /// Vulkan implementation may not support some Vulkan format. One notable example of this are the + /// formats introduced in `VK_EXT_4444_formats`. The corresponding FourCC codes will return the + /// formats from `VK_EXT_4444_formats`, but the caller is responsible for testing that a Vulkan device + /// supports these formats. + pub const fn get_vk_format(fourcc: $crate::backend::allocator::Fourcc) -> Option { + // FIXME: Use reexport for ash::vk::Format + match fourcc { + $( + $(#[$conv_meta])* + $crate::backend::allocator::Fourcc::$fourcc => Some(ash::vk::Format::$vk), + )* + + _ => None, + } + } + + /// Returns all the known format conversions. + /// + /// The list contains FourCC format codes that may be converted using [`get_vk_format`]. + pub const fn known_formats() -> &'static [$crate::backend::allocator::Fourcc] { + &[ + $( + $crate::backend::allocator::Fourcc::$fourcc + ),* + ] + } + }; +} + +// FIXME: SRGB format is not always correct. +// +// Vulkan classifies formats by both channel sizes and colorspace. FourCC format codes do not classify formats +// based on colorspace. +// +// To implement this correctly, it is likely that parsing vulkan.xml and classifying families of colorspaces +// would be needed since there are a lot of formats. +// +// Many of these conversions come from wsi_common_wayland.c in Mesa +vk_format_table! { + Argb8888 => B8G8R8A8_SRGB, + Xrgb8888 => B8G8R8A8_SRGB, + + Abgr8888 => R8G8B8A8_SRGB, + Xbgr8888 => R8G8B8A8_SRGB, + + // PACK32 formats are equivalent to u32 instead of [u8; 4] and thus depend their layout depends the host + // endian. + #[cfg(target_endian = "little")] + Rgba8888 => A8B8G8R8_SRGB_PACK32, + #[cfg(target_endian = "little")] + Rgbx8888 => A8B8G8R8_SRGB_PACK32, + + #[cfg(target_endian = "little")] + Argb2101010 => A2R10G10B10_UNORM_PACK32, + #[cfg(target_endian = "little")] + Xrgb2101010 => A2R10G10B10_UNORM_PACK32, + + #[cfg(target_endian = "little")] + Abgr2101010 => A2B10G10R10_UNORM_PACK32, + #[cfg(target_endian = "little")] + Xbgr2101010 => A2B10G10R10_UNORM_PACK32, +} diff --git a/vendor/smithay/src/backend/allocator/vulkan/mod.rs b/vendor/smithay/src/backend/allocator/vulkan/mod.rs new file mode 100644 index 0000000..33690b3 --- /dev/null +++ b/vendor/smithay/src/backend/allocator/vulkan/mod.rs @@ -0,0 +1,807 @@ +//! Module for Buffers created using Vulkan. +//! +//! The [`VulkanAllocator`] type implements the [`Allocator`] trait and [`VulkanImage`] implements [`Buffer`]. +//! A [`VulkanImage`] may be exported as a [dmabuf](super::dmabuf). +//! +//! The Vulkan allocator supports up to Vulkan 1.3. +//! +//! The Vulkan allocator requires the following device extensions (and their dependencies): +//! - `VK_EXT_image_drm_format_modifier` +//! - `VK_EXT_external_memory_dmabuf` +//! - `VK_KHR_external_memory_fd` +//! +//! Additionally the Vulkan allocator may enable the following extensions if available: +//! - `VK_EXT_4444_formats` +//! +//! To get the required extensions a device must support to use the Vulkan allocator, use +//! [`VulkanAllocator::required_extensions`]. + +pub mod format; + +use std::{ + ffi::CStr, + fmt, + os::unix::io::{FromRawFd, OwnedFd}, + sync::{Arc, Weak, mpsc}, +}; + +use ash::{ext, khr, vk}; +use bitflags::bitflags; +use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier}; +use tracing::instrument; + +#[cfg(feature = "backend_drm")] +use crate::backend::drm::DrmNode; +use crate::{ + backend::{ + allocator::dmabuf::DmabufFlags, + vulkan::{PhysicalDevice, version::Version}, + }, + utils::{Buffer as BufferCoord, Size}, +}; + +use super::{ + Allocator, Buffer, + dmabuf::{AsDmabuf, Dmabuf, MAX_PLANES}, +}; + +bitflags! { + /// Flags to indicate the intended usage for a buffer. + /// + /// The usage flags may influence whether a buffer with a specific format and modifier combination may + /// successfully be imported by a graphics api when it is exported as a [`Dmabuf`]. + /// + /// For example, if a [`Buffer`] is used for scan-out, it is only necessary to specify the color + /// attachment usage. However, if the exported buffer is used by a client and intended to be rendered to, + /// then it is possible that the import will fail because the buffer was not allocated with right usage + /// for the format and modifier combination. + /// + /// The default usage set when creating a [`VulkanAllocator`] guarantees that an exported buffer may be + /// imported successfully with the same usage. + /// + /// If you need to allocate buffers with different usages dynamically, then you may use + /// [`VulkanAllocator::create_buffer_with_usage`]. + /// + /// [`VulkanAllocator::is_format_supported`] can check if the combination of format, modifier and usage + /// flags are supported. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct ImageUsageFlags: vk::Flags { + /// The image may be the source of a transfer command. + /// + /// This allows the content of the exported buffer to be downloaded from the GPU. + const TRANSFER_SRC = vk::ImageUsageFlags::TRANSFER_SRC.as_raw(); + + /// The image may be the destination of a transfer command. + /// + /// This allows the content of the exported buffer to be modified by a memory upload to the GPU. + const TRANSFER_DST = vk::ImageUsageFlags::TRANSFER_DST.as_raw(); + + /// Image may be sampled in a shader. + /// + /// This should be used if the exported buffer will be used as a texture. + const SAMPLED = vk::ImageUsageFlags::SAMPLED.as_raw(); + + /// The image may be used in a color attachment. + /// + /// This should be used if the exported buffer will be rendered to. + const COLOR_ATTACHMENT = vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(); + } +} + +/// Error type for [`VulkanAllocator`]. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The allocator could not be created. + /// + /// This can occur for a few reasons: + /// - No suitable queue family was found. + #[error("could not create allocator")] + Setup, + + /// The size specified to create the buffer was too small. + /// + /// The size must be greater than `0 x 0` pixels. + #[error("invalid buffer size")] + InvalidSize, + + /// The specified format is not supported. + /// + /// This error may occur for a few reasons: + /// 1. There is no equivalent Vulkan format for the specified format code. + /// 2. The driver does not support any of the specified modifiers for the format code. + /// 3. The driver does support some of the format, but the size of the buffer requested is too large. + /// 4. The usage is empty + #[error("format is not supported")] + UnsupportedFormat, + + /// Some error from the Vulkan driver. + #[error(transparent)] + Vk(#[from] vk::Result), +} + +/// An allocator which uses Vulkan to create buffers. +pub struct VulkanAllocator { + formats: Vec, + images: Vec, + default_usage: ImageUsageFlags, + remaining_allocations: u32, + extension_fns: ExtensionFns, + dropped_recv: mpsc::Receiver, + dropped_sender: mpsc::Sender, + phd: PhysicalDevice, + #[cfg(feature = "backend_drm")] + node: Option, + device: Arc, +} + +impl fmt::Debug for VulkanAllocator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("VulkanAllocator") + .field("formats", &self.formats) + .field("images", &self.images) + .field("default_usage", &self.default_usage) + .field("remaining_allocations", &self.remaining_allocations) + .field("dropped_recv", &self.dropped_recv) + .field("dropped_sender", &self.dropped_sender) + .field("phd", &self.phd) + .finish() + } +} + +impl VulkanAllocator { + /// Maximum supported version instance version that may be used with the allocator. + pub const MAX_INSTANCE_VERSION: Version = Version::VERSION_1_3; + + /// Returns the list of device extensions required by the Vulkan allocator. + /// + /// This function may return a different list for each [`PhysicalDevice`], meaning each device should be + /// filtered using it's own call to this function. + pub fn required_extensions(phd: &PhysicalDevice) -> Vec<&'static CStr> { + // Always required extensions + let mut extensions = vec![ + ext::image_drm_format_modifier::NAME, + ext::external_memory_dma_buf::NAME, + khr::external_memory_fd::NAME, + ]; + + if phd.api_version() < Version::VERSION_1_2 { + // VK_EXT_image_drm_format_modifier requires VK_KHR_image_format_list. + // VK_KHR_image_format_list is part of the core API in Vulkan 1.2 + extensions.push(khr::image_format_list::NAME); + } + + // Optional extensions: + + // VK_EXT_4444_formats is part of the core API in Vulkan 1.3. Although not always supported + // (see the 1.3 features to enable) + if phd.api_version() < Version::VERSION_1_3 { + // In 1.2 and below, the device must support the extension to use it. + if phd.has_device_extension(ext::_4444_formats::NAME) { + extensions.push(ext::_4444_formats::NAME); + } + } + + extensions + } + + /// Creates a [`VulkanAllocator`]. + /// + /// # Panics + /// + /// - If the version of instance which created the [`PhysicalDevice`] is higher than [`VulkanAllocator::MAX_INSTANCE_VERSION`]. + /// - If the default [`ImageUsageFlags`] are empty. + #[instrument(err, skip(phd), fields(physical_device = phd.name()))] + pub fn new(phd: &PhysicalDevice, default_usage: ImageUsageFlags) -> Result { + // Panic if the instance version is too high + if phd.instance().api_version() > Self::MAX_INSTANCE_VERSION { + panic!("Exceeded maximum instance api version for VulkanAllocator (1.3 max)") + } + + // VUID-VkPhysicalDeviceImageFormatInfo2-usage-requiredbitmask + // At least one image usage flag must be specified. + if default_usage.is_empty() { + panic!("Default usage flags for allocator are empty") + } + + // Get required extensions + let extensions = Self::required_extensions(phd); + let extension_pointers = extensions.iter().copied().map(CStr::as_ptr).collect::>(); + + // We don't actually submit any commands to the queue, but Vulkan requires that we create devices with + // at least one queue (VUID-VkDeviceCreateInfo-queueCreateInfoCount-arraylength). + let queue_families = unsafe { + phd.instance() + .handle() + .get_physical_device_queue_family_properties(phd.handle()) + }; + + let queue_family_index = queue_families + .iter() + // Find a queue with transfer + .position(|properties| properties.queue_flags.contains(vk::QueueFlags::TRANSFER)) + // If there is no transfer queue family, then try graphics (which must support transfer) + .or_else(|| { + queue_families + .iter() + .position(|properties| properties.queue_flags.contains(vk::QueueFlags::GRAPHICS)) + }) + .ok_or(Error::Setup)?; + + // TODO: Enable 4444 formats features + let queue_create_info = [vk::DeviceQueueCreateInfo::default() + .queue_family_index(queue_family_index as u32) + .queue_priorities(&[0.0])]; + let create_info = vk::DeviceCreateInfo::default() + .enabled_extension_names(&extension_pointers) + .queue_create_infos(&queue_create_info); + + let instance = phd.instance().handle(); + let device = unsafe { instance.create_device(phd.handle(), &create_info, None) }?; + + // Load extension functions + let extension_fns = ExtensionFns { + ext_image_format_modifier: ext::image_drm_format_modifier::Device::new(instance, &device), + khr_external_memory_fd: khr::external_memory_fd::Device::new(instance, &device), + }; + + let (dropped_sender, dropped_recv) = mpsc::channel(); + + #[cfg(feature = "backend_drm")] + let node = phd + .render_node() + .ok() + .flatten() + .or_else(|| phd.primary_node().ok().flatten()); + + let mut allocator = VulkanAllocator { + formats: Vec::new(), + images: Vec::new(), + default_usage, + remaining_allocations: phd.limits().max_memory_allocation_count, + extension_fns, + dropped_recv, + dropped_sender, + phd: phd.clone(), + #[cfg(feature = "backend_drm")] + node, + device: Arc::new(device), + }; + + allocator.init_formats(); + + Ok(allocator) + } + + /// Returns whether this allocator supports the specified format with the usage flags. + pub fn is_format_supported(&self, format: DrmFormat, usage: ImageUsageFlags) -> bool { + // VUID-VkPhysicalDeviceImageFormatInfo2-usage-requiredbitmask + // At least one image usage flag must be specified. + if usage.is_empty() { + return false; + } + + // TODO: Check if the extents are also valid? + // Vulkan states a maximum extent size for images. + // This may also be useful as a function on Allocator. + unsafe { self.get_format_info(format, vk::ImageUsageFlags::from_raw(usage.bits())) } + .ok() + .is_some() + } + + /// Try to create a buffer with the given dimensions, pixel format and usage flags. + /// + /// This may return [`Err`] for one of the following reasons: + /// - The `usage` is empty. + /// - The `fourcc` format is not supported. + /// - All of the allowed `modifiers` are not supported. + /// - The size of the buffer is too large for the `usage`, `fourcc` format or `modifiers`. + /// - The `fourcc` format and `modifiers` do not support the specified usage. + #[instrument(level = "trace", err)] + #[profiling::function] + pub fn create_buffer_with_usage( + &mut self, + width: u32, + height: u32, + fourcc: DrmFourcc, + modifiers: &[DrmModifier], + usage: ImageUsageFlags, + ) -> Result { + self.cleanup(); + + let vk_format = format::get_vk_format(fourcc).ok_or(Error::UnsupportedFormat)?; + let vk_usage = vk::ImageUsageFlags::from_raw(usage.bits()); + + // VUID-VkImageCreateInfo-extent-00944, VUID-VkImageCreateInfo-extent-00945 + if width == 0 || height == 0 { + return Err(Error::InvalidSize); + } + + // VUID-VkPhysicalDeviceImageFormatInfo2-usage-requiredbitmask + // At least one image usage flag must be specified. + if usage.is_empty() { + return Err(Error::UnsupportedFormat); + } + + // VUID-VkImageCreateInfo-usage-00964, VUID-VkImageCreateInfo-usage-00965 + if usage.contains(ImageUsageFlags::COLOR_ATTACHMENT) { + let limits = self.phd.limits(); + + if width > limits.max_framebuffer_width || height > limits.max_framebuffer_height { + return Err(Error::InvalidSize); + } + } + + // Filter out any format + modifier combinations that are not supported + let modifiers = self.filter_modifiers(width, height, vk_usage, fourcc, modifiers); + + // VUID-VkImageDrmFormatModifierListCreateInfoEXT-drmFormatModifierCount-arraylength + if modifiers.is_empty() { + return Err(Error::UnsupportedFormat); + } + + Ok(unsafe { self.create_image(width, height, vk_format, vk_usage, fourcc, &modifiers[..]) }?) + } + + /// Returns the [`PhysicalDevice`] this allocator was created with. + pub fn physical_device(&self) -> &PhysicalDevice { + &self.phd + } +} + +impl Allocator for VulkanAllocator { + type Buffer = VulkanImage; + type Error = Error; + + fn create_buffer( + &mut self, + width: u32, + height: u32, + fourcc: DrmFourcc, + modifiers: &[DrmModifier], + ) -> Result { + self.create_buffer_with_usage(width, height, fourcc, modifiers, self.default_usage) + } +} + +impl Drop for VulkanAllocator { + fn drop(&mut self) { + unsafe { + for image in &self.images { + self.device.destroy_image(image.image, None); + self.device.free_memory(image.memory, None); + } + + self.device.destroy_device(None); + } + } +} + +/// Vulkan image object. +/// +/// This type implements [`Buffer`] and the underlying image may be exported as a dmabuf. +pub struct VulkanImage { + inner: ImageInner, + width: u32, + height: u32, + format: DrmFormat, + #[cfg(feature = "backend_drm")] + node: Option, + /// The number of planes the image has for dmabuf export. + format_plane_count: u32, + khr_external_memory_fd: khr::external_memory_fd::Device, + dropped_sender: mpsc::Sender, + device: Weak, +} + +impl fmt::Debug for VulkanImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("VulkanImage") + .field("width", &self.width) + .field("height", &self.height) + .field("format", &self.format) + .field("inner", &self.inner) + .finish() + } +} + +impl Buffer for VulkanImage { + fn width(&self) -> u32 { + self.width + } + + fn height(&self) -> u32 { + self.height + } + + fn size(&self) -> Size { + (self.width as i32, self.height as i32).into() + } + + fn format(&self) -> DrmFormat { + self.format + } +} + +impl AsDmabuf for VulkanImage { + type Error = ExportError; + + #[profiling::function] + fn export(&self) -> Result { + let device = self.device.upgrade().ok_or(ExportError::AllocatorDestroyed)?; + + // Implementation may be broken if the plane count is wrong. + if self.format_plane_count == 0 { + return Err(ExportError::Failed); + } + + assert!( + self.format_plane_count as usize <= MAX_PLANES, + "Vulkan implementation reported too many planes" + ); + + let create_info = vk::MemoryGetFdInfoKHR::default() + .handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) + // VUID-VkMemoryGetFdInfoKHR-handleType-00671: Memory was allocated with DMA_BUF_EXT + .memory(self.inner.memory); + + let fd = unsafe { self.khr_external_memory_fd.get_memory_fd(&create_info) }?; + // SAFETY: `vkGetMemoryFdKHR` creates a new file descriptor owned by the caller. + let fd = Arc::new(unsafe { OwnedFd::from_raw_fd(fd) }); + let mut builder = Dmabuf::builder( + self.size(), + self.format().code, + self.format().modifier, + DmabufFlags::empty(), + ); + + for idx in 0..self.format_plane_count { + // get_image_subresource_layout only gets the layout of one memory plane. This mask specifies + // which plane should the layout be obtained for. + let aspect_mask = match idx { + 0 => vk::ImageAspectFlags::MEMORY_PLANE_0_EXT, + 1 => vk::ImageAspectFlags::MEMORY_PLANE_1_EXT, + 2 => vk::ImageAspectFlags::MEMORY_PLANE_2_EXT, + 3 => vk::ImageAspectFlags::MEMORY_PLANE_3_EXT, + _ => unreachable!(), + }; + + // VUID-vkGetImageSubresourceLayout-image-02270: All allocate images are created with drm tiling + let subresource = vk::ImageSubresource::default().aspect_mask(aspect_mask); + let layout = unsafe { device.get_image_subresource_layout(self.inner.image, subresource) }; + builder.add_plane(fd.clone(), layout.offset as u32, layout.row_pitch as u32); + } + + #[cfg(feature = "backend_drm")] + if let Some(node) = self.node { + builder.set_node(node); + } + + Ok(builder.build().unwrap()) + } +} + +impl Drop for VulkanImage { + fn drop(&mut self) { + let _ = self.dropped_sender.send(self.inner); + } +} + +/// The error type for exporting a [`VulkanImage`] as a [`Dmabuf`]. +#[derive(Debug, thiserror::Error)] +pub enum ExportError { + /// The image could not export a dmabuf since the allocator has been destroyed. + #[error("allocator has been destroyed")] + AllocatorDestroyed, + + /// The allocator could not export a dmabuf for an implementation dependent reason. + #[error("could not export a dmabuf")] + Failed, + + /// Vulkan API error. + #[error(transparent)] + Vk(#[from] vk::Result), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct ImageInner { + // TODO: image usage? + image: vk::Image, + /// The first entry will always have a non-null device memory handle. + /// + /// The other three entries may be a null handle or valid device memory (the latter with disjoint dmabufs). + memory: vk::DeviceMemory, +} + +#[derive(Debug)] +struct FormatEntry { + format: DrmFormat, + modifier_properties: vk::DrmFormatModifierPropertiesEXT, +} + +struct ExtensionFns { + /// Functions to get DRM format information. + /// + /// These are required, and therefore you may assume this functionality is available. + ext_image_format_modifier: ext::image_drm_format_modifier::Device, + /// Functions used for dmabuf import and export. + /// + /// If this is [`Some`], then the allocator will support dmabuf import and export operations. + khr_external_memory_fd: khr::external_memory_fd::Device, +} + +impl VulkanAllocator { + fn init_formats(&mut self) { + for &fourcc in format::known_formats() { + let vk_format = format::get_vk_format(fourcc).unwrap(); + let modifier_properties = self + .phd + .get_format_modifier_properties(vk_format) + .expect("The Vulkan allocator requires VK_EXT_image_drm_format_modifier"); + + for modifier_properties in modifier_properties { + self.formats.push(FormatEntry { + format: DrmFormat { + code: fourcc, + modifier: DrmModifier::from(modifier_properties.drm_format_modifier), + }, + modifier_properties, + }); + } + } + } + + /// Returns whether the format + modifier combination and the usage flags are supported. + unsafe fn get_format_info( + &self, + format: DrmFormat, + usage: vk::ImageUsageFlags, + ) -> Result, vk::Result> { + let vk_format = format::get_vk_format(format.code); + + match vk_format { + Some(vk_format) => { + let mut image_drm_format_info = vk::PhysicalDeviceImageDrmFormatModifierInfoEXT::default() + .drm_format_modifier(format.modifier.into()) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + let format_info = vk::PhysicalDeviceImageFormatInfo2::default() + .format(vk_format) + .ty(vk::ImageType::TYPE_2D) + .tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT) + .usage(usage) + .flags(vk::ImageCreateFlags::empty()) + // VUID-VkPhysicalDeviceImageFormatInfo2-tiling-02249 + .push_next(&mut image_drm_format_info); + let mut image_format_properties = vk::ImageFormatProperties2::default(); + + // VUID-vkGetPhysicalDeviceImageFormatProperties-tiling-02248: Must use vkGetPhysicalDeviceImageFormatProperties2 + let result = unsafe { + self.phd + .instance() + .handle() + .get_physical_device_image_format_properties2( + self.phd.handle(), + &format_info, + &mut image_format_properties, + ) + }; + + result + .map(|_| Some(image_format_properties.image_format_properties)) + .or_else(|result| { + // Unsupported format + usage combination + if result == vk::Result::ERROR_FORMAT_NOT_SUPPORTED { + Ok(None) + } else { + Err(result) + } + }) + } + + None => Ok(None), + } + } + + fn filter_modifiers( + &self, + width: u32, + height: u32, + vk_usage: vk::ImageUsageFlags, + fourcc: DrmFourcc, + modifiers: &[DrmModifier], + ) -> Vec { + modifiers + .iter() + .copied() + .filter_map(move |modifier| { + let info = unsafe { + self.get_format_info( + DrmFormat { + code: fourcc, + modifier, + }, + vk_usage, + ) + } + .ok() + .flatten()?; + + Some((modifier, info)) + }) + // Filter modifiers where the required image creation limits are not met + .filter(move |(_, properties)| { + let max_extent = properties.max_extent; + + // VUID-VkImageCreateInfo-extent-02252 + max_extent.width >= width + // VUID-VkImageCreateInfo-extent-02253 + && max_extent.height >= height + // VUID-VkImageCreateInfo-extent-02254 + // VUID-VkImageCreateInfo-extent-00946 + // VUID-VkImageCreateInfo-imageType-00957 + && max_extent.depth >= 1 + // VUID-VkImageCreateInfo-samples-02258 + && properties.sample_counts.contains(vk::SampleCountFlags::TYPE_1) + }) + .map(|(modifier, _)| modifier) + .map(Into::::into) + // TODO: Could use a smallvec or tinyvec to reduce number allocations + .collect::>() + } + + /// # Safety + /// + /// * The list of modifiers must be supported for the given format and image usage flags. + /// * The extent of the image must be within the maximum extents Vulkan tells. + unsafe fn create_image( + &mut self, + width: u32, + height: u32, + vk_format: vk::Format, + vk_usage: vk::ImageUsageFlags, + fourcc: DrmFourcc, + modifiers: &[u64], + ) -> Result { + assert!(width > 0); + assert!(height > 0); + + // Ensure maximum allocations are not exceeded. + if self.remaining_allocations == 0 { + todo!() + } + + // Now that the list of valid modifiers is known, create an image using one of the modifiers. + let mut modifier_list = + vk::ImageDrmFormatModifierListCreateInfoEXT::default().drm_format_modifiers(modifiers); + let mut external_memory_image_create_info = vk::ExternalMemoryImageCreateInfo::default() + .handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + + let image_create_info = vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .format(vk_format) + .extent(vk::Extent3D { + width, + height, + // VUID-VkImageCreateInfo-extent-00946 + // VUID-VkImageCreateInfo-imageType-00957 + depth: 1, + }) + // VUID-VkImageCreateInfo-samples-parameter + .samples(vk::SampleCountFlags::TYPE_1) + // VUID-VkImageCreateInfo-mipLevels-00947 + .mip_levels(1) + // VUID-VkImageCreateInfo-arrayLayers-00948 + .array_layers(1) + // VUID-VkImageCreateInfo-pNext-02262 + .tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT) + // VUID-VkImageCreateInfo-usage-requiredbitmask + // FIXME: We don't assert any usage flags are set + .usage(vk_usage) + // VUID-VkImageCreateInfo-initialLayout-00993 + .initial_layout(vk::ImageLayout::UNDEFINED) + // VUID-VkImageCreateInfo-tiling-02261 + .push_next(&mut modifier_list) + // TODO: VUID-VkImageCreateInfo-pNext-00990 + .push_next(&mut external_memory_image_create_info); + + // The image is placed in a scope guard to safely handle future allocation failures. + let mut guard = scopeguard::guard( + ImageInner { + // This is the only spot where ? may be used to detect and error since no previous handles have been created. + image: unsafe { self.device.create_image(&image_create_info, None) }?, + memory: vk::DeviceMemory::null(), + }, + |inner| unsafe { self.device.destroy_image(inner.image, None) }, + ); + + // Get the modifier Vulkan created the image using. + let format = { + let mut image_modifier_properties = vk::ImageDrmFormatModifierPropertiesEXT::default(); + + unsafe { + self.extension_fns + .ext_image_format_modifier + .get_image_drm_format_modifier_properties(guard.image, &mut image_modifier_properties) + }?; + + DrmFormat { + code: fourcc, + modifier: DrmModifier::from(image_modifier_properties.drm_format_modifier), + } + }; + + // Now that we know the plane count, get the number of planes for the format + modifier + let format_plane_count = self + .formats + .iter() + .find(|entry| entry.format == format) + .unwrap() + .modifier_properties + .drm_format_modifier_plane_count; + + // Allocate image memory + let memory_reqs = unsafe { self.device.get_image_memory_requirements(guard.image) }; + // TODO: Memory type index + let mut export_memory_allocate_info = vk::ExportMemoryAllocateInfo::default() + .handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + let alloc_create_info = vk::MemoryAllocateInfo::default() + .allocation_size(memory_reqs.size) + .push_next(&mut export_memory_allocate_info); + + unsafe { + // Allocate memory for the image. + guard.memory = self.device.allocate_memory(&alloc_create_info, None)?; + // Finally bind the memory to the image + self.device.bind_image_memory(guard.image, guard.memory, 0)?; + } + + // Initialization is complete, prevent the scope guard from running it's dropfn. + let inner = scopeguard::ScopeGuard::into_inner(guard); + + // Track the image for destruction. + self.images.push(inner); + + self.remaining_allocations -= 1; + + Ok(VulkanImage { + inner, + width, + height, + format, + format_plane_count, + khr_external_memory_fd: self.extension_fns.khr_external_memory_fd.clone(), + dropped_sender: self.dropped_sender.clone(), + device: Arc::downgrade(&self.device), + #[cfg(feature = "backend_drm")] + node: self.node, + }) + } + + fn cleanup(&mut self) { + let dropped = self.dropped_recv.try_iter().collect::>(); + + self.images.retain(|image| { + // Only drop if the + let drop = dropped.contains(image); + + if drop { + // Destroy the underlying image resource + unsafe { + self.device.destroy_image(image.image, None); + self.device.free_memory(image.memory, None); + } + + self.remaining_allocations = self + .remaining_allocations + .checked_add(1) + .expect("Remaining allocations overflowed"); + debug_assert!( + self.phd.limits().max_memory_allocation_count >= self.remaining_allocations, + "Too many allocations released", + ); + } + + // If the image was dropped, return false + !drop + }) + } +} diff --git a/vendor/smithay/src/backend/drm/compositor/elements.rs b/vendor/smithay/src/backend/drm/compositor/elements.rs new file mode 100644 index 0000000..a977897 --- /dev/null +++ b/vendor/smithay/src/backend/drm/compositor/elements.rs @@ -0,0 +1,186 @@ +use crate::{ + backend::renderer::{ + Color32F, Frame, Renderer, + element::{Element, Id, RenderElement}, + utils::{CommitCounter, DamageSet, OpaqueRegions}, + }, + render_elements, + utils::{Buffer, Physical, Rectangle, Scale, Transform, user_data::UserDataMap}, +}; + +render_elements! { + pub DrmRenderElements<'a, R, E>; + Holepunch=HolepunchRenderElement, + Overlay=OverlayPlaneElement, + Other=&'a E, +} + +pub struct HolepunchRenderElement { + id: Id, + geometry: Rectangle, +} + +impl HolepunchRenderElement { + pub fn from_render_element(id: Id, element: &E, scale: impl Into>) -> Self + where + R: Renderer, + E: RenderElement, + { + HolepunchRenderElement { + id, + geometry: element.geometry(scale.into()), + } + } +} + +impl RenderElement for HolepunchRenderElement +where + R: Renderer, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + frame.clear( + Color32F::TRANSPARENT, + &damage + .iter() + .cloned() + .map(|mut d| { + d.loc += dst.loc; + d + }) + .collect::>(), + ) + } +} + +impl Element for HolepunchRenderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + CommitCounter::default() + } + + fn src(&self) -> Rectangle { + Rectangle::default() + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + self.geometry + } + + fn transform(&self) -> Transform { + Transform::Normal + } + + fn opaque_regions(&self, _scale: Scale) -> OpaqueRegions { + OpaqueRegions::from_slice(&[Rectangle::from_size(self.geometry.size)]) + } +} + +pub struct OverlayPlaneElement { + id: Id, + geometry: Rectangle, + opaque_regions: OpaqueRegions, +} + +impl OverlayPlaneElement { + pub fn from_render_element(id: Id, element: &E, scale: impl Into>) -> Option + where + R: Renderer, + E: RenderElement, + { + let scale = scale.into(); + let opaque_regions = element.opaque_regions(scale); + let geometry = element.geometry(scale); + + // We use the opaque regions to create a bounding box around them to + // create a fake geometry that only includes the opaque regions. + // If the element is not fully opaque this can save the renderer + // from some damage when the element is moved. + let mut opaque_geometry = opaque_regions + .iter() + .fold(Rectangle::default(), |acc, item| acc.merge(*item)); + + if opaque_geometry.is_empty() { + return None; + } + + // Because we will move the geometry by the top-left corner of + // the opaque geometry we calculated we have to subtract + // that from the element local opaque regions. + let opaque_regions = opaque_regions + .into_iter() + .map(|mut region| { + region.loc -= opaque_geometry.loc; + region + }) + .collect(); + + // Move the opaque geometry relative to the original + // element location + opaque_geometry.loc += geometry.loc; + + Some(OverlayPlaneElement { + id, + geometry: opaque_geometry, + opaque_regions, + }) + } +} + +impl Element for OverlayPlaneElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + CommitCounter::default() + } + + fn src(&self) -> Rectangle { + Rectangle::default() + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + self.geometry + } + + fn transform(&self) -> Transform { + Transform::Normal + } + + fn damage_since(&self, _scale: Scale, _commit: Option) -> DamageSet { + DamageSet::default() + } + + fn opaque_regions(&self, _scale: Scale) -> OpaqueRegions { + OpaqueRegions::from_slice(&self.opaque_regions) + } +} + +impl RenderElement for OverlayPlaneElement +where + R: Renderer, +{ + fn draw( + &self, + _frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + // We do not actually draw anything here + Ok(()) + } +} diff --git a/vendor/smithay/src/backend/drm/compositor/frame_result.rs b/vendor/smithay/src/backend/drm/compositor/frame_result.rs new file mode 100644 index 0000000..aba827e --- /dev/null +++ b/vendor/smithay/src/backend/drm/compositor/frame_result.rs @@ -0,0 +1,456 @@ +use std::collections::HashSet; + +use crate::{ + backend::{ + allocator::{ + Buffer, Slot, + dmabuf::{AsDmabuf, Dmabuf}, + }, + drm::Framebuffer, + renderer::{ + Bind, Blit, Color32F, Frame, Renderer, + damage::OutputDamageTracker, + element::{Element, Id, RenderElement, RenderElementStates}, + sync::SyncPoint, + utils::{CommitCounter, DamageSet, DamageSnapshot, OpaqueRegions}, + }, + }, + output::OutputNoMode, + utils::{Buffer as BufferCoords, Physical, Point, Rectangle, Scale, Size, Transform}, +}; + +use super::{DrmScanoutBuffer, ScanoutBuffer}; + +/// Result for [`DrmCompositor::render_frame`][super::DrmCompositor::render_frame] +/// +/// **Note**: This struct may contain a reference to the composited buffer +/// of the primary display plane. Dropping it will remove said reference and +/// allows the buffer to be reused. +/// +/// Keeping the buffer longer may cause the following issues: +/// - **Too much damage** - until the buffer is marked free it is not considered +/// submitted by the swapchain, causing the age value of newly queried buffers +/// to be lower than necessary, potentially resulting in more rendering than necessary. +/// To avoid this make sure the buffer is dropped before starting the next render. +/// - **Exhaustion of swapchain images** - Continuing rendering while holding on +/// to too many buffers may cause the swapchain to run out of images, returning errors +/// on rendering until buffers are freed again. The exact amount of images in a +/// swapchain is an implementation detail, but should generally be expect to be +/// large enough to hold onto at least one `RenderFrameResult`. +pub struct RenderFrameResult<'a, B: Buffer, F: Framebuffer, E> { + /// If this frame contains any changes and should be submitted + pub is_empty: bool, + /// The render element states of this frame + pub states: RenderElementStates, + /// Element for the primary plane + pub primary_element: PrimaryPlaneElement<'a, B, F, E>, + /// Overlay elements in front to back order + pub overlay_elements: Vec<&'a E>, + /// Optional cursor plane element + /// + /// If set always above all other elements + pub cursor_element: Option<&'a E>, + + pub(super) primary_plane_element_id: Id, + pub(super) supports_fencing: bool, +} + +impl RenderFrameResult<'_, B, F, E> { + /// Returns if synchronization with kms submission can't be guaranteed through the available apis. + pub fn needs_sync(&self) -> bool { + if let PrimaryPlaneElement::Swapchain(ref element) = self.primary_element { + !self.supports_fencing || !element.sync.is_exportable() + } else { + false + } + } +} + +struct SwapchainElement<'a, 'b, B: Buffer> { + id: Id, + slot: &'a Slot, + transform: Transform, + damage: &'b DamageSnapshot, +} + +impl Element for SwapchainElement<'_, '_, B> { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.damage.current_commit() + } + + fn src(&self) -> Rectangle { + Rectangle::from_size(self.slot.size()).to_f64() + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + Rectangle::from_size(self.slot.size().to_logical(1, self.transform).to_physical(1)) + } + + fn transform(&self) -> Transform { + self.transform + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + self.damage + .damage_since(commit) + .map(|d| { + d.into_iter() + .map(|d| d.to_logical(1, self.transform, &self.slot.size()).to_physical(1)) + .collect() + }) + .unwrap_or_else(|| DamageSet::from_slice(&[self.geometry(scale)])) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + OpaqueRegions::from_slice(&[self.geometry(scale)]) + } +} + +enum FrameResultDamageElement<'a, 'b, E, B: Buffer> { + Element(&'a E), + Swapchain(SwapchainElement<'a, 'b, B>), +} + +impl Element for FrameResultDamageElement<'_, '_, E, B> +where + E: Element, + B: Buffer, +{ + fn id(&self) -> &Id { + match self { + FrameResultDamageElement::Element(e) => e.id(), + FrameResultDamageElement::Swapchain(e) => e.id(), + } + } + + fn current_commit(&self) -> CommitCounter { + match self { + FrameResultDamageElement::Element(e) => e.current_commit(), + FrameResultDamageElement::Swapchain(e) => e.current_commit(), + } + } + + fn src(&self) -> Rectangle { + match self { + FrameResultDamageElement::Element(e) => e.src(), + FrameResultDamageElement::Swapchain(e) => e.src(), + } + } + + fn geometry(&self, scale: Scale) -> Rectangle { + match self { + FrameResultDamageElement::Element(e) => e.geometry(scale), + FrameResultDamageElement::Swapchain(e) => e.geometry(scale), + } + } + + fn location(&self, scale: Scale) -> Point { + match self { + FrameResultDamageElement::Element(e) => e.location(scale), + FrameResultDamageElement::Swapchain(e) => e.location(scale), + } + } + + fn transform(&self) -> Transform { + match self { + FrameResultDamageElement::Element(e) => e.transform(), + FrameResultDamageElement::Swapchain(e) => e.transform(), + } + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + match self { + FrameResultDamageElement::Element(e) => e.damage_since(scale, commit), + FrameResultDamageElement::Swapchain(e) => e.damage_since(scale, commit), + } + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + match self { + FrameResultDamageElement::Element(e) => e.opaque_regions(scale), + FrameResultDamageElement::Swapchain(e) => e.opaque_regions(scale), + } + } +} + +#[derive(Debug)] +/// Defines the element for the primary plane +pub enum PrimaryPlaneElement<'a, B: Buffer, F: Framebuffer, E> { + /// A slot from the swapchain was used for rendering + /// the primary plane + Swapchain(PrimarySwapchainElement), + /// An element has been assigned for direct scan-out + Element(&'a E), +} + +/// Error for [`RenderFrameResult::blit_frame_result`] +#[derive(Debug, thiserror::Error)] +pub enum BlitFrameResultError { + /// A render error occurred + #[error(transparent)] + Rendering(R), + /// A error occurred during exporting the buffer + #[error(transparent)] + Export(E), +} + +impl RenderFrameResult<'_, B, F, E> +where + B: Buffer, + F: Framebuffer, +{ + /// Get the damage of this frame for the specified dtr and age + pub fn damage_from_age<'d>( + &self, + damage_tracker: &'d mut OutputDamageTracker, + age: usize, + filter: impl IntoIterator, + ) -> Result<(Option<&'d Vec>>, RenderElementStates), OutputNoMode> + where + E: Element, + { + #[allow(clippy::mutable_key_type)] + let filter_ids: HashSet = filter.into_iter().collect(); + + let mut elements: Vec> = + Vec::with_capacity(usize::from(self.cursor_element.is_some()) + self.overlay_elements.len() + 1); + if let Some(cursor) = self.cursor_element { + if !filter_ids.contains(cursor.id()) { + elements.push(FrameResultDamageElement::Element(cursor)); + } + } + + elements.extend( + self.overlay_elements + .iter() + .filter(|e| !filter_ids.contains(e.id())) + .map(|e| FrameResultDamageElement::Element(*e)), + ); + + let primary_render_element = match &self.primary_element { + PrimaryPlaneElement::Swapchain(PrimarySwapchainElement { + slot, + transform, + damage, + .. + }) => FrameResultDamageElement::Swapchain(SwapchainElement { + id: self.primary_plane_element_id.clone(), + transform: *transform, + slot: match &slot.buffer { + ScanoutBuffer::Swapchain(slot) => slot, + _ => unreachable!(), + }, + damage, + }), + PrimaryPlaneElement::Element(e) => FrameResultDamageElement::Element(*e), + }; + + elements.push(primary_render_element); + + damage_tracker.damage_output(age, &elements) + } +} + +impl<'a, B, F, E> RenderFrameResult<'a, B, F, E> +where + B: Buffer + AsDmabuf, + ::Error: std::fmt::Debug, + F: Framebuffer, +{ + /// Blit the frame result + #[allow(clippy::too_many_arguments)] + pub fn blit_frame_result( + &self, + size: impl Into>, + transform: Transform, + scale: impl Into>, + renderer: &mut R, + framebuffer: &mut R::Framebuffer<'_>, + damage: impl IntoIterator>, + filter: impl IntoIterator, + ) -> Result::Error>> + where + R: Renderer + Bind + Blit, + R::TextureId: 'static, + E: Element + RenderElement, + { + let size = size.into(); + let scale = scale.into(); + #[allow(clippy::mutable_key_type)] + let filter_ids: HashSet = filter.into_iter().collect(); + let damage = damage.into_iter().collect::>(); + + // If we have no damage we can exit early + if damage.is_empty() { + return Ok(SyncPoint::signaled()); + } + + let mut opaque_regions: Vec> = Vec::new(); + + let mut elements_to_render: Vec<&'a E> = + Vec::with_capacity(usize::from(self.cursor_element.is_some()) + self.overlay_elements.len() + 1); + + if let Some(cursor_element) = self.cursor_element.as_ref() { + if !filter_ids.contains(cursor_element.id()) { + elements_to_render.push(*cursor_element); + opaque_regions.extend(cursor_element.opaque_regions(scale)); + } + } + + for element in self + .overlay_elements + .iter() + .filter(|e| !filter_ids.contains(e.id())) + { + elements_to_render.push(element); + opaque_regions.extend(element.opaque_regions(scale)); + } + + let primary_dmabuf = match &self.primary_element { + PrimaryPlaneElement::Swapchain(PrimarySwapchainElement { slot, sync, .. }) => { + let dmabuf = match &slot.buffer { + ScanoutBuffer::Swapchain(slot) => slot.export().map_err(BlitFrameResultError::Export)?, + _ => unreachable!(), + }; + let size = dmabuf.size(); + let geometry = Rectangle::from_size(size.to_logical(1, Transform::Normal).to_physical(1)); + opaque_regions.push(geometry); + Some((sync.clone(), dmabuf, geometry)) + } + PrimaryPlaneElement::Element(e) => { + elements_to_render.push(*e); + opaque_regions.extend(e.opaque_regions(scale)); + None + } + }; + + let clear_damage = + Rectangle::subtract_rects_many_in_place(damage.clone(), opaque_regions.iter().copied()); + + let mut sync: Option = None; + if !clear_damage.is_empty() { + tracing::trace!("clearing frame damage {:#?}", clear_damage); + + let mut frame = renderer + .render(framebuffer, size, transform) + .map_err(BlitFrameResultError::Rendering)?; + + frame + .clear(Color32F::BLACK, &clear_damage) + .map_err(BlitFrameResultError::Rendering)?; + + sync = Some(frame.finish().map_err(BlitFrameResultError::Rendering)?); + } + + // first do the potential blit + if let Some((primary_dmabuf_sync, mut dmabuf, geometry)) = primary_dmabuf { + let blit_damage = damage + .iter() + .filter_map(|d| d.intersection(geometry)) + .collect::>(); + + tracing::trace!("blitting frame with damage: {:#?}", blit_damage); + + renderer + .wait(&primary_dmabuf_sync) + .map_err(BlitFrameResultError::Rendering)?; + let fb = renderer + .bind(&mut dmabuf) + .map_err(BlitFrameResultError::Rendering)?; + for rect in blit_damage { + // TODO: On Vulkan, may need to combine sync points instead of just using latest? + sync = Some( + renderer + .blit( + &fb, + framebuffer, + rect, + rect, + crate::backend::renderer::TextureFilter::Linear, + ) + .map_err(BlitFrameResultError::Rendering)?, + ); + } + } + + // then render the remaining elements if any + if !elements_to_render.is_empty() { + tracing::trace!("drawing {} frame element(s)", elements_to_render.len()); + + let mut frame = renderer + .render(framebuffer, size, transform) + .map_err(BlitFrameResultError::Rendering)?; + + for element in elements_to_render.iter().rev() { + let src = element.src(); + let dst = element.geometry(scale); + let element_damage = damage + .iter() + .filter_map(|d| { + d.intersection(dst).map(|mut d| { + d.loc -= dst.loc; + d + }) + }) + .collect::>(); + + // no need to render without damage + if element_damage.is_empty() { + continue; + } + + tracing::trace!("drawing frame element with damage: {:#?}", element_damage); + + element + .draw(&mut frame, src, dst, &element_damage, &[], None) + .map_err(BlitFrameResultError::Rendering)?; + } + + Ok(frame.finish().map_err(BlitFrameResultError::Rendering)?) + } else { + Ok(sync.unwrap_or_default()) + } + } +} + +impl std::fmt::Debug + for RenderFrameResult<'_, B, F, E> +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RenderFrameResult") + .field("is_empty", &self.is_empty) + .field("states", &self.states) + .field("primary_element", &self.primary_element) + .field("overlay_elements", &self.overlay_elements) + .field("cursor_element", &self.cursor_element) + .finish() + } +} + +#[derive(Debug)] +/// Defines the element for the primary plane in cases where a composited buffer was used. +pub struct PrimarySwapchainElement { + /// The slot from the swapchain + pub(super) slot: DrmScanoutBuffer, + /// Sync point + pub sync: SyncPoint, + /// The transform applied during rendering + pub transform: Transform, + /// The damage on the primary plane + pub damage: DamageSnapshot, +} + +impl PrimarySwapchainElement { + /// Access the underlying swapchain buffer + #[inline] + pub fn buffer(&self) -> &B { + match &self.slot.buffer { + ScanoutBuffer::Swapchain(slot) => slot, + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/backend/drm/compositor/mod.rs b/vendor/smithay/src/backend/drm/compositor/mod.rs new file mode 100644 index 0000000..3f471cf --- /dev/null +++ b/vendor/smithay/src/backend/drm/compositor/mod.rs @@ -0,0 +1,4445 @@ +//! Composition for [`Element`]s using drm planes +//! +//! When possible composition can be (partially) offloaded to the display driver by assigning +//! elements to drm planes. This is especially important for latency intensive fullscreen clients +//! like video renderers or games. +//! +//! The [`DrmCompositor`] does so by walking the stack of provided [`Element`]s from front to back +//! while trying to assign each element to a drm overlay plane. Each item that fails the plane test +//! will be rendered on the primary plane using the provided [`Renderer`]. +//! Additionally it will try to assign the top most element that fit's into the cursor size (as specified +//! by the [`DrmDevice`](crate::backend::drm::DrmDevice)) on the cursor plane. If the element can not be +//! directly scanned out, pixman will be used to render the element. +//! +//! Note: While the [`DrmCompositor`] also works on *legacy* drm the use of overlay and cursor planes is disabled in that case. +//! Direct scan-out will only work with an atomic [`DrmSurface`]. +//! +//! ## What makes a [`Element`] eligible for direct scan-out +//! +//! ### General +//! +//! First the element has to provide a [`UnderlyingStorage`] which can be exported as a drm framebuffer. +//! Currently this is limited to wayland buffers, but may be extended in the future. +//! This module provides a default exporter based on [`gbm`] which should fit most use-cases. +//! +//! If a certain combination of elements works can only be determined by asking the driver by submitting +//! a atomic commit test. If that test fails the element is scheduled to be rendered on the primary plane. +//! +//! ### Overlay planes +//! +//! The element can only be directly scanned out if it's geometry does not overlap with an already assigned +//! element on a plane higher in the stack. +//! +//! ### Underlay planes +//! +//! An underlay plane is only used if it does not overlap with an already assigned plane lower in the stack +//! and the element is fully opaque. +//! +//! ### Primary plane +//! +//! For an element to be considered to be directly scanned out on the primary plane it has to be the last remaining +//! visible element on the output and no other element has been assigned to the primary plane. If there are multiple +//! element assigned to the primary plane the renderer will be used to composite the primary plane into a allocator +//! provided buffer. Additionally the element has to be either fully opaque or the clear color has to match the CRTC +//! background color and no overlap with an underlay is found. +//! +//! # How to use it +//! +//! ```no_run +//! # use smithay::backend::{ +//! # allocator::gbm::{GbmAllocator, GbmDevice}, +//! # drm::{DrmDevice, DrmDeviceFd}, +//! # renderer::{ +//! # element::surface::WaylandSurfaceRenderElement, +//! # gles::{GlesTexture, GlesRenderer}, +//! # }, +//! # }; +//! # use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier}; +//! # use std::{collections::HashSet, mem::MaybeUninit}; +//! # +//! use smithay::{ +//! backend::drm::{ +//! compositor::{DrmCompositor, FrameFlags}, +//! exporter::gbm::GbmFramebufferExporter, +//! DrmSurface, +//! }, +//! output::{Output, PhysicalProperties, Subpixel}, +//! utils::Size, +//! }; +//! +//! // ...initialize the output, drm device, drm surface and allocator +//! # +//! # const CLEAR_COLOR: [f32; 4] = [0f32, 0f32, 0f32, 0f32]; +//! # +//! let output = Output::new( +//! "e-DP".into(), +//! PhysicalProperties { +//! size: Size::from((800, 600)), +//! make: "N/A".into(), +//! model: "N/A".into(), +//! subpixel: Subpixel::Unknown, +//! serial_number: "N/A".into(), +//! }, +//! ); +//! +//! # let device: DrmDevice = todo!(); +//! # let surface: DrmSurface = todo!(); +//! # let allocator: GbmAllocator = todo!(); +//! # let exporter: GbmFramebufferExporter = todo!(); +//! # let color_formats = [DrmFourcc::Argb8888]; +//! # let renderer_formats = HashSet::from([DrmFormat { +//! # code: DrmFourcc::Argb8888, +//! # modifier: DrmModifier::Linear, +//! # }]); +//! # let gbm: GbmDevice = todo!(); +//! # let mut renderer: GlesRenderer = todo!(); +//! # +//! let mut compositor: DrmCompositor<_, _, (), _> = DrmCompositor::new( +//! &output, +//! surface, +//! None, +//! allocator, +//! exporter, +//! color_formats, +//! renderer_formats, +//! device.cursor_size(), +//! Some(gbm), +//! ) +//! .expect("failed to initialize drm compositor"); +//! +//! # let elements: Vec> = Vec::new(); +//! let render_frame_result = compositor +//! .render_frame::<_, _>(&mut renderer, &elements, CLEAR_COLOR, FrameFlags::DEFAULT) +//! .expect("failed to render frame"); +//! +//! if !render_frame_result.is_empty { +//! compositor.queue_frame(()).expect("failed to queue frame"); +//! +//! // ...wait for VBlank event +//! +//! compositor +//! .frame_submitted() +//! .expect("failed to mark frame as submitted"); +//! } else { +//! // ...re-schedule frame +//! } +//! ``` +use std::{ + collections::HashMap, + fmt::Debug, + io::ErrorKind, + os::unix::io::{AsFd, OwnedFd}, + str::FromStr, + sync::Arc, +}; + +use drm::{ + Device, DriverCapability, + control::{Device as _, Mode, PlaneType, connector, crtc, framebuffer, plane}, +}; +use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier}; +use indexmap::{IndexMap, IndexSet}; +use smallvec::SmallVec; +use tracing::{debug, error, info, info_span, instrument, trace, warn}; +use wayland_server::{Resource, protocol::wl_buffer::WlBuffer}; + +#[cfg(feature = "renderer_pixman")] +use crate::backend::renderer::{ + Frame as _, ImportAll, + pixman::{PixmanError, PixmanRenderer, PixmanTexture}, +}; +use crate::{ + backend::{ + SwapBuffersError, + allocator::{ + Allocator, Buffer, Slot, Swapchain, + dmabuf::{AsDmabuf, Dmabuf}, + format::{get_opaque, has_alpha}, + gbm::{GbmAllocator, GbmBuffer, GbmBufferFlags, GbmDevice}, + }, + drm::{DrmError, PlaneDamageClips, plane_has_property}, + renderer::{ + Bind, Color32F, DebugFlags, Renderer, RendererSuper, Texture, buffer_y_inverted, + damage::{Error as OutputDamageTrackerError, OutputDamageTracker}, + element::{ + Element, Id, Kind, RenderElement, RenderElementPresentationState, RenderElementState, + RenderElementStates, RenderingReason, UnderlyingStorage, + }, + sync::SyncPoint, + utils::{CommitCounter, DamageBag}, + }, + }, + output::OutputModeSource, + utils::{Buffer as BufferCoords, DevPath, Physical, Point, Rectangle, Scale, Size, Transform}, + wayland::{shm, single_pixel_buffer}, +}; + +use super::{ + DrmSurface, Framebuffer, PlaneClaim, PlaneInfo, Planes, + error::AccessError, + exporter::{ExportBuffer, ExportFramebuffer, gbm::GbmFramebufferExporter, gbm::NodeFilter}, + surface::VrrSupport, +}; + +mod elements; +mod frame_result; + +use elements::*; +pub use frame_result::*; + +impl RenderElementState { + pub(crate) fn zero_copy(visible_area: usize) -> Self { + RenderElementState { + visible_area, + presentation_state: RenderElementPresentationState::ZeroCopy, + needs_capture: false, + } + } + + pub(crate) fn rendering_with_reason(reason: RenderingReason) -> Self { + RenderElementState { + visible_area: 0, + presentation_state: RenderElementPresentationState::Rendering { reason: Some(reason) }, + needs_capture: false, + } + } +} + +#[allow(dead_code)] // This structs purpose is to keep buffer objects alive, most variants won't be read +#[derive(Debug)] +enum ScanoutBuffer { + Wayland(crate::backend::renderer::utils::Buffer), + Swapchain(Arc>), + Cursor(Arc), +} + +impl Clone for ScanoutBuffer { + fn clone(&self) -> Self { + match self { + Self::Wayland(arg0) => Self::Wayland(arg0.clone()), + Self::Swapchain(arg0) => Self::Swapchain(arg0.clone()), + Self::Cursor(arg0) => Self::Cursor(arg0.clone()), + } + } +} + +impl ScanoutBuffer { + fn acquire_point( + &self, + signaled_fence: Option<&Arc>, + ) -> Option<(SyncPoint, Option>)> { + if let Self::Wayland(buffer) = self { + // Assume `DrmSyncobjBlocker` is used, so acquire point has already + // been signaled. Instead of converting with `SyncPoint::from`. + if buffer.acquire_point().is_some() { + return Some((SyncPoint::signaled(), signaled_fence.cloned())); + } + } + None + } +} + +impl ScanoutBuffer { + #[inline] + fn from_underlying_storage(storage: UnderlyingStorage<'_>) -> Option { + match storage { + UnderlyingStorage::Wayland(buffer) => Some(Self::Wayland(buffer.clone())), + UnderlyingStorage::Memory { .. } => None, + } + } +} + +enum DrmFramebuffer { + Exporter(F), + Gbm(super::gbm::GbmFramebuffer), +} + +impl AsRef for DrmFramebuffer +where + F: Framebuffer, +{ + #[inline] + fn as_ref(&self) -> &framebuffer::Handle { + match self { + DrmFramebuffer::Exporter(e) => e.as_ref(), + DrmFramebuffer::Gbm(g) => g.as_ref(), + } + } +} + +impl Framebuffer for DrmFramebuffer +where + F: Framebuffer, +{ + #[inline] + fn format(&self) -> drm_fourcc::DrmFormat { + match self { + DrmFramebuffer::Exporter(e) => e.format(), + DrmFramebuffer::Gbm(g) => g.format(), + } + } +} + +impl std::fmt::Debug for DrmFramebuffer +where + F: Framebuffer + std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Exporter(arg0) => f.debug_tuple("Exporter").field(arg0).finish(), + Self::Gbm(arg0) => f.debug_tuple("Gbm").field(arg0).finish(), + } + } +} + +struct DrmScanoutBuffer { + buffer: ScanoutBuffer, + fb: CachedDrmFramebuffer, +} + +impl Clone for DrmScanoutBuffer { + fn clone(&self) -> Self { + DrmScanoutBuffer { + buffer: self.buffer.clone(), + fb: self.fb.clone(), + } + } +} + +impl std::fmt::Debug for DrmScanoutBuffer +where + B: Buffer + std::fmt::Debug, + F: Framebuffer + std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DrmScanoutBuffer") + .field("buffer", &self.buffer) + .field("fb", &self.fb) + .finish() + } +} + +impl AsRef for DrmScanoutBuffer { + #[inline] + fn as_ref(&self) -> &drm::control::framebuffer::Handle { + self.fb.as_ref() + } +} + +impl Framebuffer for DrmScanoutBuffer { + #[inline] + fn format(&self) -> drm_fourcc::DrmFormat { + self.fb.format() + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum ElementFramebufferCacheBuffer { + Wayland(wayland_server::Weak), +} + +impl ElementFramebufferCacheBuffer { + #[inline] + fn from_underlying_storage(storage: &UnderlyingStorage<'_>) -> Option { + match storage { + UnderlyingStorage::Wayland(buffer) => Some(Self::Wayland(buffer.downgrade())), + UnderlyingStorage::Memory { .. } => None, + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct ElementFramebufferCacheKey { + allow_opaque_fallback: bool, + buffer: ElementFramebufferCacheBuffer, +} + +impl ElementFramebufferCacheKey { + #[inline] + fn from_underlying_storage(storage: &UnderlyingStorage<'_>, allow_opaque_fallback: bool) -> Option { + let buffer = ElementFramebufferCacheBuffer::from_underlying_storage(storage)?; + Some(Self { + allow_opaque_fallback, + buffer, + }) + } +} + +impl ElementFramebufferCacheKey { + #[inline] + fn is_alive(&self) -> bool { + match self.buffer { + ElementFramebufferCacheBuffer::Wayland(ref buffer) => buffer.is_alive(), + } + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +struct PlanesSnapshot { + primary: bool, + cursor_bitmask: u32, + overlay_bitmask: u32, +} + +#[derive(Debug)] +struct ElementInstanceState { + properties: PlaneProperties, + active_planes: PlanesSnapshot, + failed_planes: PlanesSnapshot, +} + +#[derive(Debug)] +struct ElementState { + instances: SmallVec<[ElementInstanceState; 1]>, + fb_cache: ElementFramebufferCache, +} + +#[derive(Debug)] +struct ElementFramebufferCache +where + B: Framebuffer, +{ + /// Cache for framebuffer handles per cache key (e.g. wayland buffer) + fb_cache: SmallVec< + [( + ElementFramebufferCacheKey, + Result, ExportBufferError>, + ); 4], + >, +} + +impl ElementFramebufferCache +where + B: Framebuffer, +{ + #[inline] + fn get( + &self, + cache_key: &ElementFramebufferCacheKey, + ) -> Option, ExportBufferError>> { + self.fb_cache.iter().find_map(|(k, r)| { + if k == cache_key { + Some(r.as_ref().map_err(|err| *err)) + } else { + None + } + }) + } + + #[inline] + fn insert( + &mut self, + cache_key: ElementFramebufferCacheKey, + fb: Result, ExportBufferError>, + ) { + self.fb_cache.push((cache_key, fb)); + } + + fn cleanup(&mut self) { + self.fb_cache.retain(|(key, _)| key.is_alive()); + } +} + +impl Default for ElementFramebufferCache +where + B: Framebuffer, +{ + #[inline] + fn default() -> Self { + Self { + fb_cache: Default::default(), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +struct PlaneProperties { + pub src: Rectangle, + pub dst: Rectangle, + pub transform: Transform, + pub alpha: f32, + pub format: DrmFormat, +} + +impl PlaneProperties { + #[inline] + fn is_compatible(&self, other: &PlaneProperties) -> bool { + self.src == other.src + && self.dst == other.dst + && self.transform == other.transform + && self.alpha == other.alpha + && self.format == other.format + } +} + +struct ElementPlaneConfig<'a, B: Buffer, F: Framebuffer> { + z_index: usize, + geometry: Rectangle, + properties: PlaneProperties, + buffer: DrmScanoutBuffer, + failed_planes: &'a mut PlanesSnapshot, +} + +#[derive(Debug)] +struct PlaneConfig { + pub properties: PlaneProperties, + pub buffer: DrmScanoutBuffer, + pub damage_clips: Option, + pub plane_claim: PlaneClaim, + pub sync: Option<(SyncPoint, Option>)>, +} + +impl PlaneConfig { + #[inline] + pub fn is_compatible(&self, other: &PlaneConfig) -> bool { + self.properties.is_compatible(&other.properties) + } +} + +impl Clone for PlaneConfig { + #[inline] + fn clone(&self) -> Self { + Self { + properties: self.properties, + buffer: self.buffer.clone(), + damage_clips: self.damage_clips.clone(), + plane_claim: self.plane_claim.clone(), + sync: self.sync.clone(), + } + } +} + +#[derive(Debug, Clone)] +struct PlaneElementState { + id: Id, + commit: CommitCounter, + z_index: usize, + cursor_size: Option>, +} + +#[derive(Debug)] +struct PlaneState { + skip: bool, + needs_test: bool, + element_state: Option, + config: Option>, +} + +impl Default for PlaneState { + #[inline] + fn default() -> Self { + Self { + skip: true, + needs_test: false, + element_state: Default::default(), + config: Default::default(), + } + } +} + +impl PlaneState { + #[inline] + fn buffer(&self) -> Option<&DrmScanoutBuffer> { + self.config.as_ref().map(|config| &config.buffer) + } + + #[inline] + fn is_compatible(&self, other: &Self) -> bool { + match (self.config.as_ref(), other.config.as_ref()) { + (Some(a), Some(b)) => a.is_compatible(b), + (None, None) => true, + _ => false, + } + } +} + +impl Clone for PlaneState { + #[inline] + fn clone(&self) -> Self { + Self { + skip: self.skip, + needs_test: self.needs_test, + element_state: self.element_state.clone(), + config: self.config.clone(), + } + } +} + +#[derive(Debug)] +struct FrameState { + planes: SmallVec<[(plane::Handle, PlaneState); 10]>, +} + +impl FrameState { + #[inline] + fn is_assigned(&self, handle: plane::Handle) -> bool { + self.planes + .iter() + .find_map(|(p, state)| { + if *p == handle { + Some(state.config.is_some()) + } else { + None + } + }) + .unwrap_or(false) + } + + #[inline] + fn overlaps(&self, handle: plane::Handle, element_geometry: Rectangle) -> bool { + self.planes + .iter() + .find(|(p, _)| *p == handle) + .and_then(|(_, state)| { + state + .config + .as_ref() + .map(|config| config.properties.dst.overlaps(element_geometry)) + }) + .unwrap_or(false) + } + + #[inline] + fn plane_state(&self, handle: plane::Handle) -> Option<&PlaneState> { + self.planes + .iter() + .find_map(|(p, state)| if *p == handle { Some(state) } else { None }) + } + + #[inline] + fn plane_state_mut(&mut self, handle: plane::Handle) -> Option<&mut PlaneState> { + self.planes + .iter_mut() + .find_map(|(p, state)| if *p == handle { Some(state) } else { None }) + } + + #[inline] + fn plane_properties(&self, handle: plane::Handle) -> Option<&PlaneProperties> { + self.plane_state(handle) + .and_then(|state| state.config.as_ref()) + .map(|config| &config.properties) + } + + #[inline] + fn plane_buffer(&self, handle: plane::Handle) -> Option<&DrmScanoutBuffer> { + self.plane_state(handle) + .and_then(|state| state.config.as_ref().map(|config| &config.buffer)) + } +} + +impl FrameState { + fn from_planes(primary_plane: plane::Handle, planes: &Planes) -> Self { + let mut tmp = SmallVec::with_capacity(planes.overlay.len() + planes.cursor.len() + 1); + tmp.push((primary_plane, PlaneState::default())); + tmp.extend( + planes + .cursor + .iter() + .map(|info| (info.handle, PlaneState::default())), + ); + tmp.extend( + planes + .overlay + .iter() + .map(|info| (info.handle, PlaneState::default())), + ); + + FrameState { planes: tmp } + } +} + +impl FrameState { + #[profiling::function] + #[inline] + fn set_state(&mut self, plane: plane::Handle, state: PlaneState) { + let current_config = match self.plane_state_mut(plane) { + Some(config) => config, + None => return, + }; + *current_config = state; + } + + #[profiling::function] + fn test_state( + &mut self, + surface: &DrmSurface, + supports_fencing: bool, + plane: plane::Handle, + state: PlaneState, + allow_modeset: bool, + ) -> Result<(), DrmError> { + let current_config = match self.plane_state_mut(plane) { + Some(config) => config, + None => return Ok(()), + }; + let backup = current_config.clone(); + *current_config = state; + + let res = surface.test_state(self.build_planes(surface, supports_fencing, true), allow_modeset); + + if res.is_err() { + // test failed, restore previous state + *self.plane_state_mut(plane).unwrap() = backup; + } else { + self.planes + .iter_mut() + .for_each(|(_, state)| state.needs_test = false); + } + + res + } + + #[profiling::function] + fn test_state_complete( + &mut self, + previous_frame: &Self, + surface: &DrmSurface, + supports_fencing: bool, + allow_modeset: bool, + allow_partial_update: bool, + ) -> Result<(), DrmError> { + let needs_test = self.planes.iter().any(|(_, state)| state.needs_test); + let is_fully_compatible = self.planes.iter().all(|(handle, state)| { + previous_frame + .plane_state(*handle) + .map(|other| state.is_compatible(other)) + .unwrap_or(false) + }); + + if allow_partial_update && (!needs_test || is_fully_compatible) { + trace!("skipping fully compatible state test"); + self.planes + .iter_mut() + .for_each(|(_, state)| state.needs_test = false); + return Ok(()); + } + + let res = surface.test_state( + self.build_planes(surface, supports_fencing, allow_partial_update), + allow_modeset, + ); + + if res.is_ok() { + self.planes + .iter_mut() + .for_each(|(_, state)| state.needs_test = false); + } + + res + } + + #[profiling::function] + fn commit( + &mut self, + surface: &DrmSurface, + supports_fencing: bool, + allow_partial_update: bool, + event: bool, + ) -> Result<(), crate::backend::drm::error::Error> { + debug_assert!(!self.planes.iter().any(|(_, state)| state.needs_test)); + surface.commit( + self.build_planes(surface, supports_fencing, allow_partial_update), + event, + ) + } + + #[profiling::function] + fn page_flip( + &mut self, + surface: &DrmSurface, + supports_fencing: bool, + allow_partial_update: bool, + event: bool, + ) -> Result<(), crate::backend::drm::error::Error> { + debug_assert!(!self.planes.iter().any(|(_, state)| state.needs_test)); + surface.page_flip( + self.build_planes(surface, supports_fencing, allow_partial_update), + event, + ) + } + + #[profiling::function] + fn build_planes<'a>( + &'a mut self, + surface: &'a DrmSurface, + supports_fencing: bool, + allow_partial_update: bool, + ) -> impl IntoIterator> { + for (_, state) in self.planes.iter_mut().filter(|(_, state)| !state.skip) { + if let Some(config) = state.config.as_mut() { + // Try to extract a native fence out of the supplied sync point if any + // If the sync point has no native fence or the surface does not support + // fencing force a wait + if let Some((sync, fence)) = config.sync.as_mut() { + if supports_fencing && fence.is_none() { + *fence = sync.export().map(Arc::new); + } + } + } + } + + self.planes + .iter_mut() + .filter(move |(handle, state)| { + // If we are not allowed to do an partial update we want to update all + // planes we can claim. This makes sure we also reset planes we never + // actually used. We can skip getting a claim here if we have a + // config as this means we already claimed the plane for us. + if allow_partial_update { + // A partial update would technically only have to include planes that + // actually changed. This includes planes we previously used and have to + // reset and planes we use and want to update. + // Both is already encoded into state.skip, so this should be the only + // thing we have to consider here. + // + // But...Unfortunately some drivers seem to have issues with partial + // updates, at least when it does not contain the primary plane, resulting + // in strange issues like e.g. repeating plane content, side-scrolling planes, + // wrapping planes around edges... + // + // So until these things are fixed just always send the whole state. We do not + // have to send planes we never used, but we include planes we want to reset or + // that explicitly changed represented by !state.skip and all planes currently in + // use represented by having an config defined. + !state.skip || state.config.is_some() + } else { + state.config.is_some() || surface.claim_plane(*handle).is_some() + } + }) + .map(move |(handle, state)| super::surface::PlaneState { + handle: *handle, + config: state.config.as_mut().map(|config| super::PlaneConfig { + src: config.properties.src, + dst: config.properties.dst, + alpha: config.properties.alpha, + transform: config.properties.transform, + damage_clips: config.damage_clips.as_ref().map(|d| d.blob()), + fb: *config.buffer.as_ref(), + fence: config + .sync + .as_ref() + .and_then(|(_, fence)| fence.as_ref().map(|fence| fence.as_fd())), + }), + }) + } +} + +type CompositorFrameState = + FrameState<::Buffer, ::Buffer>>::Framebuffer>; + +type FrameErrorType = FrameError< + ::Error, + <::Buffer as AsDmabuf>::Error, + ::Buffer>>::Error, +>; + +pub(crate) type FrameResult = Result>; + +pub(crate) type RenderFrameErrorType = RenderFrameError< + ::Error, + <::Buffer as AsDmabuf>::Error, + ::Buffer>>::Error, + ::Error, +>; + +#[derive(Debug)] +struct CursorState { + allocator: GbmAllocator, + framebuffer_exporter: GbmFramebufferExporter, + previous_output_transform: Option, + previous_output_scale: Option>, + #[cfg(feature = "renderer_pixman")] + pixman_renderer: Option, +} + +#[derive(Debug, thiserror::Error, Copy, Clone)] +enum ExportBufferError { + #[error("the buffer has no underlying storage")] + NoUnderlyingStorage, + #[error("exporting the framebuffer failed")] + ExportFailed, + #[error("no framebuffer could be exported")] + Unsupported, +} + +impl From for Option { + #[inline] + fn from(err: ExportBufferError) -> Self { + if matches!(err, ExportBufferError::ExportFailed) { + // Export failed could mean the buffer could + // not be used to add a drm framebuffer. This + // especially can happen on kmsro devices where + // a buffer format not usable for scan-out can + // not be used to add a framebuffer + // We can try to give the client another chance + // by announcing a scan-out tranche + Some(RenderingReason::ScanoutFailed) + } else { + // We provide no reason for rendering here as there + // is no action that can be taken to make it work + None + } + } +} + +#[derive(Debug)] +struct OverlayPlaneElementIds { + plane_ids: Vec<(plane::Handle, Id, Id)>, +} + +impl OverlayPlaneElementIds { + fn from_planes(planes: &Planes) -> Self { + let overlay_plane_count = planes.overlay.len(); + + Self { + plane_ids: Vec::with_capacity(overlay_plane_count), + } + } + + fn plane_id_for_element_id(&mut self, plane: &plane::Handle, element_id: &Id) -> Id { + // Either get the existing plane id for the plane when the stored element id + // matches or generate a new Id (and update the element id) + let existing = self.plane_ids.iter_mut().find(|(p, _, _)| p == plane); + if let Some((_, plane_id, current_element_id)) = existing { + if current_element_id != element_id { + *plane_id = Id::new(); + *current_element_id = element_id.clone(); + } + + plane_id.clone() + } else { + let plane_id = Id::new(); + + self.plane_ids + .push((*plane, plane_id.clone(), element_id.clone())); + + plane_id + } + } + + fn contains_plane_id(&self, plane_id: &Id) -> bool { + self.plane_ids.iter().any(|(_, p, _)| p == plane_id) + } + + fn remove_plane(&mut self, plane: &plane::Handle) { + self.plane_ids.retain(|(p, _, _)| p != plane); + } +} + +struct PlaneAssignment { + handle: plane::Handle, + type_: PlaneType, +} + +impl From<&PlaneInfo> for PlaneAssignment { + #[inline] + fn from(value: &PlaneInfo) -> Self { + PlaneAssignment { + handle: value.handle, + type_: value.type_, + } + } +} + +struct PendingFrame::Buffer>, U> { + frame: CompositorFrameState, + user_data: U, +} + +impl std::fmt::Debug for PendingFrame +where + A: Allocator, + ::Buffer: std::fmt::Debug, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: std::fmt::Debug, + U: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PendingFrame") + .field("frame", &self.frame) + .field("user_data", &self.user_data) + .finish() + } +} + +struct QueuedFrame::Buffer>, U> { + prepared_frame: PreparedFrame, + user_data: U, +} + +impl std::fmt::Debug for QueuedFrame +where + A: Allocator, + ::Buffer: std::fmt::Debug, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: std::fmt::Debug, + U: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("QueuedFrame") + .field("prepared_frame", &self.prepared_frame) + .field("user_data", &self.user_data) + .finish() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum PreparedFrameKind { + Full, + Partial, +} + +struct PreparedFrame::Buffer>> { + frame: CompositorFrameState, + kind: PreparedFrameKind, +} + +impl::Buffer>> PreparedFrame { + #[inline] + fn is_empty(&self) -> bool { + // It can happen that we have no changes, but there is a pending commit or + // we are forced to do a full update in which case we just set the previous state again + self.kind == PreparedFrameKind::Partial && self.frame.planes.iter().all(|p| p.1.skip) + } +} + +impl std::fmt::Debug for PreparedFrame +where + A: Allocator, + ::Buffer: std::fmt::Debug, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PreparedFrame") + .field("frame", &self.frame) + .field("kind", &self.kind) + .finish() + } +} + +bitflags::bitflags! { + /// Possible flags for a DMA buffer + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct FrameFlags: u32 { + /// Allow to realize the frame by scanning out elements on the primary plane + /// with the same pixel format as the main swapchain + const ALLOW_PRIMARY_PLANE_SCANOUT = 1; + /// Allow to realize the frame by scanning out elements on the primary plane + /// regardless of their format + const ALLOW_PRIMARY_PLANE_SCANOUT_ANY = 2; + /// Allow to realize the frame by scanning out elements on overlay planes + const ALLOW_OVERLAY_PLANE_SCANOUT = 4; + /// Allow to realize the frame by scanning out elements on cursor planes + const ALLOW_CURSOR_PLANE_SCANOUT = 8; + /// Return `EmptyFrame`, if only the cursor plane would have been updated + const SKIP_CURSOR_ONLY_UPDATES = 16; + /// Allow to realize the frame by assigning elements on any plane + const ALLOW_SCANOUT = Self::ALLOW_PRIMARY_PLANE_SCANOUT.bits() | Self::ALLOW_OVERLAY_PLANE_SCANOUT.bits() | Self::ALLOW_CURSOR_PLANE_SCANOUT.bits(); + /// Safe default set of flags + const DEFAULT = Self::ALLOW_SCANOUT.bits(); + } +} + +/// Composite an output using a combination of planes and rendering +/// +/// see the [`module docs`](crate::backend::drm::compositor) for more information +#[derive(Debug)] +pub struct DrmCompositor +where + A: Allocator, + F: ExportFramebuffer, + >::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + output_mode_source: OutputModeSource, + surface: Arc, + planes: Planes, + overlay_plane_element_ids: OverlayPlaneElementIds, + damage_tracker: OutputDamageTracker, + primary_is_opaque: bool, + primary_plane_element_id: Id, + primary_plane_damage_bag: DamageBag, + supports_fencing: bool, + reset_pending: bool, + signaled_fence: Option>, + + framebuffer_exporter: F, + + current_frame: CompositorFrameState, + pending_frame: Option>, + queued_frame: Option>, + next_frame: Option>, + + swapchain: Swapchain, + + cursor_size: Size, + cursor_state: Option>, + + element_states: IndexMap>::Framebuffer>>, + previous_element_states: IndexMap>::Framebuffer>>, + opaque_regions: Vec>, + element_opaque_regions_workhouse: Vec>, + + debug_flags: DebugFlags, + span: tracing::Span, +} + +impl DrmCompositor +where + A: Allocator, + ::Error: std::error::Error + Send + Sync, + ::Buffer: AsDmabuf, + ::Error: std::error::Error + Send + Sync + std::fmt::Debug, + F: ExportFramebuffer, + >::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + >::Error: std::error::Error + Send + Sync, + G: AsFd + Clone, +{ + /// Initialize a new [`DrmCompositor`]. + /// + /// The [`OutputModeSource`] can be created from an [`Output`](crate::output::Output), which will automatically track + /// the output's mode changes. An [`OutputModeSource::Static`] variant should only be used when + /// manually updating modes using [`DrmCompositor::set_output_mode_source`]. + /// + /// - `output_mode_source` is used to determine the current mode, scale and transform + /// - `surface` for the compositor to use + /// - `planes` defines which planes the compositor is allowed to use for direct scan-out. + /// `None` will result in the compositor to use all planes as specified by [`DrmSurface::planes`] + /// - `allocator` used for the primary plane swapchain + /// - `color_formats` are tested in order until a working configuration is found + /// - `renderer_formats` as reported by the used renderer, used to build the intersection between + /// the possible scan-out formats of the primary plane and the renderer + /// - `framebuffer_exporter` is used to create drm framebuffers for the swapchain buffers (and if possible + /// for element buffers) for scan-out + /// - `cursor_size` as reported by the drm device, used for creating buffer for the cursor plane + /// - `gbm` device used for creating buffers for the cursor plane, `None` will disable the cursor plane + #[allow(clippy::too_many_arguments)] + #[instrument(skip_all)] + pub fn new( + output_mode_source: impl Into + Debug, + surface: DrmSurface, + planes: Option, + mut allocator: A, + framebuffer_exporter: F, + color_formats: impl IntoIterator, + renderer_formats: impl IntoIterator, + cursor_size: Size, + gbm: Option>, + ) -> FrameResult { + let signaled_fence = match surface.create_syncobj(true) { + Ok(signaled_syncobj) => match surface.syncobj_to_fd(signaled_syncobj, true) { + Ok(signaled_fence) => { + let _ = surface.destroy_syncobj(signaled_syncobj); + Some(Arc::new(signaled_fence)) + } + Err(err) => { + tracing::warn!(?err, "failed to export signaled syncobj"); + let _ = surface.destroy_syncobj(signaled_syncobj); + None + } + }, + Err(err) => { + tracing::warn!(?err, "failed to create signaled syncobj"); + None + } + }; + + let span = info_span!( + parent: None, + "drm_compositor", + device = ?surface.dev_path(), + crtc = ?surface.crtc(), + ); + + let output_mode_source = output_mode_source.into(); + let renderer_formats = renderer_formats.into_iter().collect::>(); + + let mut error = None; + let surface = Arc::new(surface); + let mut planes = match planes { + Some(planes) => planes, + None => surface.planes().clone(), + }; + + // We do not support direct scan-out on legacy + if surface.is_legacy() { + planes.cursor.clear(); + planes.overlay.clear(); + } + + // The selection algorithm expects the planes to be ordered form front to back + planes + .overlay + .sort_by_key(|p| std::cmp::Reverse(p.zpos.unwrap_or_default())); + + let driver = surface.get_driver().map_err(|err| { + FrameError::DrmError(DrmError::Access(AccessError { + errmsg: "Failed to query drm driver", + dev: surface.dev_path(), + source: err, + })) + })?; + // `IN_FENCE_FD` makes commit fail on Nvidia driver + // https://github.com/NVIDIA/open-gpu-kernel-modules/issues/622 + let is_nvidia = driver.name().to_string_lossy().to_lowercase().contains("nvidia") + || driver + .description() + .to_string_lossy() + .to_lowercase() + .contains("nvidia"); + + let cursor_size = Size::from((cursor_size.w as i32, cursor_size.h as i32)); + let damage_tracker = OutputDamageTracker::from_mode_source(output_mode_source.clone()); + let supports_fencing = !surface.is_legacy() + && surface + .get_driver_capability(DriverCapability::SyncObj) + .map(|val| val != 0) + .map_err(|err| { + FrameError::DrmError(DrmError::Access(AccessError { + errmsg: "Failed to query driver capability", + dev: surface.dev_path(), + source: err, + })) + })? + && plane_has_property(&*surface, surface.plane(), "IN_FENCE_FD")? + && !(is_nvidia && nvidia_drm_version().unwrap_or((0, 0, 0)) < (560, 35, 3)); + + for format in color_formats { + debug!("Testing color format: {}", format); + match Self::find_supported_format( + surface.clone(), + supports_fencing, + &planes, + allocator, + &framebuffer_exporter, + renderer_formats.clone(), + format, + ) { + Ok((swapchain, is_opaque)) => { + let cursor_state = gbm.map(|gbm| { + #[cfg(feature = "renderer_pixman")] + let pixman_renderer = match PixmanRenderer::new() { + Ok(pixman_renderer) => Some(pixman_renderer), + Err(err) => { + tracing::warn!(?err, "failed to initialize pixman renderer for cursor plane"); + None + } + }; + + let cursor_allocator = + GbmAllocator::new(gbm.clone(), GbmBufferFlags::CURSOR | GbmBufferFlags::WRITE); + let framebuffer_exporter = GbmFramebufferExporter::new(gbm.clone(), NodeFilter::None); + CursorState { + allocator: cursor_allocator, + framebuffer_exporter, + previous_output_scale: None, + previous_output_transform: None, + #[cfg(feature = "renderer_pixman")] + pixman_renderer, + } + }); + + let overlay_plane_element_ids = OverlayPlaneElementIds::from_planes(&planes); + let current_frame = FrameState::from_planes(surface.plane(), &planes); + + let drm_renderer = DrmCompositor { + primary_plane_element_id: Id::new(), + primary_plane_damage_bag: DamageBag::new(4), + primary_is_opaque: is_opaque, + reset_pending: true, + signaled_fence, + current_frame, + pending_frame: None, + queued_frame: None, + next_frame: None, + swapchain, + framebuffer_exporter, + cursor_size, + cursor_state, + surface, + damage_tracker, + output_mode_source, + planes, + overlay_plane_element_ids, + element_states: IndexMap::new(), + previous_element_states: IndexMap::new(), + opaque_regions: Vec::new(), + element_opaque_regions_workhouse: Vec::new(), + supports_fencing, + debug_flags: DebugFlags::empty(), + span, + }; + + return Ok(drm_renderer); + } + Err((alloc, err)) => { + warn!("Preferred format {} not available: {:?}", format, err); + allocator = alloc; + error = Some(err); + } + } + } + Err(error.unwrap()) + } + + /// Initialize a new [`DrmCompositor`] with a pre-selected format. + /// + /// The [`OutputModeSource`] can be created from an [`Output`](crate::output::Output), which will automatically track + /// the output's mode changes. An [`OutputModeSource::Static`] variant should only be used when + /// manually updating modes using [`DrmCompositor::set_output_mode_source`]. + /// + /// - `output_mode_source` is used to determine the current mode, scale and transform + /// - `surface` for the compositor to use + /// - `planes` defines which planes the compositor is allowed to use for direct scan-out. + /// `None` will result in the compositor to use all planes as specified by [`DrmSurface::planes`] + /// - `allocator` used for the primary plane swapchain + /// - `framebuffer_exporter` is used to create drm framebuffers for the swapchain buffers (and if possible + /// for element buffers) for scan-out + /// - `code` is the fixed format to initialize the framebuffer with + /// - `modifiers` is the set of modifiers allowed, when allocating buffers with the specified color format + /// - `cursor_size` as reported by the drm device, used for creating buffer for the cursor plane + /// - `gbm` device used for creating buffers for the cursor plane, `None` will disable the cursor plane + #[allow(clippy::too_many_arguments)] + pub fn with_format( + output_mode_source: impl Into + Debug, + surface: DrmSurface, + planes: Option, + allocator: A, + framebuffer_exporter: F, + code: DrmFourcc, + modifiers: impl IntoIterator, + cursor_size: Size, + gbm: Option>, + ) -> FrameResult { + let signaled_fence = match surface.create_syncobj(true) { + Ok(signaled_syncobj) => match surface.syncobj_to_fd(signaled_syncobj, true) { + Ok(signaled_fence) => { + let _ = surface.destroy_syncobj(signaled_syncobj); + Some(Arc::new(signaled_fence)) + } + Err(err) => { + tracing::warn!(?err, "failed to export signaled syncobj"); + let _ = surface.destroy_syncobj(signaled_syncobj); + None + } + }, + Err(err) => { + tracing::warn!(?err, "failed to create signaled syncobj"); + None + } + }; + + let span = info_span!( + parent: None, + "drm_compositor", + device = ?surface.dev_path(), + crtc = ?surface.crtc(), + ); + + let output_mode_source = output_mode_source.into(); + + let surface = Arc::new(surface); + let mut planes = match planes { + Some(planes) => planes, + None => surface.planes().clone(), + }; + + // We do not support direct scan-out on legacy + if surface.is_legacy() { + planes.cursor.clear(); + planes.overlay.clear(); + } + + // The selection algorithm expects the planes to be ordered form front to back + planes + .overlay + .sort_by_key(|p| std::cmp::Reverse(p.zpos.unwrap_or_default())); + + let driver = surface.get_driver().map_err(|err| { + FrameError::DrmError(DrmError::Access(AccessError { + errmsg: "Failed to query drm driver", + dev: surface.dev_path(), + source: err, + })) + })?; + // `IN_FENCE_FD` makes commit fail on Nvidia driver + // https://github.com/NVIDIA/open-gpu-kernel-modules/issues/622 + let is_nvidia = driver.name().to_string_lossy().to_lowercase().contains("nvidia") + || driver + .description() + .to_string_lossy() + .to_lowercase() + .contains("nvidia"); + + let cursor_size = Size::from((cursor_size.w as i32, cursor_size.h as i32)); + let damage_tracker = OutputDamageTracker::from_mode_source(output_mode_source.clone()); + let supports_fencing = !surface.is_legacy() + && surface + .get_driver_capability(DriverCapability::SyncObj) + .map(|val| val != 0) + .map_err(|err| { + FrameError::DrmError(DrmError::Access(AccessError { + errmsg: "Failed to query driver capability", + dev: surface.dev_path(), + source: err, + })) + })? + && plane_has_property(&*surface, surface.plane(), "IN_FENCE_FD")? + && !(is_nvidia && nvidia_drm_version().unwrap_or((0, 0, 0)) < (560, 35, 3)); + + let (swapchain, is_opaque) = Self::test_format( + &surface, + supports_fencing, + &planes, + allocator, + &framebuffer_exporter, + code, + modifiers, + ) + .map_err(|(_, err)| err)?; + + let cursor_state = gbm.map(|gbm| { + #[cfg(feature = "renderer_pixman")] + let pixman_renderer = match PixmanRenderer::new() { + Ok(pixman_renderer) => Some(pixman_renderer), + Err(err) => { + tracing::warn!(?err, "failed to initialize pixman renderer for cursor plane"); + None + } + }; + + let cursor_allocator = + GbmAllocator::new(gbm.clone(), GbmBufferFlags::CURSOR | GbmBufferFlags::WRITE); + let framebuffer_exporter = GbmFramebufferExporter::new(gbm.clone(), NodeFilter::None); + CursorState { + allocator: cursor_allocator, + framebuffer_exporter, + previous_output_scale: None, + previous_output_transform: None, + #[cfg(feature = "renderer_pixman")] + pixman_renderer, + } + }); + + let overlay_plane_element_ids = OverlayPlaneElementIds::from_planes(&planes); + let current_frame = FrameState::from_planes(surface.plane(), &planes); + + let drm_renderer = DrmCompositor { + primary_plane_element_id: Id::new(), + primary_plane_damage_bag: DamageBag::new(4), + primary_is_opaque: is_opaque, + reset_pending: true, + signaled_fence, + current_frame, + pending_frame: None, + queued_frame: None, + next_frame: None, + swapchain, + framebuffer_exporter, + cursor_size, + cursor_state, + surface, + damage_tracker, + output_mode_source, + planes, + overlay_plane_element_ids, + element_states: IndexMap::new(), + previous_element_states: IndexMap::new(), + opaque_regions: Vec::new(), + element_opaque_regions_workhouse: Vec::new(), + supports_fencing, + debug_flags: DebugFlags::empty(), + span, + }; + + Ok(drm_renderer) + } + + fn test_format( + drm: &DrmSurface, + supports_fencing: bool, + planes: &Planes, + allocator: A, + framebuffer_exporter: &F, + code: DrmFourcc, + modifiers: impl IntoIterator, + ) -> Result<(Swapchain, bool), (A, FrameErrorType)> { + let modifiers = modifiers.into_iter().collect::>(); + let mut plane_formats = drm.plane_info().formats.iter().copied().collect::>(); + + let opaque_code = get_opaque(code).unwrap_or(code); + if !plane_formats + .iter() + .any(|fmt| fmt.code == code || fmt.code == opaque_code) + { + return Err((allocator, FrameError::NoSupportedPlaneFormat)); + } + plane_formats.retain(|fmt| fmt.code == code || fmt.code == opaque_code); + + if plane_formats.is_empty() { + return Err((allocator, FrameError::NoSupportedPlaneFormat)); + } + + let plane_modifiers = plane_formats + .iter() + .map(|fmt| fmt.modifier) + .collect::>(); + + let swapchain_modifiers = plane_modifiers + .intersection(&modifiers) + .copied() + .collect::>(); + + if swapchain_modifiers.is_empty() { + return Err((allocator, FrameError::NoSupportedPlaneFormat)); + } + + let mode = drm.pending_mode(); + + let mut swapchain: Swapchain = Swapchain::new( + allocator, + mode.size().0 as u32, + mode.size().1 as u32, + code, + swapchain_modifiers, + ); + + // Test format + let buffer = match swapchain.acquire() { + Ok(buffer) => buffer.unwrap(), + Err(err) => return Err((swapchain.allocator, FrameError::Allocator(err))), + }; + + let dmabuf = match buffer.export() { + Ok(dmabuf) => dmabuf, + Err(err) => { + return Err((swapchain.allocator, FrameError::AsDmabufError(err))); + } + }; + + let use_opaque = !plane_formats.iter().any(|f| f.code == code); + let fb_buffer = match framebuffer_exporter.add_framebuffer( + drm.device_fd(), + ExportBuffer::Allocator(&buffer), + use_opaque, + ) { + Ok(Some(fb_buffer)) => fb_buffer, + Ok(None) => return Err((swapchain.allocator, FrameError::NoFramebuffer)), + Err(err) => return Err((swapchain.allocator, FrameError::FramebufferExport(err))), + }; + buffer + .userdata() + .insert_if_missing_threadsafe(|| CachedDrmFramebuffer::new(DrmFramebuffer::Exporter(fb_buffer))); + + let mode = drm.pending_mode(); + let handle = buffer + .userdata() + .get::>::Framebuffer>>() + .unwrap() + .clone(); + + let mode_size = Size::from((mode.size().0 as i32, mode.size().1 as i32)); + + let mut current_frame_state = FrameState::from_planes(drm.plane(), planes); + let plane_claim = match drm.claim_plane(drm.plane()) { + Some(claim) => claim, + None => { + warn!("failed to claim primary plane",); + return Err((swapchain.allocator, FrameError::PrimaryPlaneClaimFailed)); + } + }; + + let plane_state = PlaneState { + skip: false, + needs_test: true, + element_state: None, + config: Some(PlaneConfig { + properties: PlaneProperties { + src: Rectangle::from_size(dmabuf.size()).to_f64(), + dst: Rectangle::from_size(mode_size), + transform: Transform::Normal, + alpha: 1.0, + format: buffer.format(), + }, + buffer: DrmScanoutBuffer { + buffer: ScanoutBuffer::Swapchain(Arc::new(buffer)), + fb: handle, + }, + damage_clips: None, + plane_claim, + sync: None, + }), + }; + + match current_frame_state.test_state(drm, supports_fencing, drm.plane(), plane_state, true) { + Ok(_) => Ok((swapchain, use_opaque)), + Err(err) => { + warn!( + "Mode-setting failed with buffer format {:?}: {}", + dmabuf.format(), + err + ); + Err((swapchain.allocator, err.into())) + } + } + } + + fn find_supported_format( + drm: Arc, + supports_fencing: bool, + planes: &Planes, + allocator: A, + framebuffer_exporter: &F, + mut renderer_formats: Vec, + code: DrmFourcc, + ) -> Result<(Swapchain, bool), (A, FrameErrorType)> { + // select a format + let mut plane_formats = drm.plane_info().formats.iter().copied().collect::>(); + + let opaque_code = get_opaque(code).unwrap_or(code); + if !plane_formats + .iter() + .any(|fmt| fmt.code == code || fmt.code == opaque_code) + { + return Err((allocator, FrameError::NoSupportedPlaneFormat)); + } + plane_formats.retain(|fmt| fmt.code == code || fmt.code == opaque_code); + renderer_formats.retain(|fmt| fmt.code == code); + + trace!("Plane formats: {:?}", plane_formats); + trace!("Renderer formats: {:?}", renderer_formats); + + let plane_modifiers = plane_formats + .iter() + .map(|fmt| fmt.modifier) + .collect::>(); + let renderer_modifiers = renderer_formats + .iter() + .map(|fmt| fmt.modifier) + .collect::>(); + debug!( + "Remaining intersected modifiers: {:?}", + plane_modifiers + .intersection(&renderer_modifiers) + .collect::>() + ); + + if plane_formats.is_empty() { + return Err((allocator, FrameError::NoSupportedPlaneFormat)); + } else if renderer_formats.is_empty() { + return Err((allocator, FrameError::NoSupportedRendererFormat)); + } + + let formats = { + // Special case: if a format supports explicit LINEAR (but no implicit Modifiers) + // and the other doesn't support any modifier, force Implicit. + // This should at least result in a working pipeline possibly with a linear buffer, + // but we cannot be sure. + if (plane_formats.len() == 1 + && plane_formats.iter().next().unwrap().modifier == DrmModifier::Invalid + && renderer_formats + .iter() + .all(|x| x.modifier != DrmModifier::Invalid) + && renderer_formats.iter().any(|x| x.modifier == DrmModifier::Linear)) + || (renderer_formats.len() == 1 + && renderer_formats.first().unwrap().modifier == DrmModifier::Invalid + && plane_formats.iter().all(|x| x.modifier != DrmModifier::Invalid) + && plane_formats.iter().any(|x| x.modifier == DrmModifier::Linear)) + { + vec![DrmFormat { + code, + modifier: DrmModifier::Invalid, + }] + } else { + plane_modifiers + .intersection(&renderer_modifiers) + .cloned() + .map(|modifier| DrmFormat { code, modifier }) + .collect::>() + } + }; + + debug!("Testing Formats: {:?}", formats); + + let modifiers = formats.iter().map(|x| x.modifier).collect::>(); + + let (swapchain, use_opaque) = Self::test_format( + &drm, + supports_fencing, + planes, + allocator, + framebuffer_exporter, + code, + modifiers, + )?; + + Ok((swapchain, use_opaque)) + } + + /// Render the next frame + /// + /// - `elements` for this frame in front-to-back order + /// - `frame_flags` specifies techniques allowed to realize the frame + #[instrument(level = "trace", parent = &self.span, skip_all)] + #[profiling::function] + pub fn render_frame<'a, R, E>( + &mut self, + renderer: &mut R, + elements: &'a [E], + clear_color: impl Into, + frame_flags: FrameFlags, + ) -> Result, RenderFrameErrorType> + where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + { + let mut clear_color = clear_color.into(); + + if !self.surface.is_active() { + return Err(RenderFrameErrorType::::PrepareFrame( + FrameError::DrmError(DrmError::DeviceInactive), + )); + } + + // Just reset any next state, this will put + // any already acquired slot back to the swapchain + std::mem::drop(self.next_frame.take()); + + // If a commit is pending we may still be able to just use a previous + // state, but we want to queue a frame so we just fake the damage to + // make sure queue_frame won't be skipped because of no damage + let allow_partial_update = !self.reset_pending && !self.surface.commit_pending(); + + let (current_size, output_scale, output_transform) = (&self.output_mode_source) + .try_into() + .map_err(OutputDamageTrackerError::OutputNoMode)?; + + // Output transform is specified in surface-rotation, so inversion gives us the + // render transform for the output itself. + let output_transform = output_transform.invert(); + + // Geometry of the output derived from the output mode including the transform + // This is used to calculate the intersection between elements and the output. + // The renderer (and also the logic for direct scan-out) will take care of the + // actual transform during rendering + let output_geometry: Rectangle<_, Physical> = + Rectangle::from_size(output_transform.transform_size(current_size)); + + // We always acquire a buffer from the swapchain even + // if we could end up doing direct scan-out on the primary plane. + // The reason is that we can't know upfront and we need a framebuffer + // on the primary plane to test overlay/cursor planes + let primary_plane_buffer = self + .swapchain + .acquire() + .map_err(FrameError::Allocator)? + .ok_or(FrameError::NoFreeSlotsError)?; + + // It is safe to call export multiple times as the Slot will cache the dmabuf for us + let dmabuf = primary_plane_buffer.export().map_err(FrameError::AsDmabufError)?; + + // Let's check if we already have a cached framebuffer for this Slot, if not try to export + // it and use the Slot userdata to cache it + let maybe_buffer = primary_plane_buffer + .userdata() + .get::>::Framebuffer>>(); + if maybe_buffer.is_none() { + let fb_buffer = self + .framebuffer_exporter + .add_framebuffer( + self.surface.device_fd(), + ExportBuffer::Allocator(&primary_plane_buffer), + self.primary_is_opaque, + ) + .map_err(FrameError::FramebufferExport)? + .ok_or(FrameError::NoFramebuffer)?; + primary_plane_buffer.userdata().insert_if_missing_threadsafe(|| { + CachedDrmFramebuffer::new(DrmFramebuffer::Exporter(fb_buffer)) + }); + } + + // This unwrap is safe as we error out above if we were unable to export a framebuffer + let fb = primary_plane_buffer + .userdata() + .get::>::Framebuffer>>() + .unwrap() + .clone(); + + let mut opaque_regions: Vec> = std::mem::take(&mut self.opaque_regions); + std::mem::swap(&mut self.previous_element_states, &mut self.element_states); + let mut element_states = std::mem::take(&mut self.element_states); + element_states.reserve(std::cmp::min(elements.len(), self.planes.overlay.len())); + let mut render_element_states = RenderElementStates { + states: HashMap::with_capacity(elements.len()), + }; + + // So first we want to create a clean state, for that we have to reset all overlay and cursor planes + // to nothing. We only want to test if the primary plane alone can be used for scan-out. + let mut next_frame_state: FrameState< + ::Buffer, + ::Buffer>>::Framebuffer, + > = { + let previous_state = self + .pending_frame + .as_ref() + .map(|pending| &pending.frame) + .unwrap_or(&self.current_frame); + + // This will create an empty frame state, all planes are skipped by default + let mut next_frame_state = FrameState::from_planes(self.surface.plane(), &self.planes); + + // We want to set skip to false on all planes that previously had something assigned so that + // they get cleared when they are not longer used + for (handle, plane_state) in next_frame_state.planes.iter_mut() { + let reset_state = previous_state + .plane_state(*handle) + .map(|state| state.config.is_some()) + .unwrap_or(false); + + if reset_state { + plane_state.skip = false; + } + } + + next_frame_state + }; + + // We want to make sure we can actually scan-out the primary plane, so + // explicitly set skip to false + let plane_claim = self.surface.claim_plane(self.surface.plane()).ok_or_else(|| { + error!("failed to claim primary plane"); + FrameError::PrimaryPlaneClaimFailed + })?; + let primary_plane_state: PlaneState< + ::Buffer, + ::Buffer>>::Framebuffer, + > = PlaneState { + skip: false, + needs_test: false, + element_state: None, + config: Some(PlaneConfig { + properties: PlaneProperties { + src: Rectangle::from_size(dmabuf.size()).to_f64(), + dst: Rectangle::from_size(current_size), + // NOTE: We do not apply the transform to the primary plane as this is handled by the dtr/renderer + transform: Transform::Normal, + alpha: 1.0, + format: primary_plane_buffer.format(), + }, + buffer: DrmScanoutBuffer { + buffer: ScanoutBuffer::Swapchain(Arc::new(primary_plane_buffer)), + fb, + }, + damage_clips: None, + plane_claim, + sync: None, + }), + }; + + // unconditionally set the primary plane state + // if this would fail the test we are screwed anyway + next_frame_state.set_state(self.surface.plane(), primary_plane_state.clone()); + + // This holds all elements that are visible on the output + // A element is considered visible if it intersects with the output geometry + // AND is not completely hidden behind opaque regions + let mut output_elements: Vec<(&'a E, Rectangle, usize, bool)> = + Vec::with_capacity(elements.len()); + + let mut element_opaque_regions_workhouse = std::mem::take(&mut self.element_opaque_regions_workhouse); + for (index, element) in elements.iter().enumerate() { + let element_id = element.id(); + let element_geometry = element.geometry(output_scale); + let element_loc = element_geometry.loc; + + // First test if the element overlaps with the output + // if not we can skip it + let element_output_geometry = match element_geometry.intersection(output_geometry) { + Some(geo) => geo, + None => continue, + }; + + // Then test if the element is completely hidden behind opaque regions + element_opaque_regions_workhouse.clear(); + element_opaque_regions_workhouse.push(element_output_geometry); + element_opaque_regions_workhouse = Rectangle::subtract_rects_many_in_place( + element_opaque_regions_workhouse, + opaque_regions.iter().copied(), + ); + let element_visible_area = element_opaque_regions_workhouse + .iter() + .fold(0usize, |acc, item| acc + (item.size.w * item.size.h) as usize); + + if element_visible_area == 0 { + // No need to draw a completely hidden element + trace!("skipping completely obscured element {:?}", element.id()); + + // We allow multiple instance of a single element, so do not + // override the state if we already have one + if !render_element_states.states.contains_key(element_id) { + render_element_states + .states + .insert(element_id.clone(), RenderElementState::skipped()); + } + continue; + } + + let element_opaque_regions = element.opaque_regions(output_scale); + element_opaque_regions_workhouse.clear(); + element_opaque_regions_workhouse.push(element_output_geometry); + element_opaque_regions_workhouse = Rectangle::subtract_rects_many_in_place( + element_opaque_regions_workhouse, + element_opaque_regions.iter().copied(), + ); + let element_is_opaque = element_opaque_regions_workhouse.is_empty(); + + opaque_regions.extend( + element_opaque_regions + .into_iter() + .map(|mut region| { + region.loc += element_loc; + region + }) + .filter_map(|geo| geo.intersection(output_geometry)), + ); + + // If the element is completely opaque and spans the whole output nothing below + // will be visible. In this case we can short-cut the whole loop and just mark all + // remaining elements as skipped. + // + // We also use this to special case for single pixel buffers that span the whole + // output. If the last visible element is a solid color we can override the clear + // color and remove the element completely. This will make the element directly above + // this element the last element, enabling direct scan-out on the primary plane for it. + if element_is_opaque && element_output_geometry.contains_rect(output_geometry) { + let element_color = element.underlying_storage(renderer).and_then(|storage| { + if let UnderlyingStorage::Wayland(buffer) = storage { + single_pixel_buffer::get_single_pixel_buffer(buffer) + .ok() + .map(|spb| Color32F::from(spb.rgba32f())) + } else { + None + } + }); + + if let Some(color) = element_color { + clear_color = color; + + render_element_states + .states + .entry(element_id.clone()) + .and_modify(|state| { + if matches!(state.presentation_state, RenderElementPresentationState::Skipped) { + *state = RenderElementState::rendered(element_visible_area); + } else { + state.visible_area += element_visible_area; + } + }) + .or_insert_with(|| RenderElementState::rendered(element_visible_area)); + } else { + output_elements.push(( + element, + element_geometry, + element_visible_area, + element_is_opaque, + )); + } + + for element in elements.iter().skip(index + 1) { + let element_id = element.id(); + // We allow multiple instance of a single element, so do not + // override the state if we already have one + if !render_element_states.states.contains_key(element_id) { + render_element_states + .states + .insert(element_id.clone(), RenderElementState::skipped()); + } + } + break; + } + + output_elements.push((element, element_geometry, element_visible_area, element_is_opaque)); + } + self.element_opaque_regions_workhouse = element_opaque_regions_workhouse; + + // This will hold the element that has been selected for direct scan-out on + // the primary plane if any + let mut primary_plane_scanout_element: Option<&'a E> = None; + // This will hold all elements that have been assigned to the primary plane + // for rendering + let mut primary_plane_elements: Vec<&'a E> = Vec::with_capacity(elements.len()); + // This will hold the element per plane that has been assigned to a overlay/underlay + // plane for direct scan-out + let mut overlay_plane_elements: IndexMap = + IndexMap::with_capacity(self.planes.overlay.len()); + // This will hold the element assigned on the cursor plane if any + let mut cursor_plane_element: Option<&'a E> = None; + + let output_elements_len = output_elements.len(); + for (index, (element, element_geometry, element_visible_area, element_is_opaque)) in + output_elements.iter().enumerate() + { + let element_id = element.id(); + let element_geometry = *element_geometry; + let remaining_elements = output_elements_len - index; + let element_is_opaque = *element_is_opaque; + + // Check if we found our last item, we can try to do + // direct scan-out on the primary plane + // If we already assigned an element to + // an underlay plane we will have a hole punch element + // on the primary plane, this will disable direct scan-out + // on the primary plane. + let try_assign_primary_plane = if remaining_elements == 1 && primary_plane_elements.is_empty() { + let crtc_background_matches_clear_color = + (clear_color.r() == 0f32 && clear_color.g() == 0f32 && clear_color.b() == 0f32) + || clear_color.a() == 0f32; + let element_spans_complete_output = element_geometry.contains_rect(output_geometry); + let overlaps_with_underlay = self + .planes + .overlay + .iter() + .filter(|p| { + p.zpos.unwrap_or_default() < self.surface.plane_info().zpos.unwrap_or_default() + }) + .any(|p| next_frame_state.overlaps(p.handle, element_geometry)); + !overlaps_with_underlay + && (crtc_background_matches_clear_color + || (element_spans_complete_output && element_is_opaque)) + } else { + false + }; + + match self.try_assign_element( + renderer, + *element, + index, + element_geometry, + element_is_opaque, + &mut element_states, + &primary_plane_elements, + output_scale, + &mut next_frame_state, + output_transform, + output_geometry, + try_assign_primary_plane, + frame_flags, + ) { + Ok(direct_scan_out_plane) => { + match direct_scan_out_plane.type_ { + drm::control::PlaneType::Overlay => { + overlay_plane_elements.insert(direct_scan_out_plane.handle, element); + } + drm::control::PlaneType::Primary => primary_plane_scanout_element = Some(element), + drm::control::PlaneType::Cursor => cursor_plane_element = Some(element), + } + + if let Some(state) = render_element_states.states.get_mut(element_id) { + state.presentation_state = RenderElementPresentationState::ZeroCopy; + state.visible_area += element_visible_area; + } else { + render_element_states.states.insert( + element_id.clone(), + RenderElementState::zero_copy(*element_visible_area), + ); + } + } + Err(reason) => { + if let Some(reason) = reason { + if !render_element_states.states.contains_key(element_id) { + render_element_states.states.insert( + element_id.clone(), + RenderElementState::rendering_with_reason(reason), + ); + } + } + + primary_plane_elements.push(element); + } + } + } + + // Cleanup old state (e.g. old dmabuffers) + for element_state in element_states.values_mut() { + element_state.fb_cache.cleanup(); + } + self.element_states = element_states; + self.previous_element_states.clear(); + opaque_regions.clear(); + self.opaque_regions = opaque_regions; + + let previous_state = self + .pending_frame + .as_ref() + .map(|pending| &pending.frame) + .unwrap_or(&self.current_frame); + + // Check if the next frame state is fully compatible with the previous frame state. + // If not do a single atomic commit test and when that fails render everything that failed + // the test on the primary plane. This will also automatically correct any mistake we made + // during plane assignment and start the full test cycle on the next frame. + if next_frame_state + .test_state_complete( + previous_state, + &self.surface, + self.supports_fencing, + false, + allow_partial_update, + ) + .is_err() + { + trace!("atomic test failed for frame, resetting frame"); + + let mut removed_overlay_elements: Vec<(usize, &E)> = Vec::with_capacity( + next_frame_state + .planes + .iter() + .filter(|(_, state)| state.needs_test) + .count(), + ); + for (plane, state) in next_frame_state.planes.iter_mut() { + // We can skip everything that is known to work already + if !state.needs_test { + continue; + } + + // Check if the element we are potentially going to remove is + // on the primary plane, cursor plane or an overlay plane + let element = if *plane == self.surface.plane() { + primary_plane_scanout_element.take() + } else if self.planes.cursor.iter().any(|p| *plane == p.handle) { + cursor_plane_element.take() + } else { + overlay_plane_elements.shift_remove(plane) + }; + + // If we have no element on this plane skip the rest + let Some(element) = element else { + continue; + }; + + // Reset the plane config and state + state.config = None; + state.skip = false; + state.needs_test = false; + let element_z_index = state.element_state.take().map(|s| s.z_index).unwrap_or_default(); + removed_overlay_elements.push((element_z_index, element)); + // Note: This might not be completely correct if the same element is present + // multiple times and only gets removed once. But this is pretty unlikely to + // happen and will only result in reporting wrong visible area size and scan-out state + // for a single frame. + render_element_states.states.remove(element.id()); + } + + // If we removed any element from some plane we have + // to make sure we actually have a slot on the primary + // plane we can render into + if !removed_overlay_elements.is_empty() { + next_frame_state.set_state(self.surface.plane(), primary_plane_state); + } + + removed_overlay_elements.sort_by_key(|(z_index, _)| *z_index); + primary_plane_elements = removed_overlay_elements + .into_iter() + .map(|(_, element)| element) + .chain(primary_plane_elements.into_iter()) + .collect(); + } + + // If a plane has been moved or no longer has a buffer we need to report that as damage + for (handle, previous_plane_state) in previous_state.planes.iter() { + // plane has been removed, so remove the plane from the plane id cache + if previous_plane_state.config.is_some() + && next_frame_state + .plane_state(*handle) + .as_ref() + .and_then(|state| state.config.as_ref()) + .is_none() + { + self.overlay_plane_element_ids.remove_plane(handle); + } + } + + let render = next_frame_state + .plane_buffer(self.surface.plane()) + .map(|config| matches!(config.buffer, ScanoutBuffer::Swapchain(_))) + .unwrap_or(false); + + if render { + trace!( + "rendering {} elements on the primary {:?}", + primary_plane_elements.len(), + self.surface.plane(), + ); + let (mut dmabuf, age) = { + let primary_plane_state = next_frame_state.plane_state(self.surface.plane()).unwrap(); + let config = primary_plane_state.config.as_ref().unwrap(); + let slot = match &config.buffer.buffer { + ScanoutBuffer::Swapchain(slot) => slot, + _ => unreachable!(), + }; + + // It is safe to call export multiple times as the Slot will cache the dmabuf for us + let dmabuf = slot.export().map_err(FrameError::AsDmabufError)?; + let age = slot.age().into(); + (dmabuf, age) + }; + + // store the current renderer debug flags and replace them + // with our own + let renderer_debug_flags = renderer.debug_flags(); + renderer.set_debug_flags(self.debug_flags); + + // First we collect all our fake elements for overlay and underlays + // This is used to transport the opaque regions for elements that + // have been assigned to planes and to realize hole punching for + // underlays. We use an Id per plane/element combination to not + // interfere with the element damage state in the output damage tracker. + // Using the original element id would store the commit in the + // OutputDamageTracker without actual rendering anything -> bad + // Using a id per plane could result in an issue when a different + // element with the same geometry gets assigned and has the same + // commit -> unlikely but possible + // So we use an Id per plane for as long as we have the same element + // on that plane. + let overlay_plane_elements = overlay_plane_elements.iter().filter_map(|(p, element)| { + let id = self + .overlay_plane_element_ids + .plane_id_for_element_id(p, element.id()); + + let plane_z_pos = self + .planes + .overlay + .iter() + .find_map(|info| { + if info.handle == *p { + Some(info.zpos.unwrap_or_default()) + } else { + None + } + }) + .unwrap_or_default(); + let is_underlay = plane_z_pos < self.surface.plane_info().zpos.unwrap_or_default(); + if is_underlay { + Some(HolepunchRenderElement::from_render_element(id, element, output_scale).into()) + } else { + OverlayPlaneElement::from_render_element(id, *element, output_scale) + .map(DrmRenderElements::from) + } + }); + // Then render all remaining elements assigned to the primary plane + let elements = overlay_plane_elements + .chain( + primary_plane_elements + .into_iter() + .map(|e| DrmRenderElements::Other(e)), + ) + .collect::>(); + + let mut framebuffer = renderer + .bind(&mut dmabuf) + .map_err(|err| RenderFrameError::RenderFrame(OutputDamageTrackerError::Rendering(err)))?; + let render_res = + self.damage_tracker + .render_output(renderer, &mut framebuffer, age, &elements, clear_color); + + // restore the renderer debug flags + renderer.set_debug_flags(renderer_debug_flags); + + match render_res { + Ok(render_output_result) => { + if render_output_result.damage.is_none() { + // if we receive no damage we can assume no rendering took place + // and we should trigger a cleanup of the renderer texture cache + // to prevent holding textures longer then necessary + let _ = renderer.cleanup_texture_cache(); + } + + for (id, state) in render_output_result.states.states.into_iter() { + // Skip the state for our fake elements + if self.overlay_plane_element_ids.contains_plane_id(&id) { + continue; + } + + if let Some(existing_state) = render_element_states.states.get_mut(&id) { + if matches!( + existing_state.presentation_state, + RenderElementPresentationState::Skipped + ) { + *existing_state = state; + } else { + existing_state.visible_area += state.visible_area; + } + } else { + render_element_states.states.insert(id.clone(), state); + } + } + + // Fixup damage on plane, if we used the plane for direct scan-out before + // but now use it for rendering we do not replace the damage which is + // the whole plane initially. + let had_direct_scan_out = previous_state + .plane_state(self.surface.plane()) + .map(|state| state.element_state.is_some()) + .unwrap_or(true); + + let primary_plane_state = next_frame_state.plane_state_mut(self.surface.plane()).unwrap(); + let config = primary_plane_state.config.as_mut().unwrap(); + + if !had_direct_scan_out { + if let Some(render_damage) = render_output_result.damage { + trace!("rendering damage: {:?}", render_damage); + + self.primary_plane_damage_bag.add(render_damage.iter().map(|d| { + d.to_logical(1).to_buffer( + 1, + Transform::Normal, + &output_geometry.size.to_logical(1), + ) + })); + + // Here we need to apply the output_transform to the damage since we + // haven't rotated our framebuffer. dst is already in the same + // coordinate space src is, so no transform needed. + config.damage_clips = PlaneDamageClips::from_damage( + self.surface.device_fd(), + config.properties.src, + config.properties.dst, + Transform::Normal, + output_transform, + render_damage.iter().copied(), + ) + .ok() + .flatten(); + config.sync = Some((render_output_result.sync.clone(), None)); + } else { + trace!("skipping primary plane, no damage"); + + primary_plane_state.skip = true; + *config = previous_state + .plane_state(self.surface.plane()) + .and_then(|state| state.config.as_ref().cloned()) + .unwrap_or_else(|| config.clone()); + } + } else { + trace!( + "clearing previous direct scan-out on primary plane, damaging complete output" + ); + self.primary_plane_damage_bag + .add([output_geometry.to_logical(1).to_buffer( + 1, + Transform::Normal, + &output_geometry.size.to_logical(1), + )]); + + config.sync = Some((render_output_result.sync.clone(), None)); + } + } + Err(err) => { + // Rendering failed at some point, reset the buffers + // as we probably now have some half drawn buffer + self.swapchain.reset_buffers(); + return Err(RenderFrameError::from(err)); + } + } + } else { + // if we are constantly doing direct scan-out on the primary plane + // we have to cleanup the renderer texture cache as this would + // only happen implicit during rendering otherwise + let _ = renderer.cleanup_texture_cache(); + } + + let primary_plane_element = if render { + let (slot, sync) = { + let primary_plane_state = next_frame_state.plane_state(self.surface.plane()).unwrap(); + let config = primary_plane_state.config.as_ref().unwrap(); + ( + config.buffer.clone(), + config + .sync + .as_ref() + .map(|(sync, _)| sync.clone()) + .unwrap_or_default(), + ) + }; + + PrimaryPlaneElement::Swapchain(PrimarySwapchainElement { + slot, + transform: output_transform, + damage: self.primary_plane_damage_bag.snapshot(), + sync, + }) + } else { + PrimaryPlaneElement::Element(primary_plane_scanout_element.unwrap()) + }; + + // if the update only contains a cursor position update, skip it for vrr + if frame_flags.contains(FrameFlags::SKIP_CURSOR_ONLY_UPDATES) + && allow_partial_update + && next_frame_state.planes.iter().all(|(plane, state)| { + state.skip + || (self.planes.cursor.iter().any(|p| *plane == p.handle) + && state.buffer().map(|b| &b.fb) + == previous_state.plane_buffer(*plane).map(|b| &b.fb)) + }) + { + for plane in self.planes.cursor.iter() { + let Some(state) = next_frame_state.plane_state_mut(plane.handle) else { + continue; + }; + state.skip = true; + } + } + + let next_frame = PreparedFrame { + kind: if allow_partial_update { + PreparedFrameKind::Partial + } else { + PreparedFrameKind::Full + }, + frame: next_frame_state, + }; + let frame_reference: RenderFrameResult<'a, A::Buffer, F::Framebuffer, E> = RenderFrameResult { + is_empty: next_frame.is_empty(), + primary_element: primary_plane_element, + overlay_elements: overlay_plane_elements.into_values().collect(), + cursor_element: cursor_plane_element, + states: render_element_states, + primary_plane_element_id: self.primary_plane_element_id.clone(), + supports_fencing: self.supports_fencing, + }; + + // We only store the next frame if it actually contains any changes or if a commit is pending + // Storing the (empty) frame could keep a reference to wayland buffers which + // could otherwise be potentially released on `frame_submitted` + if !next_frame.is_empty() { + self.next_frame = Some(next_frame); + } + + Ok(frame_reference) + } + + /// Queues the current frame for scan-out. + /// + /// If `render_frame` has not been called prior to this function or returned no damage + /// this function will return [`FrameError::EmptyFrame`]. Instead of calling `queue_frame` it + /// is the callers responsibility to re-schedule the frame. A simple strategy for frame + /// re-scheduling is to queue a one-shot timer that will trigger after approximately one + /// retrace duration. + /// + /// *Note*: It is your responsibility to synchronize rendering if the [`RenderFrameResult`] + /// returned by the previous [`render_frame`](DrmCompositor::render_frame) call returns `true` on [`RenderFrameResult::needs_sync`]. + /// + /// *Note*: This function needs to be followed up with [`DrmCompositor::frame_submitted`] + /// when a vblank event is received, that denotes successful scan-out of the frame. + /// Otherwise the underlying swapchain will eventually run out of buffers. + /// + /// `user_data` can be used to attach some data to a specific buffer and later retrieved with [`DrmCompositor::frame_submitted`] + #[profiling::function] + pub fn queue_frame(&mut self, user_data: U) -> FrameResult<(), A, F> { + if !self.surface.is_active() { + return Err(FrameErrorType::::DrmError(DrmError::DeviceInactive)); + } + + let prepared_frame = self.next_frame.take().ok_or(FrameErrorType::::EmptyFrame)?; + if prepared_frame.is_empty() { + return Err(FrameErrorType::::EmptyFrame); + } + + if let Some(plane_state) = prepared_frame.frame.plane_state(self.surface.plane()) { + if !plane_state.skip { + let slot = plane_state.buffer().and_then(|config| match &config.buffer { + ScanoutBuffer::Swapchain(slot) => Some(slot), + _ => None, + }); + + if let Some(slot) = slot { + self.swapchain.submitted(slot); + } + } + } + + self.queued_frame = Some(QueuedFrame { + prepared_frame, + user_data, + }); + if self.pending_frame.is_none() { + self.submit()?; + } + Ok(()) + } + + /// Commits the current frame for scan-out. + /// + /// If `render_frame` has not been called prior to this function or returned no damage + /// this function will return [`FrameError::EmptyFrame`]. Instead of calling `commit_frame` it + /// is the callers responsibility to re-schedule the frame. A simple strategy for frame + /// re-scheduling is to queue a one-shot timer that will trigger after approximately one + /// retrace duration. + /// + /// *Note*: It is your responsibility to synchronize rendering if the [`RenderFrameResult`] + /// returned by the previous [`render_frame`](DrmCompositor::render_frame) call returns `true` on [`RenderFrameResult::needs_sync`]. + /// + /// *Note*: This function should not be followed up with [`DrmCompositor::frame_submitted`] + /// and will not generate a vblank event on the underlying device. + pub fn commit_frame(&mut self) -> FrameResult<(), A, F> { + if !self.surface.is_active() { + return Err(FrameErrorType::::DrmError(DrmError::DeviceInactive)); + } + + let mut prepared_frame = self.next_frame.take().ok_or(FrameErrorType::::EmptyFrame)?; + if prepared_frame.is_empty() { + return Err(FrameErrorType::::EmptyFrame); + } + + if let Some(plane_state) = prepared_frame.frame.plane_state(self.surface.plane()) { + if !plane_state.skip { + let slot = plane_state.buffer().and_then(|config| match &config.buffer { + ScanoutBuffer::Swapchain(slot) => Some(slot), + _ => None, + }); + + if let Some(slot) = slot { + self.swapchain.submitted(slot); + } + } + } + + let flip = prepared_frame + .frame + .commit(&self.surface, self.supports_fencing, false, false); + + let res = self.handle_flip(&prepared_frame, flip); + + if res.is_ok() { + self.queued_frame = None; + self.pending_frame = None; + self.current_frame = prepared_frame.frame; + } + + res + } + + /// Re-evaluates the current state of the crtc and forces calls to [`render_frame`](DrmCompositor::render_frame) + /// to return `false` for [`RenderFrameResult::is_empty`] until a frame is queued with [`queue_frame`](DrmCompositor::queue_frame). + /// + /// It is recommended to call this function after this used [`Session`](crate::backend::session::Session) + /// gets re-activated / VT switched to. + /// + /// Usually you do not need to call this in other circumstances, but if + /// the state of the crtc is modified elsewhere, you may call this function + /// to reset it's internal state. + pub fn reset_state(&mut self) -> Result<(), DrmError> { + self.surface.reset_state()?; + self.reset_pending = true; + Ok(()) + } + + #[profiling::function] + fn submit(&mut self) -> FrameResult<(), A, F> { + let QueuedFrame { + mut prepared_frame, + user_data, + } = self.queued_frame.take().unwrap(); + + let allow_partial_update = prepared_frame.kind == PreparedFrameKind::Partial; + let flip = if self.surface.commit_pending() { + prepared_frame + .frame + .commit(&self.surface, self.supports_fencing, allow_partial_update, true) + } else { + prepared_frame + .frame + .page_flip(&self.surface, self.supports_fencing, allow_partial_update, true) + }; + + let res = self.handle_flip(&prepared_frame, flip); + + if res.is_ok() { + self.pending_frame = Some(PendingFrame { + frame: prepared_frame.frame, + user_data, + }); + } + + res + } + + fn handle_flip( + &mut self, + prepared_frame: &PreparedFrame, + flip: Result<(), crate::backend::drm::error::Error>, + ) -> FrameResult<(), A, F> { + match flip { + Ok(_) => { + if prepared_frame.kind == PreparedFrameKind::Full { + self.reset_pending = false; + } + } + Err(crate::backend::drm::error::Error::Access(ref access)) + if access.source.kind() == ErrorKind::InvalidInput => + { + // In case the commit/flip failed while we tried to directly scan-out + // something on the primary plane we can try to mark this as failed for + // the next call to render_frame + let primary_plane_element_state = prepared_frame + .frame + .plane_state(self.surface.plane()) + .and_then(|plane_state| { + plane_state + .element_state + .as_ref() + .map(|element_state| &element_state.id) + }) + .and_then(|primary_plane_element_id| { + self.element_states.get_mut(primary_plane_element_id) + }); + + if let Some(primary_plane_element_state) = primary_plane_element_state { + for instance in primary_plane_element_state.instances.iter_mut() { + instance.failed_planes.primary = true; + } + } + } + Err(_) => {} + }; + + flip.map_err(FrameError::DrmError) + } + + /// Marks the current frame as submitted. + /// + /// *Note*: Needs to be called, after the vblank event of the matching [`DrmDevice`](super::DrmDevice) + /// was received after calling [`DrmCompositor::queue_frame`] on this surface. + /// Otherwise the underlying swapchain will run out of buffers eventually. + #[profiling::function] + pub fn frame_submitted(&mut self) -> FrameResult, A, F> { + if let Some(PendingFrame { mut frame, user_data }) = self.pending_frame.take() { + std::mem::swap(&mut frame, &mut self.current_frame); + if self.queued_frame.is_some() { + self.submit()?; + } + Ok(Some(user_data)) + } else { + Ok(None) + } + } + + /// Reset the underlying buffers + pub fn reset_buffers(&mut self) { + self.swapchain.reset_buffers(); + } + + /// Reset the age for all buffers. + /// + /// This can be used to efficiently clear the damage history without having to + /// modify the damage for each surface. + pub fn reset_buffer_ages(&mut self) { + self.swapchain.reset_buffer_ages(); + } + + /// Returns the underlying [`crtc`] of this surface + pub fn crtc(&self) -> crtc::Handle { + self.surface.crtc() + } + + /// Returns the underlying [`plane`] of this surface + pub fn plane(&self) -> plane::Handle { + self.surface.plane() + } + + /// Currently used [`connector`]s of this `Surface` + pub fn current_connectors(&self) -> impl IntoIterator { + self.surface.current_connectors() + } + + /// Returns the pending [`connector`]s + /// used for the next frame queued via [`queue_frame`](DrmCompositor::queue_frame). + pub fn pending_connectors(&self) -> impl IntoIterator { + self.surface.pending_connectors() + } + + /// Tries to add a new [`connector`] + /// to be used after the next commit. + /// + /// **Warning**: You need to make sure, that the connector is not used with another surface + /// or was properly removed via `remove_connector` + `commit` before adding it to another surface. + /// Behavior if failing to do so is undefined, but might result in rendering errors or the connector + /// getting removed from the other surface without updating it's internal state. + /// + /// Fails if the `connector` is not compatible with the underlying [`crtc`] + /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) + /// or is not compatible with the currently pending + /// [`Mode`]. + pub fn add_connector(&self, connector: connector::Handle) -> FrameResult<(), A, F> { + self.surface + .add_connector(connector) + .map_err(FrameError::DrmError) + } + + /// Tries to mark a [`connector`] + /// for removal on the next commit. + pub fn remove_connector(&self, connector: connector::Handle) -> FrameResult<(), A, F> { + self.surface + .remove_connector(connector) + .map_err(FrameError::DrmError) + } + + /// Tries to replace the current connector set with the newly provided one on the next commit. + /// + /// Fails if one new `connector` is not compatible with the underlying [`crtc`] + /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) + /// or is not compatible with the currently pending + /// [`Mode`]. + pub fn set_connectors(&self, connectors: &[connector::Handle]) -> FrameResult<(), A, F> { + self.surface + .set_connectors(connectors) + .map_err(FrameError::DrmError) + } + + /// Returns the currently active [`Mode`] + /// of the underlying [`crtc`] + pub fn current_mode(&self) -> Mode { + self.surface.current_mode() + } + + /// Returns the currently pending [`Mode`] + /// to be used after the next commit. + pub fn pending_mode(&self) -> Mode { + self.surface.pending_mode() + } + + /// Tries to set a new [`Mode`] + /// to be used after the next commit. + /// + /// Fails if the mode is not compatible with the underlying + /// [`crtc`] or any of the + /// pending [`connector`]s. + pub fn use_mode(&mut self, mode: Mode) -> FrameResult<(), A, F> { + self.surface.use_mode(mode).map_err(FrameError::DrmError)?; + let (w, h) = mode.size(); + self.swapchain.resize(w as _, h as _); + Ok(()) + } + + /// Returns if Variable Refresh Rate is advertised as supported by the given connector. + /// + /// See [`DrmSurface::vrr_supported`] for more details. + pub fn vrr_supported(&self, conn: connector::Handle) -> FrameResult { + self.surface.vrr_supported(conn).map_err(FrameError::DrmError) + } + + /// Returns if Variable Refresh Rate is currently enabled for frames composed by this [`DrmCompositor`]. + pub fn vrr_enabled(&self) -> bool { + self.surface.vrr_enabled() + } + + /// Tries to set variable refresh rate (VRR) for the next frame. + /// + /// Doing so might cause the next frame to trigger a modeset. + /// Check [`DrmCompositor::vrr_supported`], which indicates if VRR can be + /// used without a modeset on the attached connectors. + pub fn use_vrr(&mut self, vrr: bool) -> FrameResult<(), A, F> { + self.surface.use_vrr(vrr).map_err(FrameError::DrmError) + } + + /// Set the [`DebugFlags`] to use + /// + /// Note: This will reset the primary plane swapchain if + /// the flags differ from the current flags + pub fn set_debug_flags(&mut self, flags: DebugFlags) { + if self.debug_flags != flags { + self.debug_flags = flags; + self.swapchain.reset_buffers(); + } + } + + /// Returns the current enabled [`DebugFlags`] + pub fn debug_flags(&self) -> DebugFlags { + self.debug_flags + } + + /// Returns a reference to the underlying drm surface + pub fn surface(&self) -> &DrmSurface { + &self.surface + } + + /// Get the format of the underlying swapchain + pub fn format(&self) -> DrmFourcc { + self.swapchain.format() + } + + /// Get the allowed modifiers of the underlying swapchain + pub fn modifiers(&self) -> &[DrmModifier] { + self.swapchain.modifiers() + } + + /// Reset the underlying swapchain and assign a new color format. + pub fn set_format( + &mut self, + allocator: A, + code: DrmFourcc, + modifiers: impl IntoIterator, + ) -> Result<(), FrameErrorType> { + let (swapchain, is_oapque) = Self::test_format( + &self.surface, + self.supports_fencing, + &self.planes, + allocator, + &self.framebuffer_exporter, + code, + modifiers, + ) + .map_err(|(_, err)| err)?; + + self.swapchain = swapchain; + self.primary_is_opaque = is_oapque; + + Ok(()) + } + + /// Change the output mode source. + pub fn set_output_mode_source(&mut self, output_mode_source: OutputModeSource) { + // Avoid clearing damage if mode source did not change. + if output_mode_source == self.output_mode_source { + return; + } + + self.damage_tracker = OutputDamageTracker::from_mode_source(output_mode_source.clone()); + self.output_mode_source = output_mode_source; + } + + #[allow(clippy::too_many_arguments)] + #[instrument(level = "trace", skip_all)] + #[profiling::function] + fn try_assign_element<'a, R, E>( + &mut self, + renderer: &mut R, + element: &'a E, + element_zindex: usize, + element_geometry: Rectangle, + element_is_opaque: bool, + element_states: &mut IndexMap>::Framebuffer>>, + primary_plane_elements: &[&'a E], + scale: Scale, + frame_state: &mut CompositorFrameState, + output_transform: Transform, + output_geometry: Rectangle, + try_assign_primary_plane: bool, + frame_flags: FrameFlags, + ) -> Result> + where + R: Renderer + Bind, + E: RenderElement, + { + // Check if we have a free plane, otherwise we can exit early + if !frame_flags.intersects(FrameFlags::ALLOW_SCANOUT) { + trace!( + "skipping direct scan-out for element {:?}, no free planes", + element.id() + ); + return Err(None); + }; + if element.is_framebuffer_effect() { + return Err(None); + } + + let mut rendering_reason: Option = None; + + if try_assign_primary_plane { + match self.try_assign_primary_plane( + renderer, + element, + element_zindex, + element_geometry, + element_states, + scale, + frame_state, + output_transform, + output_geometry, + frame_flags, + ) { + Ok(plane) => { + trace!( + "assigned element {:?} to primary {:?}", + element.id(), + self.surface.plane() + ); + return Ok(plane); + } + Err(err) => rendering_reason = rendering_reason.or(err), + }; + } + + if let Some(plane) = self.try_assign_cursor_plane( + renderer, + element, + element_zindex, + element_geometry, + scale, + frame_state, + output_transform, + output_geometry, + frame_flags, + ) { + trace!("assigned element {:?} to cursor {:?}", element.id(), plane.handle); + return Ok(plane); + } + + match self.try_assign_overlay_plane( + renderer, + element, + element_zindex, + element_geometry, + element_is_opaque, + element_states, + primary_plane_elements, + scale, + frame_state, + output_transform, + output_geometry, + frame_flags, + ) { + Ok(plane) => { + trace!( + "assigned element {:?} to overlay plane {:?}", + element.id(), + plane.handle + ); + return Ok(plane); + } + Err(err) => rendering_reason = rendering_reason.or(err), + } + + Err(rendering_reason) + } + + #[allow(clippy::too_many_arguments)] + #[instrument(level = "trace", skip_all)] + #[profiling::function] + fn try_assign_primary_plane<'a, R, E>( + &mut self, + renderer: &mut R, + element: &'a E, + element_zindex: usize, + element_geometry: Rectangle, + element_states: &mut IndexMap>::Framebuffer>>, + scale: Scale, + frame_state: &mut CompositorFrameState, + output_transform: Transform, + output_geometry: Rectangle, + frame_flags: FrameFlags, + ) -> Result> + where + R: Renderer, + E: RenderElement, + { + if !frame_flags + .intersects(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT | FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY) + { + return Err(None); + } + + if frame_state + .plane_state(self.surface.plane()) + .map(|state| state.element_state.is_some()) + .unwrap_or(true) + { + return Err(None); + } + + let element_config = self.element_config( + renderer, + element, + element_zindex, + element_geometry, + element_states, + frame_state, + output_transform, + output_geometry, + true, + )?; + + if let ScanoutBuffer::Swapchain(slot) = &frame_state + .plane_buffer(self.surface.plane()) + .expect("We have a buffer for the primary plane") + .buffer + { + if !frame_flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY) + && slot.format() != element_config.properties.format + { + trace!( + "failed to assign element {:?} to primary {:?}, format doesn't match", + element.id(), + self.surface.plane() + ); + return Err(None); + } + } + + let has_underlay = self + .planes + .overlay + .iter() + .filter(|plane| { + self.surface.plane_info().zpos.unwrap_or_default() > plane.zpos.unwrap_or_default() + }) + .any(|plane| frame_state.is_assigned(plane.handle)); + + if has_underlay { + trace!( + "failed to assign element {:?} to primary {:?}, already has underlay", + element.id(), + self.surface.plane() + ); + return Err(None); + } + + if element_config.failed_planes.primary { + return Err(Some(RenderingReason::ScanoutFailed)); + } + + let res = self.try_assign_plane( + element, + &element_config, + self.surface.plane_info(), + scale, + frame_state, + ); + + if let Err(Some(RenderingReason::ScanoutFailed)) = res { + element_config.failed_planes.primary = true; + } + + res + } + + #[allow(clippy::too_many_arguments)] + #[instrument(level = "trace", skip_all)] + #[profiling::function] + fn try_assign_cursor_plane( + &mut self, + renderer: &mut R, + element: &E, + element_zindex: usize, + element_geometry: Rectangle, + scale: Scale, + frame_state: &mut CompositorFrameState, + output_transform: Transform, + output_geometry: Rectangle, + frame_flags: FrameFlags, + ) -> Option + where + R: Renderer, + E: RenderElement, + { + if !frame_flags.contains(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT) { + return None; + } + + let Some(cursor_state) = self.cursor_state.as_mut() else { + trace!("no cursor state, skipping cursor rendering"); + return None; + }; + + // only try to assign elements on a cursor plane that indicate so + if element.kind() != Kind::Cursor { + trace!( + "skipping element {:?} on cursor plane(s), element kind not cursor", + element.id(), + ); + return None; + } + + let element_size = output_transform.transform_size(element_geometry.size); + + // if the element is greater than the cursor size we can not + // use the cursor plane to scan out the element + if element_size.w > self.cursor_size.w || element_size.h > self.cursor_size.h { + trace!("element {:?} too big for cursor plane(s), skipping", element.id(),); + return None; + } + + // For now we only support a single cursor plane, so first test if we already + // assigned something to any cursor plane + if let Some(plane_info) = self + .planes + .cursor + .iter() + .find(|plane_info| frame_state.is_assigned(plane_info.handle)) + { + trace!( + "skipping element {:?} on cursor {:?}, plane already has element assigned", + element.id(), + plane_info.handle + ); + return None; + } + + let previous_state = self + .pending_frame + .as_ref() + .map(|pending| &pending.frame) + .unwrap_or(&self.current_frame); + + // In case we have multiple cursor planes we will try to keep using + // the same cursor plane for as long as possible. + // So first we test the previous state for an assigned cursor plane and + // if that fails we will try to pick the first unclaimed cursor plane. + let Some((plane_info, plane_claim)) = self + .planes + .cursor + .iter() + .find_map(|plane_info: &PlaneInfo| { + if previous_state.is_assigned(plane_info.handle) { + self.surface + .claim_plane(plane_info.handle) + .map(|claim| (plane_info, claim)) + } else { + None + } + }) + .or_else(|| { + self.planes.cursor.iter().find_map(|plane_info| { + self.surface + .claim_plane(plane_info.handle) + .map(|claim| (plane_info, claim)) + }) + }) + else { + trace!( + "skipping element {:?} on cursor plane(s), no free plane found", + element.id(), + ); + return None; + }; + + let cursor_plane_size = if let Some(size_hints) = plane_info.size_hints.as_deref() { + // size hints are in order of preference, so we can choose the first one + // that can hold the whole element + // + // Note: we use the legacy cursor size as a pre-check and expect it to hold the + // biggest possible size + size_hints + .iter() + .find(|hint| hint.w as i32 >= element_size.w && hint.h as i32 >= element_size.h) + .map(|hint| Size::::from((hint.w as i32, hint.h as i32))) + .unwrap_or(self.cursor_size) + } else { + self.cursor_size + }; + + // this calculates the location of the cursor plane taking the simulated transform + // into consideration + let cursor_plane_location = output_transform + .transform_point_in(element.location(scale), &output_geometry.size) + - output_transform.transform_point_in(Point::default(), &cursor_plane_size); + + let previous_state = self + .pending_frame + .as_ref() + .map(|pending| &pending.frame) + .unwrap_or(&self.current_frame); + + let previous_element_state = previous_state + .plane_state(plane_info.handle) + .and_then(|state| state.element_state.as_ref()); + + // if the output transform or scale change we have to (re-)render the cursor plane, + // also if the element changed or reports damage we have to render it + let render = cursor_state + .previous_output_transform + .map(|t| t != output_transform) + .unwrap_or(true) + || cursor_state + .previous_output_scale + .map(|s| s != scale) + .unwrap_or(true) + || previous_element_state + .map(|element_state| { + element_state.id != *element.id() + || element.current_commit() != element_state.commit + || element_state.cursor_size != Some(element_size) + }) + .unwrap_or(true) + || previous_state + .plane_state(plane_info.handle) + .and_then(|state| { + state + .config + .as_ref() + .map(|config| config.properties.dst.size != cursor_plane_size) + }) + .unwrap_or(true); + + // check if the cursor plane location changed + let reposition = previous_state + .plane_state(plane_info.handle) + .and_then(|state| { + state + .config + .as_ref() + .map(|config| config.properties.dst.loc != cursor_plane_location) + }) + .unwrap_or(true); + + // ok, nothing changed, try to keep the previous state + if !render && !reposition { + let mut plane_state = previous_state.plane_state(plane_info.handle).unwrap().clone(); + plane_state.skip = true; + // Note: we know that we had a cursor plane in the + // previous frame and that nothing changed. In this + // case skip the whole testing + plane_state.needs_test = false; + frame_state.set_state(plane_info.handle, plane_state); + return Some(plane_info.into()); + } + + // we no not have to re-render but update the planes location + if !render && reposition { + trace!("repositioning cursor plane"); + let mut plane_state = previous_state.plane_state(plane_info.handle).unwrap().clone(); + plane_state.skip = false; + // Note: we know that we had a cursor plane in the + // previous frame, so we assume a simple location change + // does not not to be tested + plane_state.needs_test = false; + let config = plane_state.config.as_mut().unwrap(); + config.properties.dst.loc = cursor_plane_location; + frame_state.set_state(plane_info.handle, plane_state); + return Some(plane_info.into()); + } + + trace!( + "trying to render element {:?} on cursor {:?}", + element.id(), + plane_info.handle + ); + + // if we fail to create a buffer we can just return false and + // force the cursor to be rendered on the primary plane + let mut cursor_buffer = match cursor_state.allocator.create_buffer( + cursor_plane_size.w as u32, + cursor_plane_size.h as u32, + DrmFourcc::Argb8888, + &[DrmModifier::Linear], + ) { + Ok(buffer) => buffer, + Err(err) => { + debug!("failed to create cursor buffer: {}", err); + return None; + } + }; + + // if we fail to export a framebuffer for our buffer we can skip the rest + let framebuffer = match cursor_state.framebuffer_exporter.add_framebuffer( + self.surface.device_fd(), + ExportBuffer::Allocator(&cursor_buffer), + false, + ) { + Ok(Some(fb)) => fb, + Ok(None) => { + debug!( + "failed to export framebuffer for cursor {:?}: no framebuffer available", + plane_info.handle + ); + return None; + } + Err(err) => { + debug!( + "failed to export framebuffer for cursor {:?}: {}", + plane_info.handle, err + ); + return None; + } + }; + + let cursor_buffer_size = cursor_plane_size.to_logical(1).to_buffer(1, Transform::Normal); + + #[cfg(not(feature = "renderer_pixman"))] + if !copy_element_to_cursor_bo( + renderer, + element, + element_size, + cursor_plane_size, + output_transform, + &mut cursor_buffer, + ) { + tracing::trace!("failed to copy element to cursor bo, skipping element on cursor plane"); + return None; + } + + #[cfg(feature = "renderer_pixman")] + if !copy_element_to_cursor_bo( + renderer, + element, + element_size, + cursor_plane_size, + output_transform, + &mut cursor_buffer, + ) { + profiling::scope!("render cursor plane"); + tracing::trace!("cursor fast-path copy failed, falling back to rendering using offscreen buffer"); + + let Some(storage) = element.underlying_storage(renderer) else { + trace!("Can't obtain cursor's underlying storage"); + return None; + }; + + let pixman_renderer = cursor_state.pixman_renderer.as_mut()?; + + // Create a pixman image from the source cursor data. This will either be set by the + // client, or the compositor's choice. + let cursor_texture = match storage { + UnderlyingStorage::Wayland(buffer) => pixman_renderer + .import_buffer(buffer, None, &[element.src().to_i32_up()]) + .transpose() + .ok() + .flatten(), + UnderlyingStorage::Memory(memory) => { + let format = memory.format(); + let size = memory.size(); + let Ok(pixman_format) = pixman::FormatCode::try_from(format) else { + debug!("No pixman format for {format}"); + return None; + }; + unsafe { + match pixman::Image::from_raw_mut( + pixman_format, + size.w as usize, + size.h as usize, + memory.as_ptr() as *mut u32, + memory.stride() as usize, + false, + ) { + Ok(image) => Some(PixmanTexture::from(image)), + Err(e) => { + debug!("pixman cursor: {e}"); + None + } + } + } + } + }?; + + let ret = cursor_buffer + .map_mut::<_, Result<_, PixmanError>>( + 0, + 0, + cursor_buffer_size.w as u32, + cursor_buffer_size.h as u32, + |mbo| { + let plane_pixman_format = pixman::FormatCode::try_from(DrmFourcc::Argb8888).unwrap(); + let mut cursor_dst = unsafe { + pixman::Image::from_raw_mut( + plane_pixman_format, + mbo.width() as usize, + mbo.height() as usize, + mbo.buffer_mut().as_mut_ptr() as *mut u32, + mbo.stride() as usize, + false, + ) + } + .map_err(|_| PixmanError::ImportFailed)?; + let mut framebuffer = pixman_renderer.bind(&mut cursor_dst)?; + let mut frame = + pixman_renderer.render(&mut framebuffer, cursor_plane_size, output_transform)?; + frame.clear(Color32F::TRANSPARENT, &[Rectangle::from_size(cursor_plane_size)])?; + let src = element.src(); + let dst = Rectangle::from_size(element_geometry.size); + frame.render_texture_from_to( + &cursor_texture, + src, + dst, + &[dst], + &[], + element.transform(), + element.alpha(), + )?; + let _ = frame.finish()?.wait(); // what can we do? + Ok(()) + }, + ) + .expect("Lost track of cursor device"); + + if let Err(err) = ret { + debug!("{err}"); + return None; + } + }; + + let src = Rectangle::from_size(cursor_buffer_size).to_f64(); + let dst = Rectangle::new(cursor_plane_location, cursor_plane_size); + + let config = PlaneConfig { + properties: PlaneProperties { + src, + dst, + alpha: 1.0, + transform: Transform::Normal, + format: framebuffer.format(), + }, + buffer: DrmScanoutBuffer { + buffer: ScanoutBuffer::Cursor(Arc::new(cursor_buffer)), + fb: CachedDrmFramebuffer::new(DrmFramebuffer::Gbm(framebuffer)), + }, + damage_clips: None, + plane_claim, + sync: None, + }; + let is_compatible = previous_state + .plane_state(plane_info.handle) + .map(|state| { + state + .config + .as_ref() + .map(|other| { + // Note: We do not use the plane config `is_compatible` test + // here as we exclude the destination location from the test + other.properties.src == config.properties.src + && other.properties.dst.size == config.properties.dst.size + && other.properties.alpha == config.properties.alpha + && other.properties.transform == config.properties.transform + && other.properties.format == config.properties.format + }) + .unwrap_or(false) + }) + .unwrap_or(false); + + let plane_state = PlaneState { + skip: false, + // Note: we assume we only have to test if the plane is + // not compatible. This should only happen if we either + // had no cursor plane before or we did direct scan-out + // on it. A simple re-position without re-render is + // already handled earlier. + needs_test: !is_compatible, + element_state: Some(PlaneElementState { + id: element.id().clone(), + commit: element.current_commit(), + z_index: element_zindex, + cursor_size: Some(element_size), + }), + config: Some(config), + }; + + let res = if is_compatible { + frame_state.set_state(plane_info.handle, plane_state); + true + } else { + frame_state + .test_state( + &self.surface, + self.supports_fencing, + plane_info.handle, + plane_state, + false, + ) + .is_ok() + }; + + if res { + cursor_state.previous_output_scale = Some(scale); + cursor_state.previous_output_transform = Some(output_transform); + Some(plane_info.into()) + } else { + info!("failed to test cursor {:?} state", plane_info.handle); + None + } + } + + #[allow(clippy::too_many_arguments)] + #[instrument(level = "trace", skip_all)] + #[profiling::function] + fn element_config<'a, R, E>( + &mut self, + renderer: &mut R, + element: &E, + element_zindex: usize, + element_geometry: Rectangle, + element_states: &'a mut IndexMap>::Framebuffer>>, + frame_state: &mut CompositorFrameState, + output_transform: Transform, + output_geometry: Rectangle, + allow_opaque_fallback: bool, + ) -> Result< + ElementPlaneConfig< + 'a, + ::Buffer, + ::Buffer>>::Framebuffer, + >, + ExportBufferError, + > + where + R: Renderer, + E: RenderElement, + { + let element_id = element.id(); + + // We can only try to do direct scan-out for element that provide a underlying storage + let underlying_storage = element + .underlying_storage(renderer) + .ok_or(ExportBufferError::NoUnderlyingStorage)?; + + let export_buffer = ExportBuffer::from_underlying_storage(&underlying_storage) + .ok_or(ExportBufferError::Unsupported)?; + + if !self.framebuffer_exporter.can_add_framebuffer(&export_buffer) { + return Err(ExportBufferError::Unsupported); + } + + // First we try to find a state in our new states, this is important if + // we got the same id multiple times. If we can't find it we use the previous + // state if available + if !element_states.contains_key(element_id) { + let previous_fb_cache = self + .previous_element_states + .get_mut(element_id) + // Note: We can mem::take the old fb_cache here here as we guarantee that + // the element state will always overwrite the current state at the end of render_frame + .map(|state| std::mem::take(&mut state.fb_cache)) + .unwrap_or_default(); + element_states.insert( + element_id.clone(), + ElementState { + instances: SmallVec::new(), + fb_cache: previous_fb_cache, + }, + ); + } + let element_fb_cache: &mut ElementFramebufferCache< + ::Buffer>>::Framebuffer, + > = element_states + .get_mut(element_id) + .map(|state| &mut state.fb_cache) + .unwrap(); + + let element_cache_key = + ElementFramebufferCacheKey::from_underlying_storage(&underlying_storage, allow_opaque_fallback) + .ok_or(ExportBufferError::Unsupported)?; + let cached_fb = element_fb_cache.get(&element_cache_key); + + if cached_fb.is_none() { + trace!( + "no cached fb, exporting new fb for element {:?} underlying storage {:?}", + element_id, &underlying_storage + ); + + let fb = self + .framebuffer_exporter + .add_framebuffer(self.surface.device_fd(), export_buffer, allow_opaque_fallback) + .map_err(|err| { + trace!("failed to add framebuffer: {:?}", err); + ExportBufferError::ExportFailed + }) + .and_then(|fb| { + fb.map(|fb| CachedDrmFramebuffer::new(DrmFramebuffer::Exporter(fb))) + .ok_or(ExportBufferError::Unsupported) + }); + + if fb.is_err() { + trace!( + "could not import framebuffer for element {:?} underlying storage {:?}", + element_id, &underlying_storage + ); + } + + element_fb_cache.insert(element_cache_key.clone(), fb); + } else { + trace!( + "using cached fb for element {:?} underlying storage {:?}", + element_id, &underlying_storage + ); + } + + let fb: &CachedDrmFramebuffer<::Buffer>>::Framebuffer> = + element_fb_cache.get(&element_cache_key).unwrap()?; + + let src = element.src(); + let dst = output_transform.transform_rect_in(element_geometry, &output_geometry.size); + // the output transform we are passed is already inverted to represent CW rotation (this is done to match what the + // renderer is doing), but drm and the elements actually use/expect CCW rotation. to solve this we just invert + // the transform again here. + let transform = apply_output_transform( + apply_underlying_storage_transform(element.transform(), &underlying_storage), + output_transform.invert(), + ); + let alpha = element.alpha(); + let properties = PlaneProperties { + src, + dst, + alpha, + transform, + format: fb.format(), + }; + let buffer: DrmScanoutBuffer< + ::Buffer, + ::Buffer>>::Framebuffer, + > = ScanoutBuffer::from_underlying_storage(underlying_storage) + .map(|buffer| DrmScanoutBuffer { + fb: fb.clone(), + buffer, + }) + .ok_or(ExportBufferError::Unsupported)?; + + if !element_states + .get(element_id) + .unwrap() + .instances + .iter() + .any(|i| i.properties == properties) + { + let overlay_bitmask = + self.planes + .overlay + .iter() + .enumerate() + .fold(0u32, |mut acc, (index, plane)| { + if frame_state.is_assigned(plane.handle) { + acc |= 1 << index; + } + acc + }); + let cursor_bitmask = + self.planes + .cursor + .iter() + .enumerate() + .fold(0u32, |mut acc, (index, plane)| { + if frame_state.is_assigned(plane.handle) { + acc |= 1 << index; + } + acc + }); + let current_plane_snapshot = PlanesSnapshot { + primary: frame_state.is_assigned(self.surface.plane()), + cursor_bitmask, + overlay_bitmask, + }; + + let element_state = element_states.get_mut(element_id).unwrap(); + element_state.instances.push(ElementInstanceState { + properties, + active_planes: current_plane_snapshot, + failed_planes: Default::default(), + }); + + if let Some(previous_state) = self.previous_element_states.get(element_id) { + // lets look if we find a previous instance with exactly the same properties. + // if we find one we can test if nothing changed and re-use the failed tests + let matching_instance = previous_state + .instances + .iter() + .find(|i| i.properties == properties); + + if let Some(matching_instance) = matching_instance { + if current_plane_snapshot == matching_instance.active_planes { + let previous_frame_state = self + .pending_frame + .as_ref() + .map(|pending| &pending.frame) + .unwrap_or(&self.current_frame); + + // Note: we ignore the cursor plane here as this would result + // in constant re-tests of cursor moves and we do not expect + // that to influence the test state of our elements. + // Adding or removing cursor can influence the other planes, but + // is already covered in the active planes check. + let primary_plane_changed = if current_plane_snapshot.primary { + frame_state.plane_properties(self.surface.plane()) + != previous_frame_state.plane_properties(self.surface.plane()) + } else { + false + }; + + let overlay_plane_changed = + self.planes.overlay.iter().enumerate().any(|(index, plane)| { + // we only want to test planes that are currently in use + if current_plane_snapshot.overlay_bitmask & (1 << index) == 0 { + return false; + } + + frame_state.plane_properties(plane.handle) + != previous_frame_state.plane_properties(plane.handle) + }); + + if !(primary_plane_changed || overlay_plane_changed) { + // we now know that nothing changed and we can assume any previously failed + // test will again fail + let instance_state = element_state + .instances + .iter_mut() + .find(|i| i.properties == properties) + .unwrap(); + instance_state.failed_planes = matching_instance.failed_planes; + } + } + } + } + } + + let failed_planes = element_states + .get_mut(element_id) + .unwrap() + .instances + .iter_mut() + .find_map(|i| { + if i.properties == properties { + Some(&mut i.failed_planes) + } else { + None + } + }) + .unwrap(); + + Ok(ElementPlaneConfig { + properties, + z_index: element_zindex, + geometry: element_geometry, + buffer, + failed_planes, + }) + } + + #[allow(clippy::too_many_arguments)] + #[instrument(level = "trace", skip_all)] + #[profiling::function] + fn try_assign_overlay_plane<'a, R, E>( + &mut self, + renderer: &mut R, + element: &'a E, + element_zindex: usize, + element_geometry: Rectangle, + element_is_opaque: bool, + element_states: &mut IndexMap>::Framebuffer>>, + primary_plane_elements: &[&'a E], + scale: Scale, + frame_state: &mut CompositorFrameState, + output_transform: Transform, + output_geometry: Rectangle, + frame_flags: FrameFlags, + ) -> Result> + where + R: Renderer, + E: RenderElement, + { + if !frame_flags.contains(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT) { + return Err(None); + } + + // only try to assign elements on an overlay plane that indicate so + if element.kind() != Kind::ScanoutCandidate && element.kind() != Kind::Cursor { + trace!( + "skipping element {:?} on overlay plane(s), element kind not scanout-candidate/cursor", + element.id(), + ); + return Err(None); + } + + let element_id = element.id(); + + // Check if we have a free plane, otherwise we can exit early + if self + .planes + .overlay + .iter() + .all(|plane| frame_state.is_assigned(plane.handle)) + { + trace!( + "skipping overlay planes for element {:?}, no free planes", + element_id + ); + return Err(None); + } + + let element_config = self.element_config( + renderer, + element, + element_zindex, + element_geometry, + element_states, + frame_state, + output_transform, + output_geometry, + false, + )?; + + let overlaps_with_primary_plane_element = primary_plane_elements.iter().any(|e| { + let other_geometry = e.geometry(scale); + other_geometry.overlaps(element_config.geometry) + }); + + let primary_plane_has_alpha = frame_state + .plane_buffer(self.surface.plane()) + .map(|state| has_alpha(state.format().code)) + .unwrap_or(false); + + let previous_frame_state = self + .pending_frame + .as_ref() + .map(|pending| &pending.frame) + .unwrap_or(&self.current_frame); + + // We consider a plane compatible if the z-index of the previous assigned + // element is or equal to our z-index and the properties (src/dst/format/...) + // are equal. The reason for the z-index limitation is that we do not want + // to assign ourself to the same plane if our z-index changed. That could + // result in assigning the element on a lower plane as necessary and then + // blocking direct scan-out for some other element + let is_plane_compatible = |plane: &&PlaneInfo| { + previous_frame_state + .plane_state(plane.handle) + .map(|state| { + state + .element_state + .as_ref() + .map(|state| state.z_index <= element_config.z_index) + .unwrap_or(false) + && state + .config + .as_ref() + .map(|config| config.properties.is_compatible(&element_config.properties)) + .unwrap_or(false) + }) + .unwrap_or(false) + }; + + let mut test_overlay_plane = |plane: &PlaneInfo, + element_config: &ElementPlaneConfig< + '_, + ::Buffer, + ::Buffer>>::Framebuffer, + >| { + // something is already assigned to our overlay plane + if frame_state.is_assigned(plane.handle) { + trace!( + "skipping {:?} with zpos {:?} for element {:?}, already has element assigned, skipping", + plane.handle, plane.zpos, element_id, + ); + return Err(None); + } + + // test if the plane represents an underlay + let is_underlay = + self.surface.plane_info().zpos.unwrap_or_default() > plane.zpos.unwrap_or_default(); + + if is_underlay && !(element_is_opaque && primary_plane_has_alpha) { + trace!( + "skipping direct scan-out on underlay {:?} with zpos {:?}, element {:?} is not opaque or primary plane has no alpha channel", + plane.handle, plane.zpos, element_id + ); + return Err(None); + } + + // if the element overlaps with an element on + // the primary plane and is not an underlay + // we can not assign it to any overlay plane + if overlaps_with_primary_plane_element && !is_underlay { + trace!( + "skipping direct scan-out on {:?} with zpos {:?}, element {:?} overlaps with element on primary plane", + plane.handle, plane.zpos, element_id, + ); + return Err(None); + } + + let overlaps_with_plane_underneath = self + .planes + .overlay + .iter() + .filter(|info| { + info.handle != plane.handle + && info.zpos.unwrap_or_default() <= plane.zpos.unwrap_or_default() + }) + .any(|overlapping_plane| { + frame_state.overlaps(overlapping_plane.handle, element_config.geometry) + }); + + // if we overlap we a plane below which already + // has an element assigned we can not use the + // plane for direct scan-out + if overlaps_with_plane_underneath { + trace!( + "skipping direct scan-out on {:?} with zpos {:?}, element {:?} geometry {:?} overlaps with plane underneath", + plane.handle, plane.zpos, element_id, element_config.geometry, + ); + return Err(None); + } + + self.try_assign_plane(element, element_config, plane, scale, frame_state) + }; + + // First try to assign the element to a compatible plane, this can save us + // from some atomic testing + for plane in self.planes.overlay.iter().filter(is_plane_compatible) { + if let Ok(plane_assignment) = test_overlay_plane(plane, &element_config) { + trace!( + "assigned element {:?} geometry {:?} to compatible {:?} with zpos {:?}", + element_id, element_config.geometry, plane.handle, plane.zpos, + ); + return Ok(plane_assignment); + } + } + + // If we found no compatible plane fall back to walk all available planes + let mut rendering_reason: Option = None; + for (index, plane) in self.planes.overlay.iter().enumerate() { + // if the tested element state already tells us that this failed skip the test + if element_config.failed_planes.overlay_bitmask & (1 << index) != 0 { + trace!( + "skipping direct scan-out on {:?} with zpos {:?}, element {:?} geometry {:?}, test already known to fail", + plane.handle, plane.zpos, element_id, element_config.geometry, + ); + rendering_reason = rendering_reason.or(Some(RenderingReason::ScanoutFailed)); + continue; + } + + match test_overlay_plane(plane, &element_config) { + Ok(plane) => return Ok(plane), + Err(err) => { + // if the test failed save that in the tested element state + if let Some(RenderingReason::ScanoutFailed) = err { + element_config.failed_planes.overlay_bitmask |= 1 << index; + } + + rendering_reason = rendering_reason.or(err) + } + } + } + + Err(rendering_reason) + } + + #[allow(clippy::too_many_arguments)] + #[instrument(level = "trace", skip_all)] + #[profiling::function] + fn try_assign_plane( + &self, + element: &E, + element_config: &ElementPlaneConfig< + '_, + ::Buffer, + ::Buffer>>::Framebuffer, + >, + plane: &PlaneInfo, + scale: Scale, + frame_state: &mut CompositorFrameState, + ) -> Result> + where + R: Renderer, + E: RenderElement, + { + let element_id = element.id(); + + let plane_claim = match self.surface.claim_plane(plane.handle) { + Some(claim) => claim, + None => { + trace!("failed to claim {:?} for element {:?}", plane.handle, element_id); + return Err(None); + } + }; + + // Try to assign the element to a plane + trace!( + "testing direct scan-out for element {:?} on {:?} with zpos {:?}: fb: {:?}, element_geometry: {:?}", + element_id, plane.handle, plane.zpos, &element_config.buffer.fb, element_config.geometry + ); + + if !plane.formats.contains(&element_config.properties.format) { + trace!( + "skipping direct scan-out on {:?} with zpos {:?} for element {:?}, format {:?} not supported", + plane.handle, plane.zpos, element_id, element_config.properties.format, + ); + return Err(Some(RenderingReason::FormatUnsupported)); + } + + let previous_state = self + .pending_frame + .as_ref() + .map(|pending| &pending.frame) + .unwrap_or(&self.current_frame); + + let previous_commit = previous_state.plane_state(plane.handle).and_then(|state| { + state.element_state.as_ref().and_then(|state| { + if state.id == *element_id { + Some(state.commit) + } else { + None + } + }) + }); + + let element_damage = element.damage_since(scale, previous_commit); + let has_element_damage = !element_damage.is_empty(); + + // Damage were applied buffer transform to be in physical-space. We need to invert it to go + // back to buffer-coordinate. We'll apply the same transform to the element geometry for + // scale computation as it's already in physical space. + let transform = element.transform().invert(); + let damage_clips = if has_element_damage { + PlaneDamageClips::from_damage( + self.surface.device_fd(), + element_config.properties.src, + element_config.geometry, + transform, + transform, + element_damage, + ) + .ok() + .flatten() + } else { + None + }; + + let config = PlaneConfig { + properties: element_config.properties, + buffer: element_config.buffer.clone(), + damage_clips, + plane_claim, + sync: element_config + .buffer + .buffer + .acquire_point(self.signaled_fence.as_ref()), + }; + + let is_compatible = previous_state + .plane_state(plane.handle) + .map(|state| { + state + .config + .as_ref() + .map(|c| c.is_compatible(&config)) + .unwrap_or(false) + }) + .unwrap_or(false); + + // We can only skip the plane update if we have no damage and if + // the src/dst/alpha properties are unchanged. Also we can not skip if + // the fb did change (this includes the case where we previously + // had not assigned anything to the plane) + let skip = !has_element_damage + && previous_state + .plane_state(plane.handle) + .map(|state| { + state + .config + .as_ref() + .map(|c| is_compatible && c.buffer.fb == config.buffer.fb) + .unwrap_or(false) + }) + .unwrap_or(false); + + let plane_state = PlaneState { + skip, + needs_test: true, + element_state: Some(PlaneElementState { + id: element_id.clone(), + commit: element.current_commit(), + z_index: element_config.z_index, + cursor_size: None, + }), + config: Some(config), + }; + + let res = if is_compatible { + trace!( + "skipping atomic test for compatible element {:?} on {:?} with zpos {:?}", + element_id, plane.handle, plane.zpos, + ); + frame_state.set_state(plane.handle, plane_state); + true + } else { + frame_state + .test_state( + &self.surface, + self.supports_fencing, + plane.handle, + plane_state, + false, + ) + .is_ok() + }; + + if res { + trace!( + "successfully assigned element {:?} to {:?} with zpos {:?} for direct scan-out", + element_id, plane.handle, plane.zpos, + ); + + Ok(plane.into()) + } else { + trace!( + "skipping direct scan-out on {:?} with zpos {:?} for element {:?}, test failed", + plane.handle, plane.zpos, element_id + ); + + Err(Some(RenderingReason::ScanoutFailed)) + } + } + + /// Clear the surface, setting DPMS state to off, disabling all planes, + /// and clearing the pending frame. + /// + /// Calling [`queue_frame`][Self::queue_frame] will re-enable. + pub fn clear(&mut self) -> Result<(), DrmError> { + self.surface.clear()?; + + self.current_frame + .planes + .iter_mut() + .for_each(|(_, state)| *state = Default::default()); + self.pending_frame = None; + self.queued_frame = None; + self.next_frame = None; + + Ok(()) + } +} + +#[inline] +fn apply_underlying_storage_transform( + element_transform: Transform, + storage: &UnderlyingStorage<'_>, +) -> Transform { + match storage { + UnderlyingStorage::Wayland(buffer) => { + if buffer_y_inverted(buffer).unwrap_or(false) { + match element_transform { + Transform::Normal => Transform::Flipped, + Transform::_90 => Transform::Flipped90, + Transform::_180 => Transform::Flipped180, + Transform::_270 => Transform::Flipped270, + Transform::Flipped => Transform::Normal, + Transform::Flipped90 => Transform::_90, + Transform::Flipped180 => Transform::_180, + Transform::Flipped270 => Transform::_270, + } + } else { + element_transform + } + } + UnderlyingStorage::Memory { .. } => element_transform, + } +} + +#[inline] +fn apply_output_transform(transform: Transform, output_transform: Transform) -> Transform { + match (transform, output_transform) { + (Transform::Normal, output_transform) => output_transform, + + (Transform::_90, Transform::Normal) => Transform::_270, + (Transform::_90, Transform::_90) => Transform::Normal, + (Transform::_90, Transform::_180) => Transform::_90, + (Transform::_90, Transform::_270) => Transform::_180, + (Transform::_90, Transform::Flipped) => Transform::Flipped270, + (Transform::_90, Transform::Flipped90) => Transform::Flipped, + (Transform::_90, Transform::Flipped180) => Transform::Flipped90, + (Transform::_90, Transform::Flipped270) => Transform::Flipped180, + + (Transform::_180, Transform::Normal) => Transform::_180, + (Transform::_180, Transform::_90) => Transform::_270, + (Transform::_180, Transform::_180) => Transform::Normal, + (Transform::_180, Transform::_270) => Transform::_90, + (Transform::_180, Transform::Flipped) => Transform::Flipped180, + (Transform::_180, Transform::Flipped90) => Transform::Flipped270, + (Transform::_180, Transform::Flipped180) => Transform::Flipped, + (Transform::_180, Transform::Flipped270) => Transform::Flipped90, + + (Transform::_270, Transform::Normal) => Transform::_90, + (Transform::_270, Transform::_90) => Transform::_180, + (Transform::_270, Transform::_180) => Transform::_270, + (Transform::_270, Transform::_270) => Transform::Normal, + (Transform::_270, Transform::Flipped) => Transform::Flipped90, + (Transform::_270, Transform::Flipped90) => Transform::Flipped180, + (Transform::_270, Transform::Flipped180) => Transform::Flipped270, + (Transform::_270, Transform::Flipped270) => Transform::Flipped, + + (Transform::Flipped, Transform::Normal) => Transform::Flipped, + (Transform::Flipped, Transform::_90) => Transform::Flipped90, + (Transform::Flipped, Transform::_180) => Transform::Flipped180, + (Transform::Flipped, Transform::_270) => Transform::Flipped270, + (Transform::Flipped, Transform::Flipped) => Transform::Normal, + (Transform::Flipped, Transform::Flipped90) => Transform::_90, + (Transform::Flipped, Transform::Flipped180) => Transform::_180, + (Transform::Flipped, Transform::Flipped270) => Transform::_270, + + (Transform::Flipped90, Transform::Normal) => Transform::Flipped270, + (Transform::Flipped90, Transform::_90) => Transform::Flipped, + (Transform::Flipped90, Transform::_180) => Transform::Flipped90, + (Transform::Flipped90, Transform::_270) => Transform::Flipped180, + (Transform::Flipped90, Transform::Flipped) => Transform::_270, + (Transform::Flipped90, Transform::Flipped90) => Transform::Normal, + (Transform::Flipped90, Transform::Flipped180) => Transform::_90, + (Transform::Flipped90, Transform::Flipped270) => Transform::_180, + + (Transform::Flipped180, Transform::Normal) => Transform::Flipped180, + (Transform::Flipped180, Transform::_90) => Transform::Flipped270, + (Transform::Flipped180, Transform::_180) => Transform::Flipped, + (Transform::Flipped180, Transform::_270) => Transform::Flipped90, + (Transform::Flipped180, Transform::Flipped) => Transform::_180, + (Transform::Flipped180, Transform::Flipped90) => Transform::_270, + (Transform::Flipped180, Transform::Flipped180) => Transform::Normal, + (Transform::Flipped180, Transform::Flipped270) => Transform::_90, + + (Transform::Flipped270, Transform::Normal) => Transform::Flipped90, + (Transform::Flipped270, Transform::_90) => Transform::Flipped180, + (Transform::Flipped270, Transform::_180) => Transform::Flipped270, + (Transform::Flipped270, Transform::_270) => Transform::Flipped, + (Transform::Flipped270, Transform::Flipped) => Transform::_90, + (Transform::Flipped270, Transform::Flipped90) => Transform::_180, + (Transform::Flipped270, Transform::Flipped180) => Transform::_270, + (Transform::Flipped270, Transform::Flipped270) => Transform::Normal, + } +} + +#[profiling::function] +fn copy_element_to_cursor_bo( + renderer: &mut R, + element: &E, + element_size: Size, + cursor_size: Size, + output_transform: Transform, + bo: &mut GbmBuffer, +) -> bool +where + R: Renderer, + E: RenderElement, +{ + // Without access to the underlying storage we can not copy anything + let Some(underlying_storage) = element.underlying_storage(renderer) else { + return false; + }; + + let element_src = element.src(); + let element_scale = element_src.size / element_size.to_f64(); + + // We only copy if no crop, scale or transform is active + if element_src.loc != Point::default() + || element_scale != Scale::from(1f64) + || element.transform() != Transform::Normal + || output_transform != Transform::Normal + { + return false; + } + + let bo_format = bo.format().code; + let bo_stride = bo.stride(); + + let mut copy_to_bo = |src, src_stride, src_height| { + if src_stride == bo_stride as i32 { + bo.write(src).is_ok() + } else { + let res = bo.map_mut(0, 0, cursor_size.w as u32, cursor_size.h as u32, |mbo| { + let dst = mbo.buffer_mut(); + for row in 0..src_height { + let src_row_start = (row * src_stride) as usize; + let src_row_end = src_row_start + src_stride as usize; + let src_row = &src[src_row_start..src_row_end]; + let dst_row_start = (row * bo_stride as i32) as usize; + let dst_row_end = dst_row_start + src_stride as usize; + let dst_row = &mut dst[dst_row_start..dst_row_end]; + dst_row.copy_from_slice(src_row); + } + }); + res.is_ok() + } + }; + + match underlying_storage { + UnderlyingStorage::Wayland(buffer) => { + // Only shm buffers are supported for copy + shm::with_buffer_contents(buffer, |ptr, len, data| { + let Some(format) = shm::shm_format_to_fourcc(data.format) else { + return false; + }; + + if format != bo_format { + return false; + }; + + let expected_len = (data.stride * data.height) as usize; + if data.offset as usize + expected_len > len { + return false; + }; + + copy_to_bo( + unsafe { std::slice::from_raw_parts(ptr.offset(data.offset as isize), expected_len) }, + data.stride, + data.height, + ) + }) + .unwrap_or(false) + } + UnderlyingStorage::Memory(memory) => { + if memory.format() != bo_format { + return false; + }; + + copy_to_bo(memory, memory.stride(), memory.size().h) + } + } +} + +struct CachedDrmFramebuffer(Arc>); + +impl PartialEq for CachedDrmFramebuffer { + #[inline] + fn eq(&self, other: &Self) -> bool { + AsRef::::as_ref(&self) == AsRef::::as_ref(&other) + } +} + +impl std::fmt::Debug for CachedDrmFramebuffer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("CachedDrmFramebuffer").field(&self.0).finish() + } +} + +impl CachedDrmFramebuffer { + #[inline] + fn new(buffer: DrmFramebuffer) -> Self { + CachedDrmFramebuffer(Arc::new(buffer)) + } +} + +impl Clone for CachedDrmFramebuffer { + #[inline] + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl AsRef for CachedDrmFramebuffer { + #[inline] + fn as_ref(&self) -> &framebuffer::Handle { + (*self.0).as_ref() + } +} + +impl Framebuffer for CachedDrmFramebuffer { + #[inline] + fn format(&self) -> drm_fourcc::DrmFormat { + (*self.0).format() + } +} + +/// Errors thrown by a [`DrmCompositor`] +#[derive(Debug, thiserror::Error)] +pub enum FrameError< + A: std::error::Error + Send + Sync + 'static, + B: std::error::Error + Send + Sync + 'static, + F: std::error::Error + Send + Sync + 'static, +> { + /// Failed to claim the primary plane + #[error("Failed to claim the primary plane")] + PrimaryPlaneClaimFailed, + /// No supported pixel format for the given plane could be determined + #[error("No supported plane buffer format found")] + NoSupportedPlaneFormat, + /// No supported pixel format for the given renderer could be determined + #[error("No supported renderer buffer format found")] + NoSupportedRendererFormat, + /// The swapchain is exhausted, you need to call `frame_submitted` + #[error("Failed to allocate a new buffer")] + NoFreeSlotsError, + /// Error accessing the drm device + #[error("The underlying drm surface encountered an error: {0}")] + DrmError(#[from] DrmError), + /// Error during buffer allocation + #[error("The underlying allocator encountered an error: {0}")] + Allocator(#[source] A), + /// Error during exporting the buffer as dmabuf + #[error("Failed to export the allocated buffer as dmabuf: {0}")] + AsDmabufError(#[source] B), + /// Error during exporting a framebuffer + #[error("The framebuffer export encountered an error: {0}")] + FramebufferExport(#[source] F), + /// No framebuffer available + #[error("No framebuffer available")] + NoFramebuffer, + /// The frame is empty + /// + /// Possible reasons include not calling `render_frame` prior to + /// `queue_frame` or trying to queue a frame without changes. + #[error("No frame has been prepared or it does not contain any changes")] + EmptyFrame, +} + +/// Error returned from [`DrmCompositor::render_frame`] +#[derive(thiserror::Error)] +pub enum RenderFrameError< + A: std::error::Error + Send + Sync + 'static, + B: std::error::Error + Send + Sync + 'static, + F: std::error::Error + Send + Sync + 'static, + R: std::error::Error, +> { + /// Preparing the frame encountered an error + #[error(transparent)] + PrepareFrame(#[from] FrameError), + /// Rendering the frame encountered en error + #[error(transparent)] + RenderFrame(#[from] OutputDamageTrackerError), +} + +impl std::fmt::Debug for RenderFrameError +where + A: std::error::Error + Send + Sync + 'static, + B: std::error::Error + Send + Sync + 'static, + F: std::error::Error + Send + Sync + 'static, + R: std::error::Error, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PrepareFrame(arg0) => f.debug_tuple("PrepareFrame").field(arg0).finish(), + Self::RenderFrame(arg0) => f.debug_tuple("RenderFrame").field(arg0).finish(), + } + } +} + +impl< + A: std::error::Error + Send + Sync + 'static, + B: std::error::Error + Send + Sync + 'static, + F: std::error::Error + Send + Sync + 'static, +> From> for SwapBuffersError +{ + #[inline] + fn from(err: FrameError) -> SwapBuffersError { + match err { + x @ FrameError::NoSupportedPlaneFormat + | x @ FrameError::NoSupportedRendererFormat + | x @ FrameError::PrimaryPlaneClaimFailed + | x @ FrameError::NoFramebuffer => SwapBuffersError::ContextLost(Box::new(x)), + x @ FrameError::NoFreeSlotsError | x @ FrameError::EmptyFrame => { + SwapBuffersError::TemporaryFailure(Box::new(x)) + } + FrameError::DrmError(err) => err.into(), + FrameError::Allocator(err) => SwapBuffersError::ContextLost(Box::new(err)), + FrameError::AsDmabufError(err) => SwapBuffersError::ContextLost(Box::new(err)), + FrameError::FramebufferExport(err) => SwapBuffersError::ContextLost(Box::new(err)), + } + } +} + +fn nvidia_drm_version() -> Option<(u32, u32, u32)> { + let ver = std::fs::read_to_string("/sys/module/nvidia_drm/version").ok()?; + let mut components = ver.trim().split('.'); + let major = u32::from_str(components.next()?).ok()?; + let minor = u32::from_str(components.next()?).ok()?; + let patch = u32::from_str(components.next()?).ok()?; + Some((major, minor, patch)) +} + +#[test] +fn drm_compositor_is_send() { + use std::marker::PhantomData; + + use crate::backend::drm::DrmDeviceFd; + + fn is_send() { + let _ = PhantomData::; + } + + is_send::, GbmFramebufferExporter, (), DrmDeviceFd>>( + ); +} diff --git a/vendor/smithay/src/backend/drm/device/atomic.rs b/vendor/smithay/src/backend/drm/device/atomic.rs new file mode 100644 index 0000000..b0ddf1d --- /dev/null +++ b/vendor/smithay/src/backend/drm/device/atomic.rs @@ -0,0 +1,346 @@ +use std::collections::HashMap; +use std::sync::{ + Arc, RwLock, + atomic::{AtomicBool, Ordering}, +}; + +use drm::control::atomic::AtomicModeReq; +use drm::control::{ + AtomicCommitFlags, Device as ControlDevice, PropertyValueSet, ResourceHandle, connector, crtc, + framebuffer, plane, property, +}; + +use super::DrmDeviceFd; +use crate::backend::drm::error::AccessError; +use crate::{backend::drm::error::Error, utils::DevPath}; + +use tracing::{debug, error, info_span, trace}; + +type OldState = ( + Vec<(connector::Handle, PropertyValueSet)>, + Vec<(crtc::Handle, PropertyValueSet)>, + Vec<(framebuffer::Handle, PropertyValueSet)>, + Vec<(plane::Handle, PropertyValueSet)>, +); + +#[derive(Clone, Debug, Default)] +pub struct PropMapping { + pub connectors: HashMap>, + pub crtcs: HashMap>, + pub planes: HashMap>, +} + +impl PropMapping { + pub(crate) fn conn_prop_handle( + &self, + handle: connector::Handle, + name: &'static str, + ) -> Result { + self.connectors + .get(&handle) + .ok_or(Error::UnknownConnector(handle))? + .get(name) + .ok_or_else(|| Error::UnknownProperty { + handle: handle.into(), + name, + }) + .copied() + } + + pub(crate) fn crtc_prop_handle( + &self, + handle: crtc::Handle, + name: &'static str, + ) -> Result { + self.crtcs + .get(&handle) + .ok_or(Error::UnknownCrtc(handle))? + .get(name) + .ok_or_else(|| Error::UnknownProperty { + handle: handle.into(), + name, + }) + .copied() + } + + pub(crate) fn plane_prop_handle( + &self, + handle: plane::Handle, + name: &'static str, + ) -> Result { + self.planes + .get(&handle) + .ok_or(Error::UnknownPlane(handle))? + .get(name) + .ok_or_else(|| Error::UnknownProperty { + handle: handle.into(), + name, + }) + .copied() + } +} + +#[derive(Debug)] +pub struct AtomicDrmDevice { + pub(crate) fd: DrmDeviceFd, + pub(crate) active: Arc, + old_state: OldState, + pub(crate) prop_mapping: Arc>, + pub(super) span: tracing::Span, +} + +impl AtomicDrmDevice { + pub fn new(fd: DrmDeviceFd, active: Arc, disable_connectors: bool) -> Result { + let span = info_span!("drm_atomic"); + let mut dev = AtomicDrmDevice { + fd, + active, + old_state: (Vec::new(), Vec::new(), Vec::new(), Vec::new()), + prop_mapping: Default::default(), + span, + }; + let _guard = dev.span.enter(); + + // Enumerate (and save) the current device state. + let res_handles = dev.fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading drm resources", + dev: dev.fd.dev_path(), + source, + }) + })?; + + let planes = dev.fd.plane_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading planes", + dev: dev.fd.dev_path(), + source, + }) + })?; + + let mut old_state = dev.old_state.clone(); + let mut mapping = dev.prop_mapping.write().unwrap(); + + // This helper function takes a snapshot of the current device properties. + // (everything in the atomic api is set via properties.) + add_props(&dev.fd, res_handles.connectors(), &mut old_state.0)?; + add_props(&dev.fd, res_handles.crtcs(), &mut old_state.1)?; + add_props(&dev.fd, res_handles.framebuffers(), &mut old_state.2)?; + add_props(&dev.fd, &planes, &mut old_state.3)?; + + // And because the mapping is not consistent across devices, + // we also need to lookup the handle for a property name. + // And we do this a fair bit, so lets cache that mapping. + map_props(&dev.fd, res_handles.connectors(), &mut mapping.connectors)?; + map_props(&dev.fd, res_handles.crtcs(), &mut mapping.crtcs)?; + map_props(&dev.fd, &planes, &mut mapping.planes)?; + + dev.old_state = old_state; + trace!("Mapping: {:#?}", mapping); + + drop(mapping); + + // If the user does not explicitly requests us to skip this, + // we clear out the complete connector<->crtc mapping on device creation. + // + // The reason is, that certain operations may be racy otherwise. Surfaces can + // exist on different threads: as a result, we cannot really enumerate the current state + // (it might be changed on another thread during the enumeration). And commits can fail, + // if e.g. a connector is already bound to another surface, which is difficult to analyse at runtime. + // + // An easy workaround is to set a known state on device creation, so we can only + // run into these errors on our own and not because previous compositors left the device + // in a funny state. + if disable_connectors { + debug!("Resetting drm device to known state"); + dev.reset_state()?; + } + + drop(_guard); + Ok(dev) + } + + pub(super) fn reset_state(&self) -> Result<(), Error> { + // reset state sets the connectors into a known state (all disabled), + // for the same reasons we do this on device creation. + // + // We might end up with conflicting commit requirements, if we want to restore our state, + // on top of the state the previous compositor left the device in. + // This is because we do commits per surface and not per device, so we do a global + // commit here, to fix any conflicts. + let res_handles = self.fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading drm resources", + dev: self.fd.dev_path(), + source, + }) + })?; + let plane_handles = self.fd.plane_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading drm plane resources", + dev: self.fd.dev_path(), + source, + }) + })?; + + let mut prop_mapping = self.prop_mapping.write().unwrap(); + + // Make sure the mapping is up to date + prop_mapping.connectors.clear(); + map_props(&self.fd, res_handles.connectors(), &mut prop_mapping.connectors)?; + + // Disable all connectors (otherwise we might run into conflicting commits when restarting the rendering loop) + let mut req = AtomicModeReq::new(); + for conn in res_handles.connectors() { + let prop = prop_mapping + .conn_prop_handle(*conn, "CRTC_ID") + .expect("Unknown property CRTC_ID"); + req.add_property(*conn, prop, property::Value::CRTC(None)); + } + // Disable all planes + for plane in plane_handles { + let prop = prop_mapping + .plane_prop_handle(plane, "CRTC_ID") + .expect("Unknown property CRTC_ID"); + req.add_property(plane, prop, property::Value::CRTC(None)); + + let prop = prop_mapping + .plane_prop_handle(plane, "FB_ID") + .expect("Unknown property FB_ID"); + req.add_property(plane, prop, property::Value::Framebuffer(None)); + } + // A crtc without a connector has no mode, we also need to reset that. + // Otherwise the commit will not be accepted. + for crtc in res_handles.crtcs() { + let mode_prop = prop_mapping + .crtc_prop_handle(*crtc, "MODE_ID") + .expect("Unknown property MODE_ID"); + let active_prop = prop_mapping + .crtc_prop_handle(*crtc, "ACTIVE") + .expect("Unknown property ACTIVE"); + req.add_property(*crtc, active_prop, property::Value::Boolean(false)); + req.add_property(*crtc, mode_prop, property::Value::Unknown(0)); + } + self.fd + .atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to disable connectors", + dev: self.fd.dev_path(), + source, + }) + })?; + + Ok(()) + } +} + +impl Drop for AtomicDrmDevice { + fn drop(&mut self) { + if self.active.load(Ordering::SeqCst) { + let _guard = self.span.enter(); + + // Here we restore the card/tty's to it's previous state. + // In case e.g. getty was running on the tty sets the correct framebuffer again, + // so that getty will be visible. + // We do exit correctly if this fails, but the user will be presented with + // a black screen if no display handler takes control again. + + // create an atomic mode request consisting of all properties we captured on creation. + // TODO, check current connector status and remove deactivated connectors from this req. + + debug!("Device still active, trying to restore previous state"); + let mut req = AtomicModeReq::new(); + fn add_multiple_props( + req: &mut AtomicModeReq, + old_state: &[(T, PropertyValueSet)], + ) { + for (handle, set) in old_state { + let (prop_handles, values) = set.as_props_and_values(); + for (&prop_handle, &val) in prop_handles.iter().zip(values.iter()) { + req.add_raw_property((*handle).into(), prop_handle, val); + } + } + } + + add_multiple_props(&mut req, &self.old_state.0); + add_multiple_props(&mut req, &self.old_state.1); + add_multiple_props(&mut req, &self.old_state.2); + add_multiple_props(&mut req, &self.old_state.3); + + trace!("Previous state: {:?}", req); + if let Err(err) = self.fd.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req) { + error!("Failed to restore previous state. Error: {}", err); + } + } + } +} + +// Add all properties of given handles to a given drm resource type to state. +// You may use this to snapshot the current state of the drm device (fully or partially). +fn add_props(fd: &D, handles: &[T], state: &mut Vec<(T, PropertyValueSet)>) -> Result<(), Error> +where + D: DevPath + ControlDevice, + T: ResourceHandle, +{ + let iter = handles.iter().map(|x| (x, fd.get_properties(*x))); + if let Some(len) = iter.size_hint().1 { + state.reserve_exact(len) + } + + iter.map(|(x, y)| (*x, y)) + .try_for_each(|(x, y)| match y { + Ok(y) => { + state.push((x, y)); + Ok(()) + } + Err(err) => Err(err), + }) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error reading properties", + dev: fd.dev_path(), + source, + }) + }) +} + +/// Create a mapping of property names and handles for given handles of a given drm resource type. +/// You may use this to easily lookup properties by name instead of going through this procedure manually. +pub(in crate::backend::drm) fn map_props( + fd: &D, + handles: &[T], + mapping: &mut HashMap>, +) -> Result<(), Error> +where + D: DevPath + ControlDevice, + T: ResourceHandle + Eq + std::hash::Hash, +{ + handles + .iter() + .map(|x| (x, fd.get_properties(*x))) + .try_for_each(|(handle, props)| { + let mut map = HashMap::new(); + match props { + Ok(props) => { + let (prop_handles, _) = props.as_props_and_values(); + for prop in prop_handles { + if let Ok(info) = fd.get_property(*prop) { + let name = info.name().to_string_lossy().into_owned(); + map.insert(name, *prop); + } + } + mapping.insert(*handle, map); + Ok(()) + } + Err(err) => Err(err), + } + }) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error reading properties", + dev: fd.dev_path(), + source, + }) + }) +} diff --git a/vendor/smithay/src/backend/drm/device/fd.rs b/vendor/smithay/src/backend/drm/device/fd.rs new file mode 100644 index 0000000..2f44245 --- /dev/null +++ b/vendor/smithay/src/backend/drm/device/fd.rs @@ -0,0 +1,141 @@ +use drm::{Device as BasicDevice, control::Device as ControlDevice}; +use std::{ + os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}, + sync::{Arc, Weak}, +}; +use tracing::{error, info, warn}; + +use crate::backend::drm::{DrmNode, NodeType}; +use crate::utils::{DevPath, DeviceFd}; + +#[derive(Debug)] +struct InternalDrmDeviceFd { + fd: DeviceFd, + privileged: bool, +} + +impl PartialEq for InternalDrmDeviceFd { + fn eq(&self, other: &Self) -> bool { + self.fd == other.fd + } +} + +impl Drop for InternalDrmDeviceFd { + fn drop(&mut self) { + info!("Dropping device: {:?}", self.fd.dev_path()); + if self.privileged { + if let Err(err) = self.release_master_lock() { + error!("Failed to drop drm master state. Error: {}", err); + } + } + } +} + +impl AsFd for InternalDrmDeviceFd { + fn as_fd(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } +} +impl BasicDevice for InternalDrmDeviceFd {} +impl ControlDevice for InternalDrmDeviceFd {} + +/// Ref-counted file descriptor of an open drm device +#[derive(Debug, Clone, PartialEq)] +pub struct DrmDeviceFd(Arc); + +impl AsFd for DrmDeviceFd { + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.fd.as_fd() + } +} + +// TODO: drop impl once not needed anymore by smithay or dependencies +impl AsRawFd for DrmDeviceFd { + fn as_raw_fd(&self) -> RawFd { + self.0.fd.as_raw_fd() + } +} + +impl DrmDeviceFd { + /// Create a new `DrmDeviceFd`. + /// + /// This function will try to acquire the master lock for the underlying drm device + /// and release the lock on drop again. + /// For that reason you should never create multiple `DrmDeviceFd` out of the same + /// `DeviceFd`, but instead clone the `DrmDeviceFd`. + /// + /// Failing to do so might fail to acquire set lock and release it early, + /// which can cause some drm ioctls to fail later. + pub fn new(fd: DeviceFd) -> DrmDeviceFd { + let mut dev = InternalDrmDeviceFd { + fd, + privileged: false, + }; + + // Render nodes have no master; the kernel refuses SET_MASTER on them. + let is_render_node = DrmNode::from_file(&dev.fd).is_ok_and(|node| node.ty() == NodeType::Render); + + // We want to modeset, so we better be the master, if we run via a tty session. + // This is only needed on older kernels. Newer kernels grant this permission, + // if no other process is already the *master*. So we skip over this error. + if !is_render_node { + if dev.acquire_master_lock().is_err() { + warn!("Unable to become drm master, assuming unprivileged mode"); + } else { + dev.privileged = true; + } + } + + DrmDeviceFd(Arc::new(dev)) + } + + pub(in crate::backend::drm) fn is_privileged(&self) -> bool { + self.0.privileged + } + + /// Returns the underlying `DeviceFd` + pub fn device_fd(&self) -> DeviceFd { + self.0.fd.clone() + } + + /// Returns the `dev_t` of the underlying device + pub fn dev_id(&self) -> rustix::io::Result { + Ok(rustix::fs::fstat(&self.0.fd)?.st_rdev) + } + + /// Returns a weak reference to the underlying device + pub fn downgrade(&self) -> WeakDrmDeviceFd { + WeakDrmDeviceFd(Arc::downgrade(&self.0)) + } +} + +impl BasicDevice for DrmDeviceFd {} +impl ControlDevice for DrmDeviceFd {} + +/// Weak variant of [`DrmDeviceFd`] +#[derive(Debug, Clone, Default)] +pub struct WeakDrmDeviceFd(Weak); + +impl WeakDrmDeviceFd { + /// Construct an empty Weak reference, that will never upgrade successfully + pub fn new() -> Self { + WeakDrmDeviceFd(Weak::new()) + } + + /// Try to upgrade to a strong reference + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(DrmDeviceFd) + } +} + +impl PartialEq for WeakDrmDeviceFd { + fn eq(&self, other: &Self) -> bool { + Weak::ptr_eq(&self.0, &other.0) + } +} + +impl PartialEq for WeakDrmDeviceFd { + fn eq(&self, other: &DrmDeviceFd) -> bool { + Weak::upgrade(&self.0).is_some_and(|arc| Arc::ptr_eq(&arc, &other.0)) + } +} diff --git a/vendor/smithay/src/backend/drm/device/legacy.rs b/vendor/smithay/src/backend/drm/device/legacy.rs new file mode 100644 index 0000000..5a1105a --- /dev/null +++ b/vendor/smithay/src/backend/drm/device/legacy.rs @@ -0,0 +1,222 @@ +use std::collections::HashMap; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; + +use drm::control::{Device as ControlDevice, connector, crtc}; + +use super::DrmDeviceFd; +use crate::backend::drm::error::{AccessError, Error}; +use crate::utils::DevPath; + +use tracing::{debug, error, info_span, trace}; + +#[derive(Debug)] +pub struct LegacyDrmDevice { + pub(crate) fd: DrmDeviceFd, + pub(crate) active: Arc, + old_state: HashMap)>, + pub(super) span: tracing::Span, +} + +impl LegacyDrmDevice { + pub fn new(fd: DrmDeviceFd, active: Arc, disable_connectors: bool) -> Result { + let span = info_span!("drm_legacy"); + let mut dev = LegacyDrmDevice { + fd, + active, + old_state: HashMap::new(), + span, + }; + let _guard = dev.span.enter(); + + // Enumerate (and save) the current device state. + // We need to keep the previous device configuration to restore the state later, + // so we query everything, that we can set. + let res_handles = dev.fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading drm resources", + dev: dev.fd.dev_path(), + source, + }) + })?; + for &con in res_handles.connectors() { + let con_info = dev.fd.get_connector(con, false).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading connector info", + dev: dev.fd.dev_path(), + source, + }) + })?; + if let Some(enc) = con_info.current_encoder() { + let enc_info = dev.fd.get_encoder(enc).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading encoder info", + dev: dev.fd.dev_path(), + source, + }) + })?; + if let Some(crtc) = enc_info.crtc() { + let info = dev.fd.get_crtc(crtc).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading crtc info", + dev: dev.fd.dev_path(), + source, + }) + })?; + dev.old_state + .entry(crtc) + .or_insert((info, Vec::new())) + .1 + .push(con); + } + } + } + + // If the user does not explicitly requests us to skip this, + // we clear out the complete connector<->crtc mapping on device creation. + // + // The reason is, that certain operations may be racy otherwise, as surfaces can + // exist on different threads. As a result, we cannot enumerate the current state + // on surface creation (it might be changed on another thread during the enumeration). + // An easy workaround is to set a known state on device creation. + if disable_connectors { + debug!("Resetting drm device to known state"); + dev.reset_state()?; + } + + drop(_guard); + Ok(dev) + } + + pub(super) fn reset_state(&self) -> Result<(), Error> { + let res_handles = self.fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to query resource handles", + dev: self.fd.dev_path(), + source, + }) + })?; + set_connector_state(&self.fd, res_handles.connectors().iter().copied(), false)?; + + for crtc in res_handles.crtcs() { + #[allow(deprecated)] + let _ = self + .fd + .set_cursor(*crtc, Option::<&drm::control::dumbbuffer::DumbBuffer>::None); + // null commit (necessary to trigger removal on the kernel side with the legacy api.) + self.fd + .set_crtc(*crtc, None, (0, 0), &[], None) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error setting crtc", + dev: self.fd.dev_path(), + source, + }) + })?; + } + + Ok(()) + } +} + +impl Drop for LegacyDrmDevice { + fn drop(&mut self) { + if self.active.load(Ordering::SeqCst) { + let _guard = self.span.enter(); + + // Here we restore the tty to it's previous state. + // In case e.g. getty was running on the tty sets the correct framebuffer again, + // so that getty will be visible. + // We do exit correctly, if this fails, but the user will be presented with + // a black screen, if no display handler takes control again. + + debug!("Device still active, trying to restore previous state"); + for (handle, (info, connectors)) in self.old_state.drain() { + trace!( + framebuffer = ?info.framebuffer(), + offset = ?info.position(), + ?connectors, + mode = ?info.mode(), + "Resetting crtc {:?}", + handle, + ); + if let Err(err) = self.fd.set_crtc( + handle, + info.framebuffer(), + info.position(), + &connectors, + info.mode(), + ) { + error!("Failed to reset crtc ({:?}). Error: {}", handle, err); + } + } + } + } +} + +pub fn set_connector_state( + dev: &D, + connectors: impl Iterator, + enabled: bool, +) -> Result<(), Error> +where + D: DevPath + ControlDevice, +{ + // for every connector... + for conn in connectors { + let info = dev.get_connector(conn, false).map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to get connector infos", + dev: dev.dev_path(), + source, + }) + })?; + // that is currently connected ... + if info.state() == connector::State::Connected { + // get a list of it's properties. + let props = dev.get_properties(conn).map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to get properties for connector", + dev: dev.dev_path(), + source, + }) + })?; + let (handles, _) = props.as_props_and_values(); + // for every handle ... + for handle in handles { + // get information of that property + let info = dev.get_property(*handle).map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to get property of connector", + dev: dev.dev_path(), + source, + }) + })?; + // to find out, if we got the handle of the "DPMS" property ... + if info.name().to_str().map(|x| x == "DPMS").unwrap_or(false) { + // so we can use that to turn on / off the connector + trace!(connector = ?conn, "Setting DPMS {}", enabled); + dev.set_property( + conn, + *handle, + if enabled { + drm_ffi::DRM_MODE_DPMS_ON.into() + } else { + drm_ffi::DRM_MODE_DPMS_OFF.into() + }, + ) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to set property of connector", + dev: dev.dev_path(), + source, + }) + })?; + } + } + } + } + Ok(()) +} diff --git a/vendor/smithay/src/backend/drm/device/mod.rs b/vendor/smithay/src/backend/drm/device/mod.rs new file mode 100644 index 0000000..0075a9e --- /dev/null +++ b/vendor/smithay/src/backend/drm/device/mod.rs @@ -0,0 +1,604 @@ +use std::collections::HashMap; +use std::io; +use std::os::unix::io::{AsFd, BorrowedFd}; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex, Weak, atomic::AtomicBool}; +use std::time::{Duration, SystemTime}; + +use calloop::{EventSource, Interest, Poll, PostAction, Readiness, Token, TokenFactory}; +use drm::control::{Device as ControlDevice, Event, Mode, ResourceHandles, connector, crtc, plane}; +use drm::{ClientCapability, Device as BasicDevice, DriverCapability}; +use libc::dev_t; + +pub(super) mod atomic; +mod fd; +pub use self::fd::{DrmDeviceFd, WeakDrmDeviceFd}; +pub(super) mod legacy; +use crate::utils::{Buffer, DevPath, Size}; + +use super::error::AccessError; +use super::surface::{DrmSurface, DrmSurfaceInternal, atomic::AtomicDrmSurface, legacy::LegacyDrmSurface}; +use super::{Planes, error::Error, planes}; +use atomic::AtomicDrmDevice; +use legacy::LegacyDrmDevice; + +use tracing::{debug_span, error, info, instrument, trace}; + +#[derive(Debug)] +struct PlaneClaimInner { + plane: drm::control::plane::Handle, + crtc: drm::control::crtc::Handle, + storage: PlaneClaimStorage, +} + +impl Drop for PlaneClaimInner { + fn drop(&mut self) { + self.storage.remove(self.plane); + } +} + +#[derive(Debug, Clone)] +struct PlaneClaimWeak(Weak); + +impl PlaneClaimWeak { + fn upgrade(&self) -> Option { + self.0.upgrade().map(|claim| PlaneClaim { claim }) + } +} + +/// A claim of a plane +#[derive(Debug, Clone)] +pub struct PlaneClaim { + claim: Arc, +} + +impl PlaneClaim { + /// The plane the claim was taken for + pub fn plane(&self) -> drm::control::plane::Handle { + self.claim.plane + } + + /// The crtc the claim was taken for + pub fn crtc(&self) -> drm::control::crtc::Handle { + self.claim.crtc + } +} + +impl PlaneClaim { + fn downgrade(&self) -> PlaneClaimWeak { + PlaneClaimWeak(Arc::downgrade(&self.claim)) + } +} + +#[derive(Debug, Clone, Default)] +pub struct PlaneClaimStorage { + claimed_planes: Arc>>, +} + +impl PlaneClaimStorage { + pub fn claim( + &self, + plane: drm::control::plane::Handle, + crtc: drm::control::crtc::Handle, + ) -> Option { + let mut guard = self.claimed_planes.lock().unwrap(); + if let Some(claim) = guard.get(&plane).and_then(|claim| claim.upgrade()) { + if claim.crtc() == crtc { Some(claim) } else { None } + } else { + let claim = PlaneClaim { + claim: Arc::new(PlaneClaimInner { + plane, + crtc, + storage: self.clone(), + }), + }; + guard.insert(plane, claim.downgrade()); + Some(claim) + } + } + + fn remove(&self, plane: drm::control::plane::Handle) { + let mut guard = self.claimed_planes.lock().unwrap(); + guard.remove(&plane); + } +} + +/// An open drm device +#[derive(Debug)] +pub struct DrmDevice { + pub(super) dev_id: dev_t, + pub(crate) internal: Arc, + has_universal_planes: bool, + cursor_size: Size, + resources: ResourceHandles, + plane_claim_storage: PlaneClaimStorage, + surfaces: Vec>, +} + +impl AsFd for DrmDevice { + fn as_fd(&self) -> BorrowedFd<'_> { + match &*self.internal { + DrmDeviceInternal::Atomic(dev) => dev.fd.as_fd(), + DrmDeviceInternal::Legacy(dev) => dev.fd.as_fd(), + } + } +} + +impl BasicDevice for DrmDevice {} +impl ControlDevice for DrmDevice {} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum DrmDeviceInternal { + Atomic(AtomicDrmDevice), + Legacy(LegacyDrmDevice), +} + +impl DrmDeviceInternal { + pub(crate) fn device_fd(&self) -> &DrmDeviceFd { + match self { + DrmDeviceInternal::Atomic(dev) => &dev.fd, + DrmDeviceInternal::Legacy(dev) => &dev.fd, + } + } + + fn span(&self) -> &tracing::Span { + match self { + DrmDeviceInternal::Atomic(internal) => &internal.span, + DrmDeviceInternal::Legacy(internal) => &internal.span, + } + } +} + +impl AsFd for DrmDeviceInternal { + fn as_fd(&self) -> BorrowedFd<'_> { + match self { + DrmDeviceInternal::Atomic(dev) => dev.fd.as_fd(), + DrmDeviceInternal::Legacy(dev) => dev.fd.as_fd(), + } + } +} + +impl BasicDevice for DrmDeviceInternal {} +impl ControlDevice for DrmDeviceInternal {} + +impl DrmDevice { + /// Create a new [`DrmDevice`] from an open drm node + /// + /// # Arguments + /// + /// - `fd` - Open drm node + /// - `disable_connectors` - Setting this to true will initialize all connectors \ + /// as disabled on device creation. smithay enables connectors, when attached \ + /// to a surface, and disables them, when detached. Setting this to `false` \ + /// requires usage of `drm-rs` to disable unused connectors to prevent them \ + /// showing garbage, but will also prevent flickering of already turned on \ + /// connectors (assuming you won't change the resolution). + /// + /// # Return + /// + /// Returns an error if the file is no valid drm node or the device is not accessible. + pub fn new(fd: DrmDeviceFd, disable_connectors: bool) -> Result<(Self, DrmDeviceNotifier), Error> { + // setup parent span for internal device types + let span = debug_span!( + "drm_device", + device = ?fd.dev_path() + ); + let _guard = span.enter(); + + info!("DrmDevice initializing"); + + let dev_id = fd.dev_id().map_err(Error::UnableToGetDeviceId)?; + let active = Arc::new(AtomicBool::new(true)); + + let has_universal_planes = fd + .set_client_capability(ClientCapability::UniversalPlanes, true) + .is_ok(); + let has_monotonic_timestamps = fd + .get_driver_capability(DriverCapability::MonotonicTimestamp) + .unwrap_or(0) + == 1; + let cursor_width = fd + .get_driver_capability(DriverCapability::CursorWidth) + .unwrap_or(64); + let cursor_height = fd + .get_driver_capability(DriverCapability::CursorHeight) + .unwrap_or(64); + let cursor_size = Size::from((cursor_width as u32, cursor_height as u32)); + let resources = fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading resource handles", + dev: fd.dev_path(), + source, + }) + })?; + + let internal = Arc::new(DrmDevice::create_internal(fd, active, disable_connectors)?); + + Ok(( + DrmDevice { + dev_id, + internal: internal.clone(), + has_universal_planes, + cursor_size, + resources, + plane_claim_storage: Default::default(), + surfaces: Default::default(), + }, + DrmDeviceNotifier { + internal, + has_monotonic_timestamps, + token: None, + }, + )) + } + + fn create_internal( + fd: DrmDeviceFd, + active: Arc, + disable_connectors: bool, + ) -> Result { + let force_legacy = std::env::var("SMITHAY_USE_LEGACY") + .map(|x| { + x == "1" || x.to_lowercase() == "true" || x.to_lowercase() == "yes" || x.to_lowercase() == "y" + }) + .unwrap_or(false); + + if force_legacy { + info!("SMITHAY_USE_LEGACY is set. Forcing LegacyDrmDevice."); + }; + + Ok( + if !force_legacy && fd.set_client_capability(ClientCapability::Atomic, true).is_ok() { + DrmDeviceInternal::Atomic(AtomicDrmDevice::new(fd, active, disable_connectors)?) + } else { + info!("Falling back to LegacyDrmDevice"); + DrmDeviceInternal::Legacy(LegacyDrmDevice::new(fd, active, disable_connectors)?) + }, + ) + } + + /// Returns if the underlying implementation uses atomic-modesetting or not. + pub fn is_atomic(&self) -> bool { + match *self.internal { + DrmDeviceInternal::Atomic(_) => true, + DrmDeviceInternal::Legacy(_) => false, + } + } + + /// Returns a list of crtcs for this device + pub fn crtcs(&self) -> &[crtc::Handle] { + self.resources.crtcs() + } + + /// Returns a set of available planes for a given crtc + pub fn planes(&self, crtc: &crtc::Handle) -> Result { + planes(self, crtc, self.has_universal_planes) + } + + /// Claim a plane so that it won't be used by a different crtc + /// + /// Returns `None` if the plane could not be claimed + pub fn claim_plane(&self, plane: plane::Handle, crtc: crtc::Handle) -> Option { + self.plane_claim_storage.claim(plane, crtc) + } + + /// Returns the size of the hardware cursor + /// + /// Note: In case of universal planes this is the + /// maximum size of a buffer that can be used on + /// the cursor plane. + pub fn cursor_size(&self) -> Size { + self.cursor_size + } + + /// Creates a new rendering surface. + /// + /// # Arguments + /// + /// Initialization of surfaces happens through the types provided by + /// [`drm-rs`](drm). + /// + /// - [`crtcs`](drm::control::crtc) represent scanout engines of the device pointing to one framebuffer. \ + /// Their responsibility is to read the data of the framebuffer and export it into an "Encoder". \ + /// The number of crtc's represent the number of independent output devices the hardware may handle. + /// - [`mode`](drm::control::Mode) describes the resolution and rate of images produced by the crtc and \ + /// has to be compatible with the provided `connectors`. + /// - [`connectors`](drm::control::connector) - List of connectors driven by the crtc. At least one(!) connector needs to be \ + /// attached to a crtc in smithay. + #[instrument(skip(self), parent = self.internal.span(), err)] + pub fn create_surface( + &mut self, + crtc: crtc::Handle, + mode: Mode, + connectors: &[connector::Handle], + ) -> Result { + self.surfaces.retain(|surface| surface.strong_count() != 0); + + if connectors.is_empty() { + return Err(Error::SurfaceWithoutConnectors(crtc)); + } + + if !self.is_active() { + return Err(Error::DeviceInactive); + } + + let planes = self.planes(&crtc)?; + + let selected_primary_plane = planes.primary.iter().find_map(|plane| { + let claim = match self.plane_claim_storage.claim(plane.handle, crtc) { + Some(claim) => claim, + None => { + tracing::debug!(?crtc, ?plane, "skipping already claimed primary plane"); + return None; + } + }; + let info = match self.get_plane(plane.handle) { + Ok(info) => info, + Err(err) => { + tracing::warn!(?crtc, ?err, ?plane, "failed to get primary plane info"); + return None; + } + }; + + let filter = info.possible_crtcs(); + if !self.resources.filter_crtcs(filter).contains(&crtc) { + tracing::warn!(?crtc, ?plane, "primary plane not compatible with crtc"); + return None; + } + + Some((plane.clone(), claim)) + }); + + let Some((plane, claim)) = selected_primary_plane else { + return Err(Error::NoPlane); + }; + + let active = match &*self.internal { + DrmDeviceInternal::Atomic(dev) => dev.active.clone(), + DrmDeviceInternal::Legacy(dev) => dev.active.clone(), + }; + + let internal = if self.is_atomic() { + let mapping = match &*self.internal { + DrmDeviceInternal::Atomic(dev) => dev.prop_mapping.clone(), + _ => unreachable!(), + }; + + DrmSurfaceInternal::Atomic(AtomicDrmSurface::new( + self.internal.clone(), + active, + crtc, + plane.handle, + mapping, + mode, + connectors, + )?) + } else { + DrmSurfaceInternal::Legacy(LegacyDrmSurface::new( + self.internal.clone(), + active, + crtc, + mode, + connectors, + )?) + }; + let internal = Arc::new(internal); + self.surfaces.push(Arc::downgrade(&internal)); + + Ok(DrmSurface { + dev_id: self.dev_id, + crtc, + planes, + internal, + plane_claim_storage: self.plane_claim_storage.clone(), + primary_plane: (plane, claim), + }) + } + + /// Returns the device_id of the underlying drm node + pub fn device_id(&self) -> dev_t { + self.dev_id + } + + /// Returns the underlying file descriptor + pub fn device_fd(&self) -> &DrmDeviceFd { + self.internal.device_fd() + } + + /// Pauses the device. + /// + /// This will cause the `DrmDevice` to avoid making calls to the file descriptor e.g. on drop. + /// Note that calls directly utilizing the underlying file descriptor, like the traits of the `drm-rs` crate, + /// will ignore this state. Use [`DrmDevice::is_active`] to guard these calls. + pub fn pause(&mut self) { + self.set_active(false); + self.surfaces.retain(|surface| surface.strong_count() != 0); + if self.device_fd().is_privileged() { + if let Err(err) = self.release_master_lock() { + error!("Failed to drop drm master state Error: {}", err); + } + } + } + + /// Activates a previously paused device. + /// + /// Specifying `true` for `disable_connectors` will call [`DrmDevice::reset_state`] if + /// the device was not active before. Otherwise you need to make sure there are no + /// conflicting requirements when enabling or creating surfaces or you are prepared + /// to handle errors caused by those. + pub fn activate(&mut self, disable_connectors: bool) -> Result<(), Error> { + if self.device_fd().is_privileged() { + if let Err(err) = self.acquire_master_lock() { + error!("Failed to acquire drm master on activate. Error: {}", err); + return Err(Error::DrmMasterFailed); + } + } + if !self.set_active(true) && disable_connectors { + self.reset_state() + } else { + self.surfaces.retain(|surface| surface.strong_count() != 0); + Ok(()) + } + } + + /// Returns if the device is currently paused or not. + pub fn is_active(&self) -> bool { + match &*self.internal { + DrmDeviceInternal::Atomic(internal) => internal.active.load(Ordering::SeqCst), + DrmDeviceInternal::Legacy(internal) => internal.active.load(Ordering::SeqCst), + } + } + + /// Reset the state of this device + /// + /// This will disable all connectors and reset all planes. + /// Additional this will also reset the state on all known surfaces. + pub fn reset_state(&mut self) -> Result<(), Error> { + if !self.is_active() { + return Err(Error::DeviceInactive); + } + + match &*self.internal { + DrmDeviceInternal::Atomic(internal) => internal.reset_state(), + DrmDeviceInternal::Legacy(internal) => internal.reset_state(), + }?; + + let mut i = 0; + while i != self.surfaces.len() { + if let Some(surface) = self.surfaces[i].upgrade() { + match &*surface { + DrmSurfaceInternal::Atomic(surf) => surf.reset_state::(None), + DrmSurfaceInternal::Legacy(surf) => surf.reset_state::(None), + }?; + i += 1; + } else { + self.surfaces.remove(i); + } + } + Ok(()) + } + + fn set_active(&self, active: bool) -> bool { + match &*self.internal { + DrmDeviceInternal::Atomic(internal) => internal.active.swap(active, Ordering::SeqCst), + DrmDeviceInternal::Legacy(internal) => internal.active.swap(active, Ordering::SeqCst), + } + } +} + +/// Events that can be generated by a DrmDevice +#[derive(Debug)] +pub enum DrmEvent { + /// A vblank blank event on the provided crtc has happened + VBlank(crtc::Handle), + /// An error happened while processing events + Error(Error), +} + +/// Timing metadata for page-flip events +#[derive(Debug, Clone, Copy)] +pub struct EventMetadata { + /// The time the frame flip happened + pub time: Time, + /// The sequence number of the frame + pub sequence: u32, +} + +/// Either a realtime or monotonic timestamp +#[derive(Debug, Clone, Copy)] +pub enum Time { + /// Monotonic time stamp + Monotonic(Duration), + /// Realtime time stamp + Realtime(SystemTime), +} + +/// Even source of [`DrmDevice`] +#[derive(Debug)] +pub struct DrmDeviceNotifier { + internal: Arc, + has_monotonic_timestamps: bool, + token: Option, +} + +impl EventSource for DrmDeviceNotifier { + type Event = DrmEvent; + type Metadata = Option; + type Ret = (); + type Error = io::Error; + + fn process_events(&mut self, _: Readiness, token: Token, mut callback: F) -> io::Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let _guard = self.internal.span().enter(); + + if Some(token) != self.token { + return Ok(PostAction::Continue); + } + + match self.internal.receive_events() { + Ok(events) => { + for event in events { + if let Event::PageFlip(event) = event { + trace!("Got a page-flip event for crtc ({:?})", event.crtc); + let metadata = EventMetadata { + time: if self.has_monotonic_timestamps { + Time::Monotonic(event.duration) + } else { + Time::Realtime(SystemTime::UNIX_EPOCH + event.duration) + }, + sequence: event.frame, + }; + callback(DrmEvent::VBlank(event.crtc), &mut Some(metadata)); + } else { + trace!( + "Got a non-page-flip event of device '{:?}'.", + self.internal.dev_path() + ); + } + } + } + Err(source) => { + callback( + DrmEvent::Error(Error::Access(AccessError { + errmsg: "Error processing drm events", + dev: self.internal.dev_path(), + source, + })), + &mut None, + ); + } + } + Ok(PostAction::Continue) + } + + fn register(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.token = Some(factory.token()); + // Safety: the FD cannot be closed without removing the DrmDeviceNotifier from the event loop + unsafe { + poll.register( + self.internal.as_fd(), + Interest::READ, + calloop::Mode::Level, + self.token.unwrap(), + ) + } + } + + fn reregister(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.token = Some(factory.token()); + poll.reregister( + self.internal.as_fd(), + Interest::READ, + calloop::Mode::Level, + self.token.unwrap(), + ) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.token = None; + poll.unregister(self.internal.as_fd()) + } +} diff --git a/vendor/smithay/src/backend/drm/dumb.rs b/vendor/smithay/src/backend/drm/dumb.rs new file mode 100644 index 0000000..c867bb3 --- /dev/null +++ b/vendor/smithay/src/backend/drm/dumb.rs @@ -0,0 +1,203 @@ +//! Utilities to attach [`framebuffer::Handle`]s to dumb buffers + +use drm_fourcc::{DrmFormat, DrmModifier}; + +use drm::{ + buffer::{Buffer as DrmBuffer, PlanarBuffer}, + control::{Device, FbCmd2Flags, framebuffer}, +}; +use tracing::{trace, warn}; + +use crate::utils::DevPath; +use crate::{ + backend::{ + allocator::{ + Buffer, Fourcc, + dumb::DumbBuffer, + format::{get_bpp, get_depth, get_opaque}, + }, + drm::DrmDeviceFd, + }, + utils::{Buffer as BufferCoords, Size}, +}; + +use super::{Framebuffer, error::AccessError, warn_legacy_fb_export}; + +/// A GBM backed framebuffer +#[derive(Debug)] +pub struct DumbFramebuffer { + fb: framebuffer::Handle, + format: drm_fourcc::DrmFormat, + drm: DrmDeviceFd, +} + +impl Drop for DumbFramebuffer { + fn drop(&mut self) { + trace!(fb = ?self.fb, "destroying framebuffer"); + if let Err(err) = self.drm.destroy_framebuffer(self.fb) { + warn!(fb = ?self.fb, ?err, "failed to destroy framebuffer"); + } + } +} + +impl AsRef for DumbFramebuffer { + #[inline] + fn as_ref(&self) -> &framebuffer::Handle { + &self.fb + } +} + +impl Framebuffer for DumbFramebuffer { + #[inline] + fn format(&self) -> drm_fourcc::DrmFormat { + self.format + } +} + +struct PlanarDumbBuffer<'a>(&'a DumbBuffer); + +impl Buffer for PlanarDumbBuffer<'_> { + #[inline] + fn size(&self) -> Size { + self.0.size() + } + + #[inline] + fn format(&self) -> DrmFormat { + self.0.format() + } +} + +impl PlanarBuffer for PlanarDumbBuffer<'_> { + #[inline] + fn size(&self) -> (u32, u32) { + let size = self.0.size(); + (size.w as u32, size.h as u32) + } + + #[inline] + fn format(&self) -> Fourcc { + self.0.format().code + } + + #[inline] + fn modifier(&self) -> Option { + Some(self.0.format().modifier) + } + + #[inline] + fn pitches(&self) -> [u32; 4] { + [self.0.handle().pitch(), 0, 0, 0] + } + + #[inline] + fn handles(&self) -> [Option; 4] { + [Some(self.0.handle().handle()), None, None, None] + } + + #[inline] + fn offsets(&self) -> [u32; 4] { + [0, 0, 0, 0] + } +} + +struct OpaqueBufferWrapper<'a, B>(&'a B); +impl PlanarBuffer for OpaqueBufferWrapper<'_, B> +where + B: PlanarBuffer, +{ + #[inline] + fn size(&self) -> (u32, u32) { + self.0.size() + } + + #[inline] + fn format(&self) -> Fourcc { + let fmt = self.0.format(); + get_opaque(fmt).unwrap_or(fmt) + } + + #[inline] + fn modifier(&self) -> Option { + self.0.modifier() + } + + #[inline] + fn pitches(&self) -> [u32; 4] { + self.0.pitches() + } + + #[inline] + fn handles(&self) -> [Option; 4] { + self.0.handles() + } + + #[inline] + fn offsets(&self) -> [u32; 4] { + self.0.offsets() + } +} + +/// Attach a [`framebuffer::Handle`] to an [`DumbBuffer`] +#[profiling::function] +pub fn framebuffer_from_dumb_buffer( + drm: &DrmDeviceFd, + buffer: &DumbBuffer, + use_opaque: bool, +) -> Result { + let buffer = PlanarDumbBuffer(buffer); + + let format = Buffer::format(&buffer); + let modifier = buffer.modifier(); + let flags = if modifier.is_some() { + FbCmd2Flags::MODIFIERS + } else { + FbCmd2Flags::empty() + }; + + let ret = if use_opaque { + let opaque_wrapper = OpaqueBufferWrapper(&buffer); + drm.add_planar_framebuffer(&opaque_wrapper, flags).map(|fb| { + ( + fb, + drm_fourcc::DrmFormat { + code: opaque_wrapper.format(), + modifier: modifier.unwrap_or(DrmModifier::Invalid), + }, + ) + }) + } else { + drm.add_planar_framebuffer(&buffer, flags).map(|fb| (fb, format)) + }; + + let (fb, format) = match ret { + Ok(fb) => fb, + Err(source) => { + warn_legacy_fb_export(); + + let fourcc = format.code; + let (depth, bpp) = get_depth(fourcc) + .zip(get_bpp(fourcc)) + .ok_or_else(|| AccessError { + errmsg: "Unknown format for legacy framebuffer", + dev: drm.dev_path(), + source, + })?; + + let fb = drm + .add_framebuffer(buffer.0.handle(), depth as u32, bpp as u32) + .map_err(|source| AccessError { + errmsg: "Failed to add framebuffer", + dev: drm.dev_path(), + source, + })?; + (fb, format) + } + }; + + Ok(DumbFramebuffer { + fb, + format, + drm: drm.clone(), + }) +} diff --git a/vendor/smithay/src/backend/drm/error.rs b/vendor/smithay/src/backend/drm/error.rs new file mode 100644 index 0000000..5002671 --- /dev/null +++ b/vendor/smithay/src/backend/drm/error.rs @@ -0,0 +1,116 @@ +use crate::backend::SwapBuffersError; +use drm::control::{Mode, RawResourceHandle, connector, crtc, plane}; +use std::{ + io::{self, ErrorKind}, + path::PathBuf, +}; + +/// DRM access error +#[derive(Debug, thiserror::Error)] +#[error("DRM access error: {errmsg} on device `{dev:?}` ({source:})")] +pub struct AccessError { + /// Error message associated to the access error + pub(crate) errmsg: &'static str, + /// Device on which the error was generated + pub(crate) dev: Option, + /// Underlying device error + #[source] + pub source: io::Error, +} + +/// Errors thrown by the [`DrmDevice`](crate::backend::drm::DrmDevice) +/// and the [`DrmSurface`](crate::backend::drm::DrmSurface). +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Unable to acquire DRM master + #[error("Failed to acquire DRM master")] + DrmMasterFailed, + /// The `DrmDevice` encountered an access error + #[error(transparent)] + Access(#[from] AccessError), + /// Unable to determine device id of drm device + #[error("Unable to determine device id of drm device")] + UnableToGetDeviceId(#[source] rustix::io::Errno), + /// Device is currently paused + #[error("Device is currently paused, operation rejected")] + DeviceInactive, + /// Mode is not compatible with all given connectors + #[error("Mode `{0:?}` is not compatible with all given connectors")] + ModeNotSuitable(Mode), + /// The given crtc is already in use by another surface + #[error("Crtc `{0:?}` is already in use by another surface")] + CrtcAlreadyInUse(crtc::Handle), + /// This operation would result in a surface without connectors. + #[error("Surface of crtc `{0:?}` would have no connectors, which is not accepted")] + SurfaceWithoutConnectors(crtc::Handle), + /// The given plane cannot be used with the given crtc + #[error("Plane `{1:?}` is not compatible for use with crtc `{0:?}`")] + PlaneNotCompatible(crtc::Handle, plane::Handle), + /// The given configuration does not specify a plane which is not supported by the underlying implementation + #[error("No Plane has been specified which is not supported by the underlying implementation")] + NoPlane, + /// The given plane is not a primary plane and therefor not supported by the underlying implementation + #[error("Non-Primary Planes (provided was `{0:?}`) are not available for use with legacy devices")] + NonPrimaryPlane(plane::Handle), + /// The given plane does not allow to clear the framebuffer + #[error("Clearing the framebuffer on plane `{0:?}` is not supported")] + NoFramebuffer(plane::Handle), + /// The configuration is not supported on the given plane + #[error("The configuration is not supported on plane `{0:?}`")] + UnsupportedPlaneConfiguration(plane::Handle), + /// No encoder was found for a given connector on the set crtc + #[error("No encoder found for the given connector '{connector:?}' on crtc `{crtc:?}`")] + NoSuitableEncoder { + /// Connector + connector: connector::Handle, + /// CRTC + crtc: crtc::Handle, + }, + /// Unknown connector handle + #[error("The connector ({0:?}) is unknown")] + UnknownConnector(connector::Handle), + /// Unknown crtc handle + #[error("The crtc ({0:?}) is unknown")] + UnknownCrtc(crtc::Handle), + /// Unknown crtc handle + #[error("The plane ({0:?}) is unknown")] + UnknownPlane(plane::Handle), + /// The DrmDevice is missing a required property + #[error("The DrmDevice is missing a required property '{name}' for handle ({handle:?})")] + UnknownProperty { + /// Property handle + handle: RawResourceHandle, + /// Property name + name: &'static str, + }, + /// Atomic Test failed for new properties + #[error("Atomic Test failed for new properties on crtc ({0:?})")] + TestFailed(crtc::Handle), +} + +impl From for SwapBuffersError { + #[inline] + fn from(err: Error) -> SwapBuffersError { + // FIXME: replace the special handling for EBUSY with ErrorKind::ResourceBusy once + // we reach MSRV >= 1.83 + match err { + x @ Error::DeviceInactive | x @ Error::DrmMasterFailed => { + SwapBuffersError::TemporaryFailure(Box::new(x)) + } + Error::Access(AccessError { + errmsg, dev, source, .. + }) if matches!( + source.kind(), + ErrorKind::PermissionDenied | ErrorKind::WouldBlock | ErrorKind::Interrupted + ) || rustix::io::Errno::from_io_error(&source) == Some(rustix::io::Errno::BUSY) => + { + SwapBuffersError::TemporaryFailure(Box::new(Error::Access(AccessError { + errmsg, + dev, + source, + }))) + } + x => SwapBuffersError::ContextLost(Box::new(x)), + } + } +} diff --git a/vendor/smithay/src/backend/drm/exporter/dumb.rs b/vendor/smithay/src/backend/drm/exporter/dumb.rs new file mode 100644 index 0000000..c69f2a0 --- /dev/null +++ b/vendor/smithay/src/backend/drm/exporter/dumb.rs @@ -0,0 +1,54 @@ +//! Implementation for ExportFramebuffer and related utilities for dumb buffers + +use thiserror::Error; + +use super::{ExportBuffer, ExportFramebuffer}; +use crate::backend::{ + allocator::dumb::DumbBuffer, + drm::{ + DrmDeviceFd, + dumb::{DumbFramebuffer, framebuffer_from_dumb_buffer}, + error::AccessError, + }, +}; + +/// Possible errors for attaching a [`framebuffer::Handle`](::drm::control::framebuffer::Handle) +#[derive(Error, Debug)] +pub enum Error { + /// The buffer is not supported + #[error("Unsupported buffer supplied")] + Unsupported, + /// Failed to add a framebuffer for the dumb buffer + #[error("failed to add a framebuffer for the dumb buffer")] + Drm(AccessError), +} + +impl ExportFramebuffer for DrmDeviceFd { + type Framebuffer = DumbFramebuffer; + type Error = Error; + + #[profiling::function] + fn add_framebuffer( + &self, + _drm: &DrmDeviceFd, + buffer: ExportBuffer<'_, DumbBuffer>, + use_opaque: bool, + ) -> Result, Self::Error> { + match buffer { + #[cfg(feature = "wayland_frontend")] + ExportBuffer::Wayland(_) => return Err(Error::Unsupported), + ExportBuffer::Allocator(buffer) => framebuffer_from_dumb_buffer(self, buffer, use_opaque) + .map_err(Error::Drm) + .map(Some), + } + } + + #[inline] + fn can_add_framebuffer(&self, buffer: &ExportBuffer<'_, DumbBuffer>) -> bool { + match buffer { + #[cfg(feature = "wayland_frontend")] + ExportBuffer::Wayland(_) => false, + ExportBuffer::Allocator(_) => true, + } + } +} diff --git a/vendor/smithay/src/backend/drm/exporter/gbm.rs b/vendor/smithay/src/backend/drm/exporter/gbm.rs new file mode 100644 index 0000000..65be795 --- /dev/null +++ b/vendor/smithay/src/backend/drm/exporter/gbm.rs @@ -0,0 +1,167 @@ +//! Implementation for ExportFramebuffer and related utilizes for gbm types + +use std::os::unix::io::AsFd; + +use drm::node::DrmNode; + +use super::{ExportBuffer, ExportFramebuffer}; +#[cfg(feature = "wayland_frontend")] +use crate::backend::drm::gbm::framebuffer_from_wayland_buffer; +use crate::backend::{ + allocator::{ + dmabuf::AsDmabuf, + gbm::{GbmBuffer, GbmConvertError}, + }, + drm::{ + DrmDeviceFd, + gbm::{Error as GbmError, GbmFramebuffer, framebuffer_from_bo, framebuffer_from_dmabuf}, + }, +}; + +/// Error for [`GbmFramebufferExporter`] +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Exporting the [`GbmBuffer`] as a [`Dmabuf`](crate::backend::allocator::dmabuf::Dmabuf) failed + #[error(transparent)] + Dmabuf(#[from] GbmConvertError), + /// Exporting the [`GbmBuffer`] failed + #[error(transparent)] + Gbm(#[from] GbmError), +} + +/// Export framebuffers based on [`gbm::Device`] +#[derive(Debug, Clone)] +pub struct GbmFramebufferExporter { + gbm: gbm::Device, + drm_node: Option, + #[cfg_attr(not(feature = "wayland_frontend"), allow(unused))] + import_node: NodeFilter, +} + +impl GbmFramebufferExporter { + /// Initialize a new framebuffer exporter. + /// + /// `import_node` will be used to filter dmabufs to originate from a particular + /// device before considering them for direct scanout. + /// + /// If `import_node` is [`NodeFilter::None`], direct-scanout of client-buffers + /// won't be used. + pub fn new(gbm: gbm::Device, import_node: NodeFilter) -> Self { + let drm_node = DrmNode::from_file(gbm.as_fd()).ok(); + Self { + gbm, + drm_node, + import_node, + } + } +} + +impl ExportFramebuffer for GbmFramebufferExporter { + type Framebuffer = GbmFramebuffer; + type Error = Error; + + #[profiling::function] + fn add_framebuffer( + &self, + drm: &DrmDeviceFd, + buffer: ExportBuffer<'_, GbmBuffer>, + use_opaque: bool, + ) -> Result, Self::Error> { + let framebuffer = match buffer { + #[cfg(feature = "wayland_frontend")] + ExportBuffer::Wayland(wl_buffer) => { + framebuffer_from_wayland_buffer(drm, &self.gbm, wl_buffer, use_opaque)? + } + ExportBuffer::Allocator(buffer) => { + let foreign = self.drm_node.is_none() + || buffer.device_node().is_none() + || self.drm_node != buffer.device_node(); + if foreign { + tracing::debug!("importing foreign buffer"); + let dmabuf = buffer.export()?; + framebuffer_from_dmabuf(drm, &self.gbm, &dmabuf, use_opaque, true).map(Some)? + } else { + framebuffer_from_bo(drm, buffer, use_opaque) + .map_err(GbmError::Drm) + .map(Some)? + } + } + }; + Ok(framebuffer) + } + + #[inline] + #[cfg(feature = "wayland_frontend")] + fn can_add_framebuffer(&self, buffer: &ExportBuffer<'_, GbmBuffer>) -> bool { + match buffer { + #[cfg(not(all(feature = "backend_egl", feature = "use_system_lib")))] + ExportBuffer::Wayland(buffer) => { + let node = crate::wayland::dmabuf::get_dmabuf(buffer) + .ok() + .and_then(|buf| buf.node()); + self.import_node == node + } + #[cfg(all(feature = "backend_egl", feature = "use_system_lib"))] + ExportBuffer::Wayland(buffer) => match crate::backend::renderer::buffer_type(buffer) { + Some(crate::backend::renderer::BufferType::Dma) => { + let node = crate::wayland::dmabuf::get_dmabuf(buffer).unwrap().node(); + self.import_node == node + } + // Arguably we need specialization here. If the renderer (which we have in `element_config`, which calls this function) + // has `ImportEGL`, we can verify that `EGLBufferRender` is some, which means we have the renderer advertised via wl_drm, + // which means this is probably fine. + // If we don't have `ImportEGL` or `EGLBufferReader` is none, we should reject this, but we don't want to require `ImportEGL`. + // + // So for now hope that `gbm_framebuffer_from_wayland_buffer` is smart enough to deal with this correctly. + // (And most modern compositor don't use wl_drm anyway.) + Some(crate::backend::renderer::BufferType::Egl) => true, + _ => false, + }, + ExportBuffer::Allocator(_) => true, + } + } + + #[inline] + #[cfg(not(feature = "wayland_frontend"))] + fn can_add_framebuffer(&self, buffer: &ExportBuffer<'_, GbmBuffer>) -> bool { + match buffer { + ExportBuffer::Allocator(_) => true, + } + } +} + +/// Filter to matching nodes against. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum NodeFilter { + /// Consider no nodes a match. + None, + /// Consider all nodes a match. + All, + /// Consider only the specified node a match. + Node(DrmNode), +} + +impl PartialEq> for NodeFilter { + fn eq(&self, other: &Option) -> bool { + match self { + Self::None => false, + Self::All => true, + Self::Node(node) => other.is_some_and(|n| &n == node), + } + } +} + +impl From for NodeFilter { + fn from(node: DrmNode) -> Self { + Self::Node(node) + } +} + +impl From> for NodeFilter { + fn from(node: Option) -> Self { + match node { + Some(node) => Self::Node(node), + None => Self::None, + } + } +} diff --git a/vendor/smithay/src/backend/drm/exporter/mod.rs b/vendor/smithay/src/backend/drm/exporter/mod.rs new file mode 100644 index 0000000..cca2458 --- /dev/null +++ b/vendor/smithay/src/backend/drm/exporter/mod.rs @@ -0,0 +1,114 @@ +//! Trait and data structures to describe types, that can be exported to a drm framebuffer + +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, Mutex}, +}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_buffer::WlBuffer; + +use crate::backend::{allocator::Buffer, renderer::element::UnderlyingStorage}; + +use super::{DrmDeviceFd, Framebuffer}; + +#[cfg(feature = "backend_drm")] +pub mod dumb; +#[cfg(feature = "backend_gbm")] +pub mod gbm; + +/// Possible buffers to export as a framebuffer using [`ExportFramebuffer`] +#[derive(Debug)] +pub enum ExportBuffer<'a, B: Buffer> { + /// A wayland buffer + #[cfg(feature = "wayland_frontend")] + Wayland(&'a WlBuffer), + /// A [`Allocator`][crate::backend::allocator::Allocator] buffer + Allocator(&'a B), +} + +impl<'a, B: Buffer> ExportBuffer<'a, B> { + /// Create the export buffer from an [`UnderlyingStorage`] + #[inline] + pub fn from_underlying_storage(storage: &'a UnderlyingStorage<'_>) -> Option { + match storage { + #[cfg(feature = "wayland_frontend")] + UnderlyingStorage::Wayland(buffer) => Some(Self::Wayland(buffer)), + UnderlyingStorage::Memory { .. } => None, + } + } +} + +/// Export a [`ExportBuffer`] as a framebuffer +pub trait ExportFramebuffer +where + B: Buffer, +{ + /// Type of the framebuffer + type Framebuffer: Framebuffer; + + /// Type of the error + type Error: std::error::Error; + + /// Add a framebuffer for the specified buffer + fn add_framebuffer( + &self, + drm: &DrmDeviceFd, + buffer: ExportBuffer<'_, B>, + use_opaque: bool, + ) -> Result, Self::Error>; + + /// Test if the provided buffer is eligible for adding a framebuffer + fn can_add_framebuffer(&self, buffer: &ExportBuffer<'_, B>) -> bool; +} + +impl ExportFramebuffer for Arc> +where + F: ExportFramebuffer, + B: Buffer, +{ + type Framebuffer = >::Framebuffer; + type Error = >::Error; + + #[inline] + fn add_framebuffer( + &self, + drm: &DrmDeviceFd, + buffer: ExportBuffer<'_, B>, + use_opaque: bool, + ) -> Result, Self::Error> { + let guard = self.lock().unwrap(); + guard.add_framebuffer(drm, buffer, use_opaque) + } + + #[inline] + fn can_add_framebuffer(&self, buffer: &ExportBuffer<'_, B>) -> bool { + let guard = self.lock().unwrap(); + guard.can_add_framebuffer(buffer) + } +} + +impl ExportFramebuffer for Rc> +where + F: ExportFramebuffer, + B: Buffer, +{ + type Framebuffer = >::Framebuffer; + type Error = >::Error; + + #[inline] + fn add_framebuffer( + &self, + drm: &DrmDeviceFd, + buffer: ExportBuffer<'_, B>, + use_opaque: bool, + ) -> Result, Self::Error> { + self.borrow().add_framebuffer(drm, buffer, use_opaque) + } + + #[inline] + fn can_add_framebuffer(&self, buffer: &ExportBuffer<'_, B>) -> bool { + self.borrow().can_add_framebuffer(buffer) + } +} diff --git a/vendor/smithay/src/backend/drm/gbm.rs b/vendor/smithay/src/backend/drm/gbm.rs new file mode 100644 index 0000000..ef4d8c7 --- /dev/null +++ b/vendor/smithay/src/backend/drm/gbm.rs @@ -0,0 +1,381 @@ +//! Utilities to attach [`framebuffer::Handle`]s to gbm backed buffers + +use std::os::unix::io::AsFd; + +use thiserror::Error; + +use drm::{ + buffer::PlanarBuffer, + control::{Device, FbCmd2Flags, framebuffer}, +}; +use drm_fourcc::DrmModifier; +use tracing::{trace, warn}; +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_buffer::WlBuffer; + +#[cfg(feature = "wayland_frontend")] +use crate::backend::allocator::Buffer; +use crate::backend::{ + allocator::{ + Fourcc, + dmabuf::Dmabuf, + format::{get_bpp, get_depth, get_opaque}, + gbm::GbmBuffer, + }, + drm::DrmDeviceFd, +}; +use crate::utils::DevPath; + +use super::{Framebuffer, error::AccessError, warn_legacy_fb_export}; + +/// A GBM backed framebuffer +#[derive(Debug)] +pub struct GbmFramebuffer { + fb: framebuffer::Handle, + format: drm_fourcc::DrmFormat, + drm: DrmDeviceFd, +} + +impl Drop for GbmFramebuffer { + #[inline] + fn drop(&mut self) { + trace!(fb = ?self.fb, "destroying framebuffer"); + if let Err(err) = self.drm.destroy_framebuffer(self.fb) { + warn!(fb = ?self.fb, ?err, "failed to destroy framebuffer"); + } + } +} + +impl AsRef for GbmFramebuffer { + #[inline] + fn as_ref(&self) -> &framebuffer::Handle { + &self.fb + } +} + +impl Framebuffer for GbmFramebuffer { + #[inline] + fn format(&self) -> drm_fourcc::DrmFormat { + self.format + } +} + +/// Attach a framebuffer for a [`WlBuffer`] +/// +/// This tries to import the buffer to gbm and attach a [`framebuffer::Handle`] for +/// the imported [`BufferObject`][gbm::BufferObject]. +/// +/// Returns `Ok(None)` for unknown buffer types and buffer types that do not +/// support attaching a framebuffer (e.g. shm-buffers) +#[cfg(feature = "wayland_frontend")] +#[profiling::function] +pub fn framebuffer_from_wayland_buffer( + drm: &DrmDeviceFd, + gbm: &gbm::Device, + buffer: &WlBuffer, + use_opaque: bool, +) -> Result, Error> { + if let Ok(dmabuf) = crate::wayland::dmabuf::get_dmabuf(buffer) { + // From weston: + /* We should not import to KMS a buffer that has been allocated using no + * modifiers. Usually drivers use linear layouts to allocate with no + * modifiers, but this is not a rule. The driver could use, for + * instance, a tiling layout under the hood - and both Weston and the + * KMS driver can't know. So giving the buffer to KMS is not safe, as + * not knowing its layout can result in garbage being displayed. In + * short, importing a buffer to KMS requires explicit modifiers. */ + if dmabuf.format().modifier != DrmModifier::Invalid { + return Ok(Some(framebuffer_from_dmabuf( + drm, gbm, dmabuf, use_opaque, false, + )?)); + } + } + + #[cfg(all(feature = "backend_egl", feature = "use_system_lib"))] + if matches!( + crate::backend::renderer::buffer_type(buffer), + Some(crate::backend::renderer::BufferType::Egl) + ) { + let bo = gbm + .import_buffer_object_from_wayland::<()>(buffer, gbm::BufferObjectFlags::SCANOUT) + .map(|bo| GbmBuffer::from_bo(bo, true)) + .map_err(Error::Import)?; + let (fb, format) = framebuffer_from_bo_internal( + drm, + BufferObjectInternal { + bo: &bo, + offsets: None, + pitches: None, + }, + use_opaque, + true, + ) + .map_err(Error::Drm)?; + + return Ok(Some(GbmFramebuffer { + fb, + format, + drm: drm.clone(), + })); + } + + Ok(None) +} + +/// Possible errors for attaching a [`framebuffer::Handle`] +#[derive(Error, Debug)] +pub enum Error { + /// Importing the [`Dmabuf`] to gbm failed + #[error("failed to import the dmabuf to gbm")] + Import(std::io::Error), + /// Failed to add a framebuffer for the bo + #[error("failed to add a framebuffer for the bo")] + Drm(AccessError), +} + +/// Attach a framebuffer for a [`Dmabuf`] +/// +/// This tries to import the [`Dmabuf`] using gbm and attach +/// a [`framebuffer::Handle`] for the imported [`BufferObject`][gbm::BufferObject] +#[profiling::function] +pub fn framebuffer_from_dmabuf( + drm: &DrmDeviceFd, + gbm: &gbm::Device, + dmabuf: &Dmabuf, + use_opaque: bool, + allow_legacy: bool, +) -> Result { + let bo: GbmBuffer = dmabuf + .import_to(gbm, gbm::BufferObjectFlags::SCANOUT) + .map_err(Error::Import)?; + + // We override the offsets and pitches here cause the imported bo + // can return the wrong values. bo will only return the correct values + // for buffers we have allocated, but not for all client provided buffers. + let mut offsets: [u32; 4] = [0; 4]; + let mut pitches: [u32; 4] = [0; 4]; + + for (index, offset) in dmabuf.offsets().enumerate() { + offsets[index] = offset; + } + + for (index, stride) in dmabuf.strides().enumerate() { + pitches[index] = stride; + } + + framebuffer_from_bo_internal( + drm, + BufferObjectInternal { + bo: &bo, + offsets: Some(offsets), + pitches: Some(pitches), + }, + use_opaque, + allow_legacy, + ) + .map_err(Error::Drm) + .map(|(fb, format)| GbmFramebuffer { + fb, + format, + drm: drm.clone(), + }) +} + +/// Attach a [`framebuffer::Handle`] to an [`BufferObject`][gbm::BufferObject] +#[profiling::function] +pub fn framebuffer_from_bo( + drm: &DrmDeviceFd, + bo: &GbmBuffer, + use_opaque: bool, +) -> Result { + framebuffer_from_bo_internal( + drm, + BufferObjectInternal { + bo, + offsets: None, + pitches: None, + }, + use_opaque, + true, + ) + .map(|(fb, format)| GbmFramebuffer { + fb, + format, + drm: drm.clone(), + }) +} + +struct BufferObjectInternal<'a> { + bo: &'a GbmBuffer, + pitches: Option<[u32; 4]>, + offsets: Option<[u32; 4]>, +} + +impl std::ops::Deref for BufferObjectInternal<'_> { + type Target = GbmBuffer; + + #[inline] + fn deref(&self) -> &Self::Target { + self.bo + } +} + +impl PlanarBuffer for BufferObjectInternal<'_> { + #[inline] + fn size(&self) -> (u32, u32) { + PlanarBuffer::size(self.bo) + } + + #[inline] + fn format(&self) -> drm_fourcc::DrmFourcc { + PlanarBuffer::format(self.bo) + } + + #[inline] + fn modifier(&self) -> Option { + PlanarBuffer::modifier(self.bo) + } + + #[inline] + fn pitches(&self) -> [u32; 4] { + self.pitches.unwrap_or_else(|| PlanarBuffer::pitches(self.bo)) + } + + #[inline] + fn handles(&self) -> [Option; 4] { + PlanarBuffer::handles(self.bo) + } + + #[inline] + fn offsets(&self) -> [u32; 4] { + self.offsets.unwrap_or_else(|| PlanarBuffer::offsets(self.bo)) + } +} + +struct OpaqueBufferWrapper<'a, B>(&'a B); +impl PlanarBuffer for OpaqueBufferWrapper<'_, B> +where + B: PlanarBuffer, +{ + #[inline] + fn size(&self) -> (u32, u32) { + self.0.size() + } + + #[inline] + fn format(&self) -> Fourcc { + let fmt = self.0.format(); + get_opaque(fmt).unwrap_or(fmt) + } + + #[inline] + fn modifier(&self) -> Option { + self.0.modifier() + } + + #[inline] + fn pitches(&self) -> [u32; 4] { + self.0.pitches() + } + + #[inline] + fn handles(&self) -> [Option; 4] { + self.0.handles() + } + + #[inline] + fn offsets(&self) -> [u32; 4] { + self.0.offsets() + } +} + +#[profiling::function] +#[inline] +fn framebuffer_from_bo_internal( + drm: &D, + bo: BufferObjectInternal<'_>, + use_opaque: bool, + allow_legacy: bool, +) -> Result<(framebuffer::Handle, drm_fourcc::DrmFormat), AccessError> +where + D: drm::control::Device + DevPath, +{ + let modifier = bo.modifier(); + let flags = if bo.modifier().is_some() { + FbCmd2Flags::MODIFIERS + } else { + FbCmd2Flags::empty() + }; + + let ret = if use_opaque { + let opaque_wrapper = OpaqueBufferWrapper(&bo); + drm.add_planar_framebuffer(&opaque_wrapper, flags).map(|fb| { + ( + fb, + drm_fourcc::DrmFormat { + code: opaque_wrapper.format(), + modifier: modifier.unwrap_or(DrmModifier::Invalid), + }, + ) + }) + } else { + drm.add_planar_framebuffer(&bo, flags).map(|fb| { + ( + fb, + drm_fourcc::DrmFormat { + code: bo.format(), + modifier: modifier.unwrap_or(DrmModifier::Invalid), + }, + ) + }) + }; + + let (fb, format) = match ret { + Ok(fb) => fb, + Err(source) => { + if !allow_legacy { + return Err(AccessError { + errmsg: "Failed to add framebuffer", + dev: drm.dev_path(), + source, + }); + } + + // We only support this as a fallback of last resort like xf86-video-modesetting does. + warn_legacy_fb_export(); + + if bo.plane_count() > 1 { + return Err(AccessError { + errmsg: "Failed to add framebuffer", + dev: drm.dev_path(), + source, + }); + } + + let fourcc = bo.format(); + let (depth, bpp) = get_depth(fourcc) + .zip(get_bpp(fourcc)) + .ok_or_else(|| AccessError { + errmsg: "Unknown format for legacy framebuffer", + dev: drm.dev_path(), + source, + })?; + + let fb = drm + .add_framebuffer(&*bo, depth as u32, bpp as u32) + .map_err(|source| AccessError { + errmsg: "Failed to add framebuffer", + dev: drm.dev_path(), + source, + })?; + ( + fb, + drm_fourcc::DrmFormat { + code: fourcc, + modifier: drm_fourcc::DrmModifier::Invalid, + }, + ) + } + }; + Ok((fb, format)) +} diff --git a/vendor/smithay/src/backend/drm/mod.rs b/vendor/smithay/src/backend/drm/mod.rs new file mode 100644 index 0000000..b33a96a --- /dev/null +++ b/vendor/smithay/src/backend/drm/mod.rs @@ -0,0 +1,517 @@ +//! This module represents abstraction on top the linux direct rendering manager api (drm). +//! +//! ## [`DrmDevice`] +//! +//! A device exposes certain properties, which are directly derived +//! from the *device* as perceived by the direct rendering manager api (drm). These resources consists +//! out of: +//! - [`connectors`](drm::control::connector) represents a port on your computer, possibly with a connected monitor, TV, capture card, etc. +//! - [`encoder`](drm::control::encoder) encodes the data of connected crtcs into a video signal for a fixed set of connectors. +//! E.g. you might have an analog encoder based on a DAG for VGA ports, but another one for digital ones. +//! Also not every encoder might be connected to every crtc. +//! - [`framebuffer`] represents a buffer you may display, see `DrmSurface` below. +//! - [`plane`] adds another layer on top of the crtcs, which allow us to layer multiple images on top of each other more efficiently +//! then by combining the rendered images in the rendering phase, e.g. via OpenGL. Planes have to be explicitly used by the user to be useful. +//! Every device has at least one primary plane used to display an image to the whole crtc. Additionally cursor and overlay planes may be present. +//! Cursor planes are usually very restricted in size and meant to be used for hardware cursors, while overlay planes may +//! be used for performance reasons to display any overlay on top of the image, e.g. the top-most windows. +//! Overlay planes may have a bunch of weird limitation, that you cannot query, e.g. only working on round pixel coordinates. +//! You code should never rely on a fixed set of overlay planes, but always have a fallback solution in place. +//! - [`crtc`]s represent scanout engines of the device pointer to one framebuffer. +//! Their responsibility is to read the data of the framebuffer and export it into an "Encoder". +//! The number of crtc's represent the number of independent output devices the hardware may handle. +//! +//! On modern graphic cards it is better to think about the `crtc` as some sort of rendering engine. +//! You can only have so many different pictures, you may display, as you have `crtc`s, but a single image +//! may be put onto multiple displays. +//! +//! The main functionality of a `Device` in smithay is to give access to all these properties for the user to +//! choose an appropriate rendering configuration. What that means is defined by the requirements and constraints documented +//! in the specific device implementations. The second functionality is the creation of a `Surface`. +//! Surface creation requires a `crtc` (which cannot be the same as another existing `Surface`'s crtc), a plane, +//! as well as a `Mode` and a set of `connectors`. +//! +//! smithay does not make sure that `connectors` are not already in use by another `Surface`. Overlapping `connector`-Sets may +//! be an error or result in undefined rendering behavior depending on the `Surface` implementation. +//! +//! ## [`DrmSurface`] +//! +//! A surface is a part of a `Device` that may output a picture to a number of connectors. It pumps pictures of buffers to outputs. +//! +//! On surface creation a matching encoder for your `connector` is automatically selected, if one exists(!), +//! which means you still need to check your configuration beforehand, even if you do not need to provide an encoder. +//! +//! A surface consists of one `crtc` that is rendered to by the user. This is fixed for the `Surface`s lifetime and cannot be changed. +//! A surface also always needs at least one connector to output the resulting image to as well as a `Mode` that is valid for the given connectors. +//! +//! The state of a `Surface` is double-buffered, meaning all operations that chance the set of `connector`s or their `Mode` are stored and +//! only applied on the next commit. `Surface`s do their best to validate these changes, if possible. +//! +//! A commit/page_flip may be triggered to apply the pending state. +//! +//! ## Rendering +//! +//! The drm infrastructure makes no assumptions about the used renderer and does not interface with them directly. +//! It just provides a way to create framebuffers from various buffer types (mainly `DumbBuffer`s and hardware-backed gbm `BufferObject`s). +//! +//! Buffer management and details about the various types can be found in the [`allocator`-Module](crate::backend::allocator) and +//! rendering abstractions, which can target these buffers can be found in the [`renderer`-Module](crate::backend::renderer). +//! +//! ### Hardware composition +//! +//! The [`DrmCompositor`](crate::backend::drm::compositor::DrmCompositor) provides a simplified way to utilize drm planes for +//! using hardware composition. +//! See the [`compositor`] module docs for more information on that topic. +//! +//! ## [`DrmNode`] +//! +//! A drm node refers to a drm device and the capabilities that may be performed using the node. +//! Generally [`DrmNode`] is primarily used by clients (such as the output backends) which need +//! to allocate buffers for use in X11 or Wayland. If you need to do mode setting, you should use +//! [`DrmDevice`] instead. + +#[cfg(all(feature = "wayland_frontend", feature = "backend_gbm"))] +pub mod compositor; +pub(crate) mod device; +#[cfg(feature = "backend_drm")] +pub mod dumb; +mod error; +pub mod exporter; +#[cfg(feature = "backend_gbm")] +pub mod gbm; +#[cfg(all(feature = "wayland_frontend", feature = "backend_gbm"))] +pub mod output; + +mod surface; + +use std::sync::Once; + +use crate::utils::{DevPath, Physical, Size}; +pub use device::{ + DrmDevice, DrmDeviceFd, DrmDeviceNotifier, DrmEvent, EventMetadata as DrmEventMetadata, PlaneClaim, + Time as DrmEventTime, WeakDrmDeviceFd, +}; +pub use drm::node::{CreateDrmNodeError, DrmNode, NodeType}; +use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier}; +pub use error::AccessError as DrmAccessError; +pub use error::Error as DrmError; +use indexmap::IndexSet; +#[cfg(feature = "backend_gbm")] +pub use surface::gbm::{Error as GbmBufferedSurfaceError, GbmBufferedSurface}; +pub use surface::{DrmSurface, PlaneConfig, PlaneDamageClips, PlaneState, VrrSupport}; + +use drm::{ + DriverCapability, + control::{Device as ControlDevice, PlaneType, crtc, framebuffer, plane}, +}; +use tracing::trace; + +use self::error::AccessError; + +use super::allocator::format::FormatSet; + +fn warn_legacy_fb_export() { + static WARN_LEGACY_FB_EXPORT: Once = Once::new(); + WARN_LEGACY_FB_EXPORT.call_once(|| { + tracing::warn!("using legacy fbadd"); + }); +} + +/// Common framebuffer operations +pub trait Framebuffer: AsRef { + /// Retrieve the format of the framebuffer + fn format(&self) -> drm_fourcc::DrmFormat; +} + +/// A set of planes as supported by a crtc +#[derive(Debug, Clone)] +pub struct Planes { + /// The primary plane(s) of the crtc + pub primary: Vec, + /// The cursor plane(s) of the crtc, if available + pub cursor: Vec, + /// Overlay planes supported by the crtc, if available + pub overlay: Vec, +} + +/// Info about a single plane +#[derive(Debug, Clone)] +pub struct PlaneInfo { + /// Handle of the plane + pub handle: plane::Handle, + /// Type of the plane + pub type_: PlaneType, + /// z-position of the plane if available + pub zpos: Option, + /// Formats supported by this plane + pub formats: FormatSet, + /// Recommended plane size in order of preference + pub size_hints: Option>>, +} + +fn planes( + dev: &(impl DevPath + ControlDevice), + crtc: &crtc::Handle, + has_universal_planes: bool, +) -> Result { + let mut primary = Vec::with_capacity(1); + let mut cursor = Vec::new(); + let mut overlay = Vec::new(); + + let planes = dev.plane_handles().map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Error loading plane handles", + dev: dev.dev_path(), + source, + }) + })?; + + let resources = dev.resource_handles().map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Error loading resource handles", + dev: dev.dev_path(), + source, + }) + })?; + + for plane in planes { + let info = dev.get_plane(plane).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get plane information", + dev: dev.dev_path(), + source, + }) + })?; + let filter = info.possible_crtcs(); + if resources.filter_crtcs(filter).contains(crtc) { + let zpos = plane_zpos(dev, plane).ok().flatten(); + let type_ = plane_type(dev, plane)?; + let formats = plane_formats(dev, plane)?; + let size_hints = plane_size_hints(dev, plane)?; + let plane_info = PlaneInfo { + handle: plane, + type_, + zpos, + formats, + size_hints, + }; + match type_ { + PlaneType::Primary => { + primary.push(plane_info); + } + PlaneType::Cursor => { + cursor.push(plane_info); + } + PlaneType::Overlay => { + overlay.push(plane_info); + } + }; + } + } + + Ok(Planes { + primary, + cursor: if has_universal_planes { cursor } else { Vec::new() }, + overlay: if has_universal_planes { overlay } else { Vec::new() }, + }) +} + +fn plane_type(dev: &(impl ControlDevice + DevPath), plane: plane::Handle) -> Result { + let props = dev.get_properties(plane).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get properties of plane", + dev: dev.dev_path(), + source, + }) + })?; + let (ids, vals) = props.as_props_and_values(); + for (&id, &val) in ids.iter().zip(vals.iter()) { + let info = dev.get_property(id).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get property info", + dev: dev.dev_path(), + source, + }) + })?; + if info.name().to_str().map(|x| x == "type").unwrap_or(false) { + return Ok(match val { + x if x == (PlaneType::Primary as u64) => PlaneType::Primary, + x if x == (PlaneType::Cursor as u64) => PlaneType::Cursor, + _ => PlaneType::Overlay, + }); + } + } + unreachable!() +} + +fn plane_zpos(dev: &(impl ControlDevice + DevPath), plane: plane::Handle) -> Result, DrmError> { + let props = dev.get_properties(plane).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get properties of plane", + dev: dev.dev_path(), + source, + }) + })?; + let (ids, vals) = props.as_props_and_values(); + for (&id, &val) in ids.iter().zip(vals.iter()) { + let info = dev.get_property(id).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get property info", + dev: dev.dev_path(), + source, + }) + })?; + if info.name().to_str().map(|x| x == "zpos").unwrap_or(false) { + let plane_zpos = match info.value_type().convert_value(val) { + drm::control::property::Value::UnsignedRange(u) => Some(u as i32), + drm::control::property::Value::SignedRange(i) => Some(i as i32), + // A range from [0,1] will be interpreted as Boolean in drm-rs + // TODO: Once that has been changed we can remove this special handling here + drm::control::property::Value::Boolean(b) => Some(b.into()), + _ => None, + }; + return Ok(plane_zpos); + } + } + Ok(None) +} + +fn plane_formats(dev: &(impl ControlDevice + DevPath), plane: plane::Handle) -> Result { + // get plane formats + let plane_info = dev.get_plane(plane).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Error loading plane info", + dev: dev.dev_path(), + source, + }) + })?; + let mut formats = IndexSet::new(); + for code in plane_info + .formats() + .iter() + .flat_map(|x| DrmFourcc::try_from(*x).ok()) + { + formats.insert(DrmFormat { + code, + modifier: DrmModifier::Invalid, + }); + } + + if let Ok(1) = dev.get_driver_capability(DriverCapability::AddFB2Modifiers) { + let set = dev.get_properties(plane).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to query properties", + dev: dev.dev_path(), + source, + }) + })?; + let (handles, _) = set.as_props_and_values(); + // for every handle ... + let prop = handles + .iter() + .find(|handle| { + // get information of that property + if let Ok(info) = dev.get_property(**handle) { + // to find out, if we got the handle of the "IN_FORMATS" property ... + if info.name().to_str().map(|x| x == "IN_FORMATS").unwrap_or(false) { + // so we can use that to get formats + return true; + } + } + false + }) + .copied(); + if let Some(prop) = prop { + let prop_info = dev.get_property(prop).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to query property", + dev: dev.dev_path(), + source, + }) + })?; + let (handles, raw_values) = set.as_props_and_values(); + let raw_value = raw_values[handles + .iter() + .enumerate() + .find_map(|(i, handle)| if *handle == prop { Some(i) } else { None }) + .unwrap()]; + if let drm::control::property::Value::Blob(blob) = prop_info.value_type().convert_value(raw_value) + { + let data = dev.get_property_blob(blob).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to query property blob data", + dev: dev.dev_path(), + source, + }) + })?; + // be careful here, we have no idea about the alignment inside the blob, so always copy using `read_unaligned`, + // although slice::from_raw_parts would be so much nicer to iterate and to read. + unsafe { + let fmt_mod_blob_ptr = data.as_ptr() as *const drm_ffi::drm_format_modifier_blob; + let fmt_mod_blob = &*fmt_mod_blob_ptr; + + let formats_ptr: *const u32 = fmt_mod_blob_ptr + .cast::() + .offset(fmt_mod_blob.formats_offset as isize) + as *const _; + let modifiers_ptr: *const drm_ffi::drm_format_modifier = fmt_mod_blob_ptr + .cast::() + .offset(fmt_mod_blob.modifiers_offset as isize) + as *const _; + #[allow(clippy::unnecessary_cast)] + let formats_ptr = formats_ptr as *const u32; + #[allow(clippy::unnecessary_cast)] + let modifiers_ptr = modifiers_ptr as *const drm_ffi::drm_format_modifier; + + for i in 0..fmt_mod_blob.count_modifiers { + let mod_info = modifiers_ptr.offset(i as isize).read_unaligned(); + for j in 0..64 { + if mod_info.formats & (1u64 << j) != 0 { + let code = DrmFourcc::try_from( + formats_ptr + .offset((j + mod_info.offset) as isize) + .read_unaligned(), + ) + .ok(); + let modifier = DrmModifier::from(mod_info.modifier); + if let Some(code) = code { + formats.insert(DrmFormat { code, modifier }); + } + } + } + } + } + } + } + } else if plane_type(dev, plane)? == PlaneType::Cursor { + // Force a LINEAR layout for the cursor if the driver doesn't support modifiers + for format in formats.clone() { + formats.insert(DrmFormat { + code: format.code, + modifier: DrmModifier::Linear, + }); + } + } + + if formats.is_empty() { + formats.insert(DrmFormat { + code: DrmFourcc::Argb8888, + modifier: DrmModifier::Invalid, + }); + } + + trace!( + "Supported scan-out formats for plane ({:?}): {:?}", + plane, formats + ); + + Ok(FormatSet::from_formats(formats)) +} + +#[cfg(feature = "backend_gbm")] +fn plane_has_property( + dev: &(impl drm::control::Device + DevPath), + plane: plane::Handle, + name: &str, +) -> Result { + let props = dev.get_properties(plane).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get properties of plane", + dev: dev.dev_path(), + source, + }) + })?; + let (ids, _) = props.as_props_and_values(); + for &id in ids { + let info = dev.get_property(id).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get property info", + dev: dev.dev_path(), + source, + }) + })?; + if info.name().to_str().map(|x| x == name).unwrap_or(false) { + return Ok(true); + } + } + Ok(false) +} + +#[repr(C)] +// FIXME: use definition from drm_ffi once available +struct drm_plane_size_hint { + width: u16, + height: u16, +} + +fn plane_size_hints( + dev: &(impl ControlDevice + DevPath), + plane: plane::Handle, +) -> Result>>, DrmError> { + let props = dev.get_properties(plane).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get properties of plane", + dev: dev.dev_path(), + source, + }) + })?; + let (ids, vals) = props.as_props_and_values(); + for (&id, &val) in ids.iter().zip(vals.iter()) { + let info = dev.get_property(id).map_err(|source| { + DrmError::Access(AccessError { + errmsg: "Failed to get property info", + dev: dev.dev_path(), + source, + }) + })?; + if info.name().to_str().map(|x| x == "SIZE_HINTS").unwrap_or(false) { + let size_hints = if let drm::control::property::Value::Blob(blob_id) = + info.value_type().convert_value(val) + { + // Note that property value 0 (ie. no blob) is reserved for potential + // future use. Current userspace is expected to ignore the property + // if the value is 0 + if blob_id == 0 { + return Ok(None); + } + + let blob_info = drm_ffi::mode::get_property_blob(dev.as_fd(), blob_id as u32, None).map_err( + |source| { + DrmError::Access(AccessError { + errmsg: "Failed to get SIZE_HINTS blob info", + dev: dev.dev_path(), + source, + }) + }, + )?; + + let mut data = Vec::with_capacity(blob_info.length as usize); + drm_ffi::mode::get_property_blob(dev.as_fd(), blob_id as u32, Some(&mut data)).map_err( + |source| { + DrmError::Access(AccessError { + errmsg: "Failed to get SIZE_HINTS blob data", + dev: dev.dev_path(), + source, + }) + }, + )?; + + let num_size_hints = data.len() / std::mem::size_of::(); + let size_hints = unsafe { + std::slice::from_raw_parts(data.as_ptr() as *const drm_plane_size_hint, num_size_hints) + }; + Some( + size_hints + .iter() + .map(|size_hint| Size::from((size_hint.width, size_hint.height))) + .collect(), + ) + } else { + tracing::debug!(?plane, "SIZE_HINTS property has wrong value type"); + None + }; + return Ok(size_hints); + } + } + Ok(None) +} diff --git a/vendor/smithay/src/backend/drm/output.rs b/vendor/smithay/src/backend/drm/output.rs new file mode 100644 index 0000000..c19163b --- /dev/null +++ b/vendor/smithay/src/backend/drm/output.rs @@ -0,0 +1,1030 @@ +//! Device-wide synchronization helpers + +use std::{ + collections::HashMap, + fmt, + marker::PhantomData, + os::fd::AsFd, + sync::{Arc, Mutex, RwLock, RwLockWriteGuard, TryLockError}, +}; + +use drm::control::{self, Mode, connector, crtc}; +use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier}; + +use crate::{ + backend::{ + allocator::{ + Allocator, + dmabuf::{AsDmabuf, Dmabuf}, + gbm::GbmDevice, + }, + renderer::{Bind, Color32F, DebugFlags, Renderer, RendererSuper, Texture, element::RenderElement}, + }, + output::OutputModeSource, +}; + +use super::{ + DrmDevice, DrmError, Planes, + compositor::{ + DrmCompositor, FrameError, FrameFlags, FrameResult, PrimaryPlaneElement, RenderFrameError, + RenderFrameErrorType, RenderFrameResult, + }, + exporter::ExportFramebuffer, +}; + +type CompositorList = Arc>>>>; + +/// Provides synchronization between a [`DrmDevice`] and derived [`DrmOutput`]s. +/// +/// When working with a single [`DrmDevice`] with multiple outputs and using the [`DrmCompositor`] +/// for hardware scanout one can quickly run into a set of bandwidth issues, where plane usage on +/// on [`DrmCompositor`] can cause commits on another to fail. Especially in multi-threaded contexts +/// these scenarios are difficult to handle as your need information over the state of all outputs at once. +/// +/// The `DrmOutputManager` provides a way to handle operation commonly affected by this by locking the +/// whole device, while still providing [`DrmOutput`]-handles to drive individual surfaces. +pub struct DrmOutputManager +where + A: Allocator, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + device: DrmDevice, + allocator: A, + exporter: F, + gbm: Option>, + compositor: CompositorList, + color_formats: Vec, + renderer_formats: Vec, +} + +/// Locked variant of the [`DrmOutputManager`]. +pub struct LockedDrmOutputManager<'a, A, F, U, G> +where + A: Allocator, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + device: &'a mut DrmDevice, + allocator: &'a mut A, + exporter: &'a mut F, + gbm: &'a mut Option>, + compositor: RwLockWriteGuard<'a, HashMap>>>, + compositor_arc: CompositorList, + color_formats: &'a [DrmFourcc], + renderer_formats: &'a [DrmFormat], +} + +impl fmt::Debug for DrmOutputManager +where + A: Allocator + fmt::Debug, + ::Buffer: fmt::Debug, + F: ExportFramebuffer<::Buffer> + fmt::Debug, + U: fmt::Debug, + ::Buffer>>::Framebuffer: fmt::Debug + Send + Sync + 'static, + G: AsFd + fmt::Debug + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DrmOutputManager") + .field("device", &self.device) + .field("allocator", &self.allocator) + .field("exporter", &self.exporter) + .field("gbm", &self.gbm) + .field("compositor", &self.compositor) + .field("color_formats", &self.color_formats) + .field("renderer_formats", &self.renderer_formats) + .finish() + } +} + +impl<'a, A, F, U, G> fmt::Debug for LockedDrmOutputManager<'a, A, F, U, G> +where + A: Allocator + fmt::Debug, + ::Buffer: fmt::Debug, + F: ExportFramebuffer<::Buffer> + fmt::Debug, + U: fmt::Debug, + ::Buffer>>::Framebuffer: fmt::Debug + Send + Sync + 'static, + G: AsFd + fmt::Debug + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LockedDrmOutputManager") + .field("device", &self.device) + .field("allocator", &self.allocator) + .field("exporter", &self.exporter) + .field("gbm", &self.gbm) + .field("compositor", &self.compositor) + // skip compositor_arc to avoid unnecessary repetition + .field("color_formats", &self.color_formats) + .field("renderer_formats", &self.renderer_formats) + .finish() + } +} + +impl DrmOutputManager +where + A: Allocator, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + /// Access the underlying managed device + pub fn device(&self) -> &DrmDevice { + &self.device + } + + /// Mutably access the underlying managed device + pub fn device_mut(&mut self) -> &mut DrmDevice { + &mut self.device + } + + /// Access the [`Allocator`] of this output manager + pub fn allocator(&self) -> &A { + &self.allocator + } + + /// Pause the underlying device. See [`DrmDevice::pause`]. + pub fn pause(&mut self) { + self.device.pause(); + } +} + +impl<'a, A, F, U, G> LockedDrmOutputManager<'a, A, F, U, G> +where + A: Allocator, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + /// Access the underlying managed device + pub fn device(&self) -> &DrmDevice { + self.device + } + + /// Mutably access the underlying managed device + pub fn device_mut(&mut self) -> &mut DrmDevice { + self.device + } + + /// Access the [`Allocator`] of this output manager + pub fn allocator(&self) -> &A { + self.allocator + } + + /// Pause the underlying device. See [`DrmDevice::pause`]. + pub fn pause(&mut self) { + self.device.pause(); + } +} + +/// Errors returned by `DrmOutputManager`'s methods +#[derive(thiserror::Error, Debug)] +pub enum DrmOutputManagerError +where + A: std::error::Error + Send + Sync + 'static, + B: std::error::Error + Send + Sync + 'static, + F: std::error::Error + Send + Sync + 'static, + R: std::error::Error + Send + Sync + 'static, +{ + /// The specified CRTC is already in use + #[error("The specified CRTC {0:?} is already in use.")] + DuplicateCrtc(crtc::Handle), + /// The underlying drm device returned an error + #[error(transparent)] + Drm(#[from] DrmError), + /// The underlying [`DrmCompositor`] returned an error + #[error(transparent)] + Frame(FrameError), + /// The underlying [`DrmCompositor`] returned an error upon rendering a frame + #[error(transparent)] + RenderFrame(RenderFrameError), +} + +/// Result returned by `DrmOutputManager`'s methods +pub type DrmOutputManagerResult = Result< + U, + DrmOutputManagerError< + ::Error, + <::Buffer as AsDmabuf>::Error, + ::Buffer>>::Error, + ::Error, + >, +>; + +impl DrmOutputManager +where + A: Allocator + std::clone::Clone + std::fmt::Debug, + ::Buffer: AsDmabuf, + ::Error: Send + Sync + 'static, + <::Buffer as AsDmabuf>::Error: + std::marker::Send + std::marker::Sync + 'static, + F: ExportFramebuffer<::Buffer> + std::clone::Clone, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + ::Buffer>>::Error: + std::marker::Send + std::marker::Sync + 'static, + G: AsFd + std::clone::Clone + 'static, + U: 'static, +{ + /// Create a new [`DrmOutputManager`] from a [`DrmDevice`]. + /// + /// - `device` the underlying [`DrmDevice`] + /// - `allocator` used by created [`DrmOutput`]s for primary plane swapchains. + /// - `exporter` used by created [`DrmOutput`]s to create drm framebuffers + /// for the swapchain buffers (and if possible for element buffers) + /// for scan-out. + /// - `gbm` device used by created [`DrmOutput`]s for creating buffers for the + /// cursor plane, `None` will disable the cursor plane. + /// - `color_formats` as tested in order when creating a new [`DrmOutput`] + /// - `renderer_formats` as reported by the used renderer, used to build the + /// intersection between the possible scan-out formats of the + /// primary plane of created [`DrmOutput`]s and the renderer + pub fn new( + device: DrmDevice, + allocator: A, + exporter: F, + gbm: Option>, + color_formats: impl IntoIterator, + renderer_formats: impl IntoIterator, + ) -> Self { + Self { + device, + allocator, + exporter, + gbm, + compositor: Default::default(), + color_formats: color_formats.into_iter().collect(), + renderer_formats: renderer_formats.into_iter().collect(), + } + } + + /// Locks the [`DrmOutputManager`] causing derived [`DrmOutput`]s to stall + /// until the returned [`LockedDrmOutputManager`] is dropped. + /// + /// The api of the [`LockedDrmOutputManager`] provides safe access to operations, + /// which potentially need to modify the state of multiple underlying surfaces at once. + pub fn lock(&mut self) -> LockedDrmOutputManager<'_, A, F, U, G> { + LockedDrmOutputManager { + device: &mut self.device, + allocator: &mut self.allocator, + exporter: &mut self.exporter, + gbm: &mut self.gbm, + compositor: self.compositor.write().unwrap(), + compositor_arc: self.compositor.clone(), + color_formats: &self.color_formats, + renderer_formats: &self.renderer_formats, + } + } +} + +impl<'a, A, F, U, G> LockedDrmOutputManager<'a, A, F, U, G> +where + A: Allocator + std::clone::Clone + std::fmt::Debug, + ::Buffer: AsDmabuf, + ::Error: Send + Sync + 'static, + <::Buffer as AsDmabuf>::Error: + std::marker::Send + std::marker::Sync + 'static, + F: ExportFramebuffer<::Buffer> + std::clone::Clone, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + ::Buffer>>::Error: + std::marker::Send + std::marker::Sync + 'static, + G: AsFd + std::clone::Clone + 'static, + U: 'static, +{ + /// Create a new [`DrmOutput`] for the provided crtc of the underlying device. + /// + /// The [`OutputModeSource`] can be created from an [`Output`](crate::output::Output), which will automatically track + /// the output's mode changes. An [`OutputModeSource::Static`] variant should only be used when + /// manually updating modes using [`DrmCompositor::set_output_mode_source`]. + /// + /// This might cause commits on other surfaces to meet the bandwidth + /// requirements of the output by temporarily disabling additional planes, + /// forcing composition or falling back to implicit modifiers. + /// + /// - `crtc` - the crtc the underlying surface should drive + /// - `mode` - the mode the underlying surface should be initialized with + /// - `connectors` - the set of connectors the underlying surface should be initialized with + /// - `output_mode_source` used to to determine the size, scale and transform + /// - `planes` defines which planes the compositor is allowed to use for direct scan-out. + /// `None` will result in the compositor to use all planes as specified by [`DrmSurface::planes`][super::DrmSurface::planes] + /// - `renderer` used for compositing, when commits are necessarily to realize bandwidth constraints + /// - `render_elements` used for rendering, when commits are necessarily to realize bandwidth constraints + #[allow(clippy::too_many_arguments)] + pub fn initialize_output( + &mut self, + crtc: crtc::Handle, + mode: control::Mode, + connectors: &[connector::Handle], + output_mode_source: impl Into + std::fmt::Debug, + planes: Option, + renderer: &mut R, + render_elements: &DrmOutputRenderElements, + ) -> DrmOutputManagerResult, A, F, R> + where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, + { + let output_mode_source = output_mode_source.into(); + + if self.compositor.contains_key(&crtc) { + return Err(DrmOutputManagerError::DuplicateCrtc(crtc)); + } + + let mut create_compositor = + |implicit_modifiers: bool| -> FrameResult, A, F> { + let surface = self.device.create_surface(crtc, mode, connectors)?; + + if implicit_modifiers { + DrmCompositor::::new( + output_mode_source.clone(), + surface, + planes.clone(), + self.allocator.clone(), + self.exporter.clone(), + self.color_formats.iter().copied(), + self.renderer_formats + .iter() + .filter(|f| f.modifier == DrmModifier::Invalid) + .copied(), + self.device.cursor_size(), + self.gbm.clone(), + ) + } else { + DrmCompositor::::new( + output_mode_source.clone(), + surface, + planes.clone(), + self.allocator.clone(), + self.exporter.clone(), + self.color_formats.iter().copied(), + self.renderer_formats.iter().copied(), + self.device.cursor_size(), + self.gbm.clone(), + ) + } + }; + + let compositor = create_compositor(false); + + // Okay, so this can fail for various reasons... + // + // Enabling an additional CRTC might fail because the bandwidth + // requirement is higher then supported with the current configuration. + // + // * Bandwidth limitation caused by overlay plane usage: + // Each overlay plane requires some certain bandwidth and we only + // test that during plane assignment implicitly through an atomic test. + // When trying to enable an additional CRTC we might hit some limit and the + // only way to resolve this might be to disable all overlay planes and + // retry enabling the CRTC. + // + // * Bandwidth limitation caused by the primary plane format: + // Different formats (might) require a higher memory bandwidth than others. + // This also applies to the same fourcc with different modifiers. For example + // the Intel CCS Formats use an additional plane to transport meta-data. + // So if we fail to enable an additional CRTC we might be able to resolve + // the issue by using a different format. Again the only way to know is by + // trying out what works. + // + // So for now we try to disable overlay planes first. If that doesn't work, + // we set the modifiers to `Invalid` (for now) to give the driver the opportunity + // to re-allocate in the background. (TODO: Do this ourselves). + + match compositor { + Ok(compositor) => { + self.compositor.insert(crtc, Mutex::new(compositor)); + } + Err(err) => { + tracing::warn!( + ?crtc, + ?err, + "failed to initialize crtc, trying to lower bandwidth" + ); + + for compositor in self.compositor.values_mut() { + let compositor = compositor.get_mut().unwrap(); + if let Err(err) = render_elements.submit_composited_frame(&mut *compositor, renderer) { + if !matches!(err, DrmOutputManagerError::Frame(FrameError::EmptyFrame)) { + return Err(err); + } + } + } + + let compositor = create_compositor(false); + + match compositor { + Ok(compositor) => { + self.compositor.insert(crtc, Mutex::new(compositor)); + } + Err(err) => { + tracing::warn!( + ?crtc, + ?err, + "failed to initialize crtc, trying implicit modifiers" + ); + + for compositor in self.compositor.values_mut() { + let compositor = compositor.get_mut().unwrap(); + + let current_format = compositor.format(); + if let Err(err) = compositor.set_format( + self.allocator.clone(), + current_format, + [DrmModifier::Invalid], + ) { + tracing::warn!(?err, "failed to set new format"); + continue; + } + + render_elements.submit_composited_frame(&mut *compositor, renderer)?; + } + + let compositor = create_compositor(true); + + match compositor { + Ok(compositor) => { + self.compositor.insert(crtc, Mutex::new(compositor)); + } + Err(err) => { + // try to reset formats + for compositor in self.compositor.values_mut() { + let compositor = compositor.get_mut().unwrap(); + + let current_format = compositor.format(); + if let Err(err) = compositor.set_format( + self.allocator.clone(), + current_format, + self.renderer_formats + .iter() + .filter(|f| f.code == current_format) + .map(|f| f.modifier), + ) { + tracing::warn!(?err, "failed to reset format"); + continue; + } + + render_elements.submit_composited_frame(&mut *compositor, renderer)?; + } + + return Err(DrmOutputManagerError::Frame(err)); + } + } + } + } + } + }; + + // We need to render the new output once to lock in the primary plane as used with the new format, so we don't hit the bandwidth issue, + // when downstream potentially uses `FrameFlags::DEFAULT` immediately after this. + + let compositor = self.compositor.get_mut(&crtc).unwrap(); + let compositor = compositor.get_mut().unwrap(); + if let Err(err) = render_elements.submit_composited_frame(&mut *compositor, renderer) { + self.compositor.remove(&crtc); + return Err(err); + } + + Ok(DrmOutput { + compositor: self.compositor_arc.clone(), + crtc, + allocator: self.allocator.clone(), + renderer_formats: Vec::from(self.renderer_formats), + }) + } + + /// Grants exclusive access to all underlying [`DrmCompositor`]s. + pub fn compositors(&mut self) -> &HashMap>> { + &self.compositor + } + + /// Tries to apply a new [`Mode`] for the provided `crtc`. + /// + /// Fails if the mode is not compatible with the underlying + /// [`crtc`] or any of the pending [`connector`]s. + /// + /// This might cause commits on other surfaces to meet the bandwidth + /// requirements of the new mode by temporarily disabling additional planes + /// and forcing composition. + pub fn use_mode( + &mut self, + crtc: &crtc::Handle, + mode: Mode, + renderer: &mut R, + render_elements: &DrmOutputRenderElements, + ) -> DrmOutputManagerResult<(), A, F, R> + where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, + { + use_mode_internal( + &mut self.compositor, + crtc, + mode, + self.allocator, + self.renderer_formats, + renderer, + render_elements, + ) + } + + /// Tries to restore explicit modifiers on all surfaces. + /// + /// Adding new outputs (via [`LockedDrmOutputManager::initialize_output`]) might cause + /// surfaces to fall back to implicit modifiers to satisfy bandwidth requirements + /// of the new output. These should generally be avoided, so it is recommended + /// to call this method after destroying an individual output to try and re-allocate + /// implicit buffers used for the remaining outputs. + pub fn try_to_restore_modifiers( + &mut self, + renderer: &mut R, + render_elements: &DrmOutputRenderElements, + ) -> DrmOutputManagerResult<(), A, F, R> + where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, + { + // check if implicit modifiers are in use + if self + .compositor + .values_mut() + .any(|c| c.get_mut().unwrap().modifiers() == [DrmModifier::Invalid]) + { + // if so, first lower the bandwidth by disabling planes on all compositors + for compositor in self.compositor.values_mut() { + let compositor = compositor.get_mut().unwrap(); + if let Err(err) = render_elements.submit_composited_frame(&mut *compositor, renderer) { + if !matches!(err, DrmOutputManagerError::Frame(FrameError::EmptyFrame)) { + return Err(err); + } + } + } + + for compositor in self.compositor.values_mut() { + let compositor = compositor.get_mut().unwrap(); + if compositor.modifiers() != [DrmModifier::Invalid] { + continue; + } + + let current_format = compositor.format(); + if let Err(err) = compositor.set_format( + self.allocator.clone(), + current_format, + self.renderer_formats + .iter() + .filter(|f| f.code == current_format) + .map(|f| f.modifier), + ) { + tracing::warn!(?err, "failed to reset format"); + continue; + } + + render_elements.submit_composited_frame(&mut *compositor, renderer)?; + } + } + + Ok(()) + } + + /// Activates a previously paused device. + /// + /// Specifying `true` for `disable_connectors` will call [`DrmDevice::reset_state`] if + /// the device was not active before. Otherwise you need to make sure there are no + /// conflicting requirements when enabling or creating surfaces or you are prepared + /// to handle errors caused by those. + pub fn activate(&mut self, disable_connectors: bool) -> Result<(), DrmError> { + self.device.activate(disable_connectors)?; + + // We request a write guard here to guarantee unique access + for compositor in self.compositor.values_mut() { + if let Err(err) = compositor.get_mut().unwrap().reset_state() { + tracing::warn!("Failed to reset drm surface state: {}", err); + } + } + + Ok(()) + } +} + +/// A handle to an underlying [`DrmCompositor`] handled by an [`DrmOutputManager`]. +pub struct DrmOutput +where + A: Allocator, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + compositor: CompositorList, + crtc: crtc::Handle, + allocator: A, + renderer_formats: Vec, +} + +impl fmt::Debug for DrmOutput +where + A: Allocator + fmt::Debug, + ::Buffer: fmt::Debug, + F: ExportFramebuffer<::Buffer> + fmt::Debug, + U: fmt::Debug, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + G: AsFd + fmt::Debug + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("DrmOutput"); + match self.compositor.try_read() { + Ok(guard) => d.field("compositor", &guard.get(&self.crtc)), + Err(TryLockError::Poisoned(err)) => d.field("compositor", &&**err.get_ref()), + Err(TryLockError::WouldBlock) => d.field("compositor", &""), + }; + d.finish() + } +} + +impl DrmOutput +where + A: Allocator + std::clone::Clone + fmt::Debug, + ::Buffer: AsDmabuf, + ::Error: Send + Sync + 'static, + <::Buffer as AsDmabuf>::Error: std::marker::Send + std::marker::Sync + 'static, + F: ExportFramebuffer<::Buffer> + std::clone::Clone, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + ::Buffer>>::Error: + std::marker::Send + std::marker::Sync + 'static, + G: AsFd + std::clone::Clone + 'static, + U: 'static, +{ + /// Set the [`DebugFlags`] to use + /// + /// Note: This will reset the primary plane swapchain if + /// the flags differ from the current flags + pub fn set_debug_flags(&self, flags: DebugFlags) { + self.with_compositor(|compositor| compositor.set_debug_flags(flags)); + } + + /// Reset the underlying buffers + pub fn reset_buffers(&self) { + self.with_compositor(|compositor| compositor.reset_buffers()); + } + + /// Marks the current frame as submitted. + /// + /// *Note*: Needs to be called, after the vblank event of the matching [`DrmDevice`] + /// was received after calling [`DrmOutput::queue_frame`] on this surface. + /// Otherwise the underlying swapchain will run out of buffers eventually. + pub fn frame_submitted(&self) -> FrameResult, A, F> { + self.with_compositor(|compositor| compositor.frame_submitted()) + } + + /// Get the format of the underlying swapchain + pub fn format(&self) -> DrmFourcc { + self.with_compositor(|compositor| compositor.format()) + } + + /// Render the next frame + /// + /// - `elements` for this frame in front-to-back order + /// - `frame_mode` specifies techniques allowed to realize the frame + pub fn render_frame<'a, R, E>( + &mut self, + renderer: &mut R, + elements: &'a [E], + clear_color: impl Into, + frame_mode: FrameFlags, + ) -> Result, RenderFrameErrorType> + where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, + { + self.with_compositor(|compositor| { + compositor.render_frame(renderer, elements, clear_color, frame_mode) + }) + } + + /// Queues the current frame for scan-out. + /// + /// If `render_frame` has not been called prior to this function or returned no damage + /// this function will return [`FrameError::EmptyFrame`]. Instead of calling `queue_frame` it + /// is the callers responsibility to re-schedule the frame. A simple strategy for frame + /// re-scheduling is to queue a one-shot timer that will trigger after approximately one + /// retrace duration. + /// + /// *Note*: It is your responsibility to synchronize rendering if the [`RenderFrameResult`] + /// returned by the previous [`render_frame`](DrmOutput::render_frame) call returns `true` on [`RenderFrameResult::needs_sync`]. + /// + /// *Note*: This function needs to be followed up with [`DrmOutput::frame_submitted`] + /// when a vblank event is received, that denotes successful scan-out of the frame. + /// Otherwise the underlying swapchain will eventually run out of buffers. + /// + /// `user_data` can be used to attach some data to a specific buffer and later retrieved with [`DrmCompositor::frame_submitted`] + pub fn queue_frame(&mut self, user_data: U) -> FrameResult<(), A, F> { + self.with_compositor(|compositor| compositor.queue_frame(user_data)) + } + + /// Commits the current frame for scan-out. + /// + /// If `render_frame` has not been called prior to this function or returned no damage + /// this function will return [`FrameError::EmptyFrame`]. Instead of calling `commit_frame` it + /// is the callers responsibility to re-schedule the frame. A simple strategy for frame + /// re-scheduling is to queue a one-shot timer that will trigger after approximately one + /// retrace duration. + /// + /// *Note*: It is your responsibility to synchronize rendering if the [`RenderFrameResult`] + /// returned by the previous [`render_frame`](DrmOutput::render_frame) call returns `true` on [`RenderFrameResult::needs_sync`]. + /// + /// *Note*: This function should not be followed up with [`DrmOutput::frame_submitted`] + /// and will not generate a vblank event on the underlying device. + pub fn commit_frame(&mut self) -> FrameResult<(), A, F> { + self.with_compositor(|compositor| compositor.commit_frame()) + } + + /// Tries to apply a new [`Mode`] for this `DrmOutput`. + /// + /// Fails if the mode is not compatible with the underlying + /// [`crtc`] or any of the pending [`connector`]s. + /// + /// This might cause commits on other surfaces to meet the bandwidth + /// requirements of the new mode by temporarily disabling additional planes + /// and forcing composition. + pub fn use_mode( + &mut self, + mode: Mode, + renderer: &mut R, + render_elements: &DrmOutputRenderElements, + ) -> DrmOutputManagerResult<(), A, F, R> + where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, + { + let mut write_guard = self.compositor.write().unwrap(); + use_mode_internal( + &mut write_guard, + &self.crtc, + mode, + &self.allocator, + &self.renderer_formats, + renderer, + render_elements, + ) + } +} + +impl DrmOutput +where + A: Allocator, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + /// Returns the underlying [`crtc`] of this surface + pub fn crtc(&self) -> crtc::Handle { + self.crtc + } + + /// Provides exclusive access to the underlying [`DrmCompositor`] + pub fn with_compositor(&self, f: T) -> R + where + T: FnOnce(&mut DrmCompositor) -> R, + { + let read_guard = self.compositor.read().unwrap(); + let mut compositor_guard = read_guard.get(&self.crtc).unwrap().lock().unwrap(); + f(&mut compositor_guard) + } +} + +impl Drop for DrmOutput +where + A: Allocator, + F: ExportFramebuffer<::Buffer>, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + G: AsFd + 'static, +{ + fn drop(&mut self) { + let mut write_guard = self.compositor.write().unwrap(); + write_guard.remove(&self.crtc); + } +} + +fn use_mode_internal<'a, A, F, U, G, R, E>( + compositor_list: &mut RwLockWriteGuard<'a, HashMap>>>, + crtc: &crtc::Handle, + mode: Mode, + allocator: &A, + renderer_formats: &[DrmFormat], + renderer: &mut R, + render_elements: &DrmOutputRenderElements, +) -> DrmOutputManagerResult<(), A, F, R> +where + A: Allocator + std::clone::Clone + fmt::Debug, + ::Buffer: AsDmabuf, + ::Error: Send + Sync + 'static, + <::Buffer as AsDmabuf>::Error: + std::marker::Send + std::marker::Sync + 'static, + F: ExportFramebuffer<::Buffer> + std::clone::Clone, + ::Buffer>>::Framebuffer: std::fmt::Debug + Send + Sync + 'static, + ::Buffer>>::Error: + std::marker::Send + std::marker::Sync + 'static, + G: AsFd + std::clone::Clone + 'static, + U: 'static, + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, +{ + let res = compositor_list.get(crtc).unwrap().lock().unwrap().use_mode(mode); + + if let Err(err @ FrameError::DrmError(DrmError::TestFailed(_))) = res.as_ref() { + tracing::warn!(?crtc, ?err, "failed to set mode, trying to lower bandwidth usage"); + + for compositor in compositor_list.values_mut() { + let compositor = compositor.get_mut().unwrap(); + if let Err(err) = render_elements.submit_composited_frame(&mut *compositor, renderer) { + if !matches!(err, DrmOutputManagerError::Frame(FrameError::EmptyFrame)) { + return Err(err); + } + } + } + + let compositor = compositor_list.get_mut(crtc).unwrap().get_mut().unwrap(); + match compositor.use_mode(mode) { + Ok(_) => { + if let Err(err) = render_elements.submit_composited_frame(&mut *compositor, renderer) { + if !matches!(err, DrmOutputManagerError::Frame(FrameError::EmptyFrame)) { + return Err(err); + } + } + } + Err(err @ FrameError::DrmError(DrmError::TestFailed(_))) => { + tracing::warn!(?crtc, ?err, "failed to set mode, trying implicit modifiers"); + + for compositor in compositor_list.values_mut() { + let compositor = compositor.get_mut().unwrap(); + + let current_format = compositor.format(); + if let Err(err) = + compositor.set_format(allocator.clone(), current_format, [DrmModifier::Invalid]) + { + tracing::warn!(?err, "failed to set new format"); + continue; + } + + render_elements.submit_composited_frame(&mut *compositor, renderer)?; + } + + let compositor = compositor_list.get_mut(crtc).unwrap().get_mut().unwrap(); + match compositor.use_mode(mode) { + Ok(_) => render_elements.submit_composited_frame(&mut *compositor, renderer)?, + Err(err) => { + // try to reset format + + for compositor in compositor_list.values_mut() { + let compositor = compositor.get_mut().unwrap(); + + let current_format = compositor.format(); + if let Err(err) = compositor.set_format( + allocator.clone(), + current_format, + renderer_formats + .iter() + .filter(|f| f.code == current_format) + .map(|f| f.modifier), + ) { + tracing::warn!(?err, "failed to reset format"); + continue; + } + + render_elements.submit_composited_frame(&mut *compositor, renderer)?; + } + + return Err(DrmOutputManagerError::Frame(err)); + } + } + } + Err(err) => return Err(DrmOutputManagerError::Frame(err)), + }; + } + + Ok(()) +} + +/// Set of render elements for a set of outputs managed by an [`DrmOutputManager`]. +/// +/// A few methods of the [`DrmOutputManager`] and [`DrmOutput`] might need to do +/// commits to multiple surfaces to satisfy bandwidth constraints. To not render +/// a black screen this struct can be populated with screen contents to be used +/// when such an operation is required. Outputs not provided via +/// [`DrmOutputRenderElements::add_output`] will fallback to black. +#[derive(Debug)] +pub struct DrmOutputRenderElements +where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, +{ + render_elements: HashMap, Color32F)>, + _renderer: PhantomData, +} + +impl DrmOutputRenderElements +where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, +{ + /// Construct a new empty set of render elements + pub fn new() -> Self { + DrmOutputRenderElements { + render_elements: HashMap::new(), + _renderer: PhantomData, + } + } + + /// Construct a new empty set of render elements with pre-allocated capacity + /// for a number of crtcs. + pub fn with_capacity(cap: usize) -> Self { + DrmOutputRenderElements { + render_elements: HashMap::with_capacity(cap), + _renderer: PhantomData, + } + } +} + +impl Default for DrmOutputRenderElements +where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, +{ + fn default() -> Self { + Self::new() + } +} + +impl DrmOutputRenderElements +where + E: RenderElement, + R: Renderer + Bind, + R::TextureId: Texture + 'static, + R::Error: Send + Sync + 'static, +{ + /// Adds elements to be used when rendering for a given `crtc`. + /// + /// Outputs not provided via this will fallback to black when + /// this struct is passed to any consuming function. + pub fn add_output( + &mut self, + crtc: &crtc::Handle, + clear_color: Color32F, + elements: impl IntoIterator, + ) { + self.render_elements + .insert(*crtc, (elements.into_iter().collect(), clear_color)); + } + + fn submit_composited_frame( + &self, + compositor: &mut DrmCompositor, + renderer: &mut R, + ) -> DrmOutputManagerResult<(), A, F, R> + where + A: Allocator + std::clone::Clone + std::fmt::Debug, + ::Buffer: AsDmabuf, + ::Error: Send + Sync + 'static, + <::Buffer as AsDmabuf>::Error: + std::marker::Send + std::marker::Sync + 'static, + F: ExportFramebuffer<::Buffer> + std::clone::Clone, + ::Buffer>>::Framebuffer: + std::fmt::Debug + Send + Sync + 'static, + ::Buffer>>::Error: + std::marker::Send + std::marker::Sync + 'static, + G: AsFd + std::clone::Clone + 'static, + U: 'static, + { + let (elements, clear_color) = self + .render_elements + .get(&compositor.crtc()) + .map(|(elements, color)| (&**elements, color)) + .unwrap_or((&[], &Color32F::BLACK)); + let frame_result = compositor + .render_frame(renderer, elements, *clear_color, FrameFlags::empty()) + .map_err(DrmOutputManagerError::RenderFrame)?; + if frame_result.needs_sync() { + if let PrimaryPlaneElement::Swapchain(primary_swapchain_element) = frame_result.primary_element { + let _ = primary_swapchain_element.sync.wait(); + } + } + compositor.commit_frame().map_err(DrmOutputManagerError::Frame)?; + Ok(()) + } +} diff --git a/vendor/smithay/src/backend/drm/surface/atomic.rs b/vendor/smithay/src/backend/drm/surface/atomic.rs new file mode 100644 index 0000000..e63f0ef --- /dev/null +++ b/vendor/smithay/src/backend/drm/surface/atomic.rs @@ -0,0 +1,1677 @@ +use drm::control::Device as ControlDevice; +use drm::control::atomic::AtomicModeReq; +use drm::control::connector::Interface; +use drm::control::property::ValueType; +use drm::control::{ + AtomicCommitFlags, Mode, PlaneType, connector, crtc, dumbbuffer::DumbBuffer, framebuffer, plane, property, +}; + +#[cfg(debug_assertions)] +use std::collections::HashMap; +use std::collections::HashSet; +#[cfg(debug_assertions)] +use std::fmt; +use std::os::unix::io::AsRawFd; +use std::sync::{ + Arc, Mutex, RwLock, + atomic::{AtomicBool, Ordering}, +}; + +use crate::backend::drm::error::AccessError; +use crate::utils::{Coordinate, Rectangle, Transform}; +use crate::{ + backend::{ + allocator::format::{get_bpp, get_depth}, + drm::{ + DrmDeviceFd, + device::DrmDeviceInternal, + device::atomic::{PropMapping, map_props}, + error::Error, + plane_type, + }, + }, + utils::DevPath, +}; + +use tracing::{debug, info, info_span, instrument, trace, warn}; + +use super::{PlaneConfig, PlaneState, VrrSupport}; + +#[derive(Debug, Clone)] +pub struct State { + pub active: bool, + pub mode: Mode, + pub blob: property::Value<'static>, + pub vrr: bool, + pub connectors: HashSet, +} + +impl PartialEq for State { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.active == other.active + && self.mode == other.mode + && self.vrr == other.vrr + && self.connectors == other.connectors + } +} + +impl State { + fn current_state( + fd: &A, + crtc: crtc::Handle, + prop_mapping: &mut PropMapping, + ) -> Result { + let crtc_info = fd.get_crtc(crtc).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading crtc info", + dev: fd.dev_path(), + source, + }) + })?; + + // If we have no current mode, we create a fake one, which will not match (and thus gets overridden on the commit below). + // A better fix would probably be making mode an `Option`, but that would mean + // we need to be sure, we require a mode to always be set without relying on the compiler. + // So we cheat, because it works and is easier to handle later. + let current_mode = crtc_info.mode().unwrap_or_else(|| unsafe { std::mem::zeroed() }); + let current_blob = match crtc_info.mode() { + Some(mode) => fd.create_property_blob(&mode).map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to create Property Blob for mode", + dev: fd.dev_path(), + source, + }) + })?, + None => property::Value::Unknown(0), + }; + + let res_handles = fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading drm resources", + dev: fd.dev_path(), + source, + }) + })?; + + // the current set of connectors are those, that already have the correct `CRTC_ID` set. + // so we collect them for `current_state` and set the user-given once in `pending_state`. + // + // If they don't match, `commit_pending` will return true and they will be changed on the next `commit`. + let mut current_connectors = HashSet::new(); + // make sure the mapping is up to date + map_props(fd, res_handles.connectors(), &mut prop_mapping.connectors)?; + for conn in res_handles.connectors() { + let crtc_prop = prop_mapping.conn_prop_handle(*conn, "CRTC_ID")?; + if let (Ok(crtc_prop_info), Ok(props)) = (fd.get_property(crtc_prop), fd.get_properties(*conn)) { + let (ids, vals) = props.as_props_and_values(); + for (&id, &val) in ids.iter().zip(vals.iter()) { + if id == crtc_prop { + if let property::Value::CRTC(Some(conn_crtc)) = + crtc_prop_info.value_type().convert_value(val) + { + if conn_crtc == crtc { + current_connectors.insert(*conn); + } + } + break; + } + } + } + } + + // Get the current active (dpms) state and vrr state of the CRTC + // + // Changing a CRTC to active might require a modeset + let mut active = None; + let mut vrr = None; + if let Ok(props) = fd.get_properties(crtc) { + let active_prop = prop_mapping.crtcs.get(&crtc).and_then(|m| m.get("ACTIVE")); + let vrr_prop = prop_mapping.crtcs.get(&crtc).and_then(|m| m.get("VRR_ENABLED")); + let (ids, vals) = props.as_props_and_values(); + for (&id, &val) in ids.iter().zip(vals.iter()) { + if Some(&id) == active_prop { + active = property::ValueType::Boolean.convert_value(val).as_boolean(); + break; + } + if Some(&id) == vrr_prop { + vrr = property::ValueType::Boolean.convert_value(val).as_boolean(); + break; + } + } + } + + Ok(State { + // If we don't know the active state we just assume off. + // This is highly unlikely, but having a false negative should do no harm. + active: active.unwrap_or(false), + mode: current_mode, + blob: current_blob, + // If we don't know the VRR state, the driver doesn't support the property + vrr: vrr.unwrap_or(false), + connectors: current_connectors, + }) + } + + fn clear(&mut self) { + self.mode = unsafe { std::mem::zeroed() }; + self.blob = property::Value::Unknown(0); + self.connectors.clear(); + self.active = false; + self.vrr = false; + } +} + +#[derive(Debug)] +pub struct AtomicDrmSurface { + pub(in crate::backend::drm) fd: Arc, + pub(super) active: Arc, + crtc: crtc::Handle, + plane: plane::Handle, + used_planes: Mutex>, + prop_mapping: Arc>, + state: RwLock, + pending: RwLock, + pub(super) span: tracing::Span, +} + +impl AtomicDrmSurface { + #[allow(clippy::too_many_arguments)] + pub fn new( + fd: Arc, + active: Arc, + crtc: crtc::Handle, + plane: plane::Handle, + prop_mapping: Arc>, + mode: Mode, + connectors: &[connector::Handle], + ) -> Result { + let span = info_span!("drm_atomic", crtc = ?crtc); + let _guard = span.enter(); + info!( + "Initializing drm surface ({:?}:{:?}) with mode {:?} and connectors {:?}", + crtc, plane, mode, connectors + ); + + let state = State::current_state(&*fd, crtc, &mut prop_mapping.write().unwrap())?; + let blob = fd.create_property_blob(&mode).map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to create Property Blob for mode", + dev: fd.dev_path(), + source, + }) + })?; + let pending = State { + active: true, + mode, + blob, + vrr: false, + connectors: connectors.iter().copied().collect(), + }; + + drop(_guard); + let surface = AtomicDrmSurface { + fd, + active, + crtc, + plane, + used_planes: Mutex::new(HashSet::new()), + prop_mapping, + state: RwLock::new(state), + pending: RwLock::new(pending), + span, + }; + + Ok(surface) + } + + // we need a framebuffer to do test commits, which we use to verify our pending state. + // here we create a dumbbuffer for that purpose. + #[profiling::function] + fn create_test_buffer(&self, size: (u16, u16), plane: plane::Handle) -> Result { + let (w, h) = size; + let needs_alpha = plane_type(&*self.fd, plane)? != PlaneType::Primary; + let format = if needs_alpha { + crate::backend::allocator::Fourcc::Argb8888 + } else { + crate::backend::allocator::Fourcc::Xrgb8888 + }; + + let db = self + .fd + .create_dumb_buffer((w as u32, h as u32), format, get_bpp(format).unwrap() as u32) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to create dumb buffer", + dev: self.fd.dev_path(), + source, + }) + })?; + let fb_result = self + .fd + .add_framebuffer( + &db, + get_depth(format).unwrap() as u32, + get_bpp(format).unwrap() as u32, + ) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to create framebuffer", + dev: self.fd.dev_path(), + source, + }) + }); + + match fb_result { + Ok(fb) => Ok(TestBuffer { + fd: self.fd.clone(), + db, + fb, + }), + Err(err) => { + let _ = self.fd.destroy_dumb_buffer(db); + Err(err) + } + } + } + + pub fn current_connectors(&self) -> HashSet { + self.state.read().unwrap().connectors.clone() + } + + pub fn pending_connectors(&self) -> HashSet { + self.pending.read().unwrap().connectors.clone() + } + + pub fn current_mode(&self) -> Mode { + self.state.read().unwrap().mode + } + + pub fn pending_mode(&self) -> Mode { + self.pending.read().unwrap().mode + } + + fn ensure_props_known(&self, conns: &[connector::Handle]) -> Result<(), Error> { + let mapping_exists = { + let prop_mapping = self.prop_mapping.read().unwrap(); + conns + .iter() + .all(|conn| prop_mapping.connectors.contains_key(conn)) + }; + if !mapping_exists { + map_props( + &*self.fd, + self.fd + .resource_handles() + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading connector info", + dev: self.fd.dev_path(), + source, + }) + })? + .connectors(), + &mut self.prop_mapping.write().unwrap().connectors, + )?; + } + Ok(()) + } + + #[instrument(parent = &self.span, skip(self))] + pub fn add_connector(&self, conn: connector::Handle) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + self.ensure_props_known(&[conn])?; + let info = self.fd.get_connector(conn, false).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading connector info", + dev: self.fd.dev_path(), + source, + }) + })?; + + let mut pending = self.pending.write().unwrap(); + + // check if the connector can handle the current mode + if info.modes().contains(&pending.mode) { + let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?; + + // check if config is supported + let prop_mapping = self.prop_mapping.read().unwrap(); + let plane_state = PlaneState { + handle: self.plane, + config: Some(PlaneConfig { + src: Rectangle::from_size(pending.mode.size().into()).to_f64(), + dst: Rectangle::from_size( + (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(), + ), + transform: Transform::Normal, + alpha: 1.0, + damage_clips: None, + fb: test_buffer.fb, + fence: None, + }), + }; + + let mut connectors = pending.connectors.clone(); + connectors.insert(conn); + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + Some(pending.blob), + pending.vrr, + &connectors, + [], + [&plane_state], + )?; + self.fd + .atomic_commit( + AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, + req.build()?, + ) + .map_err(|_| Error::TestFailed(self.crtc))?; + + // seems to be, lets add the connector + pending.connectors.insert(conn); + + Ok(()) + } else { + Err(Error::ModeNotSuitable(pending.mode)) + } + } + + #[instrument(parent = &self.span, skip(self))] + pub fn remove_connector(&self, conn: connector::Handle) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mut pending = self.pending.write().unwrap(); + + // the test would also prevent this, but the error message is far less helpful + if pending.connectors.contains(&conn) && pending.connectors.len() == 1 { + return Err(Error::SurfaceWithoutConnectors(self.crtc)); + } + + // check if new config is supported (should be) + let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?; + + let prop_mapping = self.prop_mapping.read().unwrap(); + let plane_state = PlaneState { + handle: self.plane, + config: Some(PlaneConfig { + src: Rectangle::from_size(pending.mode.size().into()).to_f64(), + dst: Rectangle::from_size( + (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(), + ), + transform: Transform::Normal, + alpha: 1.0, + damage_clips: None, + fb: test_buffer.fb, + fence: None, + }), + }; + + let mut connectors = pending.connectors.clone(); + connectors.remove(&conn); + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + Some(pending.blob), + pending.vrr, + &connectors, + [&conn], + [&plane_state], + )?; + self.fd + .atomic_commit( + AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, + req.build()?, + ) + .map_err(|_| Error::TestFailed(self.crtc))?; + + // seems to be, lets remove the connector + pending.connectors.remove(&conn); + + Ok(()) + } + + #[instrument(parent = &self.span, skip(self))] + pub fn set_connectors(&self, connectors: &[connector::Handle]) -> Result<(), Error> { + // the test would also prevent this, but the error message is far less helpful + if connectors.is_empty() { + return Err(Error::SurfaceWithoutConnectors(self.crtc)); + } + + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let current = self.state.read().unwrap(); + let mut pending = self.pending.write().unwrap(); + + self.ensure_props_known(connectors)?; + let conns = connectors.iter().cloned().collect::>(); + let removed = current.connectors.difference(&conns); + + let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?; + + let prop_mapping = self.prop_mapping.read().unwrap(); + let plane_state = PlaneState { + handle: self.plane, + config: Some(PlaneConfig { + src: Rectangle::from_size(pending.mode.size().into()).to_f64(), + dst: Rectangle::from_size( + (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(), + ), + transform: Transform::Normal, + alpha: 1.0, + damage_clips: None, + fb: test_buffer.fb, + fence: None, + }), + }; + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + Some(pending.blob), + pending.vrr, + &conns, + removed, + [&plane_state], + )?; + + self.fd + .atomic_commit( + AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, + req.build()?, + ) + .map_err(|_| Error::TestFailed(self.crtc))?; + + pending.connectors = conns; + + Ok(()) + } + + #[instrument(level = "debug", parent = &self.span, skip(self))] + pub fn use_mode(&self, mode: Mode) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mut pending = self.pending.write().unwrap(); + + // check if new config is supported + let new_blob = self.fd.create_property_blob(&mode).map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to create Property Blob for mode", + dev: self.fd.dev_path(), + source, + }) + })?; + + let test_buffer = self.create_test_buffer(mode.size(), self.plane)?; + + let prop_mapping = self.prop_mapping.read().unwrap(); + let plane_state = PlaneState { + handle: self.plane, + config: Some(PlaneConfig { + src: Rectangle::from_size(mode.size().into()).to_f64(), + dst: Rectangle::from_size((mode.size().0 as i32, mode.size().1 as i32).into()), + transform: Transform::Normal, + alpha: 1.0, + damage_clips: None, + fb: test_buffer.fb, + fence: None, + }), + }; + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + Some(new_blob), + pending.vrr, + pending.connectors.iter(), + [], + [&plane_state], + )?; + if let Err(err) = self + .fd + .atomic_commit( + AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, + req.build()?, + ) + .map_err(|_| Error::TestFailed(self.crtc)) + { + let _ = self.fd.destroy_property_blob(new_blob.into()); + return Err(err); + } + + // seems to be, lets change the mode + pending.mode = mode; + pending.blob = new_blob; + + Ok(()) + } + + pub fn vrr_supported(&self, conn: connector::Handle) -> Result { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let props = self.prop_mapping.read().unwrap(); + if self + .prop_mapping + .read() + .unwrap() + .crtc_prop_handle(self.crtc, "VRR_ENABLED") + .is_err() + { + return Ok(VrrSupport::NotSupported); + } + + if let Some(vrr_prop) = props + .connectors + .get(&conn) + .and_then(|props| props.get("vrr_capable")) + { + for (prop, value) in self.fd.get_properties(conn).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error querying properties", + dev: self.fd.dev_path(), + source, + }) + })? { + if prop == *vrr_prop { + let interface = self + .fd + .get_connector(conn, false) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error querying connector", + dev: self.fd.dev_path(), + source, + }) + })? + .interface(); + + // see: https://gitlab.freedesktop.org/drm/amd/-/issues/2200#note_2159982 + // Currently setting VRR for HDMI connectors will cause flickering despite not needing `ALLOW_MODESET` + // TODO: Once the kernel is fixed, do actual test commits with and without `ALLOW_MODESET`. + return Ok( + match ValueType::Boolean.convert_value(value).as_boolean().unwrap() { + true if interface == Interface::HDMIA || interface == Interface::HDMIB => { + VrrSupport::RequiresModeset + } + true => VrrSupport::Supported, + false => VrrSupport::NotSupported, + }, + ); + } + } + } + + Ok(VrrSupport::NotSupported) + } + + pub fn vrr_enabled(&self) -> bool { + self.pending.read().unwrap().vrr + } + + pub fn use_vrr(&self, value: bool) -> Result<(), Error> { + let mut current = self.state.write().unwrap(); + let mut pending = self.pending.write().unwrap(); + if pending.vrr == value { + return Ok(()); + } + let prop_mapping = self.prop_mapping.read().unwrap(); + + if value && prop_mapping.crtc_prop_handle(self.crtc, "VRR_ENABLED").is_err() { + return Err(Error::UnknownProperty { + handle: self.crtc.into(), + name: "VRR_ENABLED", + }); + } + + let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?; + let plane_config = PlaneState { + handle: self.plane, + config: Some(PlaneConfig { + src: Rectangle::from_size(pending.mode.size().into()).to_f64(), + dst: Rectangle::from_size( + (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(), + ), + transform: Transform::Normal, + alpha: 1.0, + damage_clips: None, + fb: test_buffer.fb, + fence: None, + }), + }; + + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + Some(pending.blob), + value, + &pending.connectors, + &[], + [&plane_config], + )?; + + if *current == *pending { + // Try a non modesetting commit + if self + .fd + .atomic_commit(AtomicCommitFlags::TEST_ONLY, req.build()?) + .is_ok() + { + pending.vrr = value; + current.vrr = value; + return Ok(()); + } + } + + // Try a modeset commit + self.fd + .atomic_commit( + AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, + req.build()?, + ) + .map_err(|_| Error::TestFailed(self.crtc))?; + + pending.vrr = value; + Ok(()) + } + + pub fn commit_pending(&self) -> bool { + *self.pending.read().unwrap() != *self.state.read().unwrap() + } + + #[instrument(level = "trace", parent = &self.span, skip(self, planes))] + #[profiling::function] + pub fn test_state<'a>( + &self, + planes: impl IntoIterator>, + allow_modeset: bool, + ) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let current = self.state.read().unwrap(); + let pending = self.pending.read().unwrap(); + + self.test_state_internal(planes, allow_modeset, ¤t, &pending) + } + + fn test_state_internal<'a>( + &self, + planes: impl IntoIterator>, + allow_modeset: bool, + current: &'_ State, + pending: &'_ State, + ) -> Result<(), Error> { + let planes = planes.into_iter().collect::>(); + + let current_conns = current.connectors.clone(); + let pending_conns = pending.connectors.clone(); + let removed = current_conns.difference(&pending_conns); + let prop_mapping = self.prop_mapping.read().unwrap(); + + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + Some(pending.blob), + pending.vrr, + &pending_conns, + removed, + &*planes, + )?; + + let flags = if allow_modeset { + AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY + } else { + AtomicCommitFlags::TEST_ONLY + }; + self.fd.atomic_commit(flags, req.build()?).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error testing state", + dev: self.fd.dev_path(), + source, + }) + }) + } + + #[instrument(level = "trace", parent = &self.span, skip(self, planes))] + #[profiling::function] + pub fn commit<'a>( + &self, + planes: impl IntoIterator>, + event: bool, + ) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let planes = planes.into_iter().collect::>(); + let mut current = self.state.write().unwrap(); + let mut used_planes = self.used_planes.lock().unwrap(); + let pending = self.pending.read().unwrap(); + + debug!(current = ?*current, pending = ?*pending, ?planes, "Preparing Commit",); + + // we need the differences to know, which connectors need to change properties + let current_conns = current.connectors.clone(); + let pending_conns = pending.connectors.clone(); + let removed = current_conns.difference(&pending_conns); + + for conn in removed.clone() { + if let Ok(info) = self.fd.get_connector(*conn, false) { + info!("Removing connector: {:?}", info.interface()); + } else { + info!("Removing unknown connector"); + } + } + + for conn in &pending_conns { + if let Ok(info) = self.fd.get_connector(*conn, false) { + info!("Adding connector: {:?}", info.interface()); + } else { + info!("Adding unknown connector"); + } + } + + if current.mode != pending.mode { + info!("Setting new mode: {:?}", pending.mode.name()); + } + + trace!("Testing screen config"); + + // test the new config and return the request if it would be accepted by the driver. + let prop_mapping = self.prop_mapping.read().unwrap(); + let req = { + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + Some(pending.blob), + pending.vrr, + &pending_conns, + removed, + &*planes, + )?; + + if let Err(err) = self.fd.atomic_commit( + AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY, + req.build()?, + ) { + warn!("New screen configuration invalid!:\n\t{:?}\n\t{}\n", req, err); + + return Err(Error::TestFailed(self.crtc)); + } else { + if current.mode != pending.mode { + if let Err(err) = self.fd.destroy_property_blob(current.blob.into()) { + warn!("Failed to destroy old mode property blob: {}", err); + } + } + + // new config + req + } + }; + + debug!("Setting screen: {:?}", req); + let result = self + .fd + .atomic_commit( + if event { + // on the atomic api we can modeset and trigger a page_flip event on the same call! + AtomicCommitFlags::PAGE_FLIP_EVENT | AtomicCommitFlags::ALLOW_MODESET + // we also *should* not need to wait for completion, like with `set_crtc`, + // because we have tested this exact commit already, so we do not expect any errors later down the line. + // + // but there is always an exception and `amdgpu` can fail in interesting ways with this flag set... + // https://gitlab.freedesktop.org/drm/amd/-/issues?scope=all&utf8=%E2%9C%93&state=opened&search=drm_atomic_helper_wait_for_flip_done + // + // so we skip this flag: + // AtomicCommitFlags::Nonblock, + } else { + AtomicCommitFlags::ALLOW_MODESET + }, + req.build()?, + ) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error setting crtc", + dev: self.fd.dev_path(), + source, + }) + }); + + if result.is_ok() { + *current = pending.clone(); + for plane in planes.iter() { + if plane.config.is_some() { + used_planes.insert(plane.handle); + } else { + used_planes.remove(&plane.handle); + } + } + } + + result + } + + #[instrument(level = "trace", parent = &self.span, skip(self, planes))] + #[profiling::function] + pub fn page_flip<'a>( + &self, + planes: impl IntoIterator>, + event: bool, + ) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mut used_planes = self.used_planes.lock().unwrap(); + let planes = planes.into_iter().collect::>(); + + // page flips work just like commits with fewer parameters.. + let prop_mapping = self.prop_mapping.read().unwrap(); + let req = AtomicRequest::build_request( + &prop_mapping, + self.crtc, + None, + self.state.read().unwrap().vrr, + [], + [], + &*planes, + )?; + + // .. and without `AtomicCommitFlags::AllowModeset`. + // If we would set anything here, that would require a modeset, this would fail, + // indicating a problem in our assumptions. + trace!(?planes, "Queueing page flip: {:?}", req); + let res = self + .fd + .atomic_commit( + if event { + AtomicCommitFlags::PAGE_FLIP_EVENT | AtomicCommitFlags::NONBLOCK + } else { + AtomicCommitFlags::NONBLOCK + }, + req.build()?, + ) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Page flip commit failed", + dev: self.fd.dev_path(), + source, + }) + }); + + if res.is_ok() { + for plane in planes.iter() { + if plane.config.is_some() { + used_planes.insert(plane.handle); + } else { + used_planes.remove(&plane.handle); + } + } + } + + res + } + + // this helper function disconnects the plane. + // this is mostly used to remove the contents quickly, e.g. on tty switch, + // as other compositors might not make use of other planes, + // leaving our e.g. cursor or overlays as a relict of a better time on the screen. + pub fn clear_plane(&self, plane: plane::Handle) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mapping = self.prop_mapping.read().unwrap(); + let mut req = AtomicRequest::new(&mapping); + req.reset_plane(plane)?; + let req = req.build()?; + + let result = self + .fd + .atomic_commit(AtomicCommitFlags::empty(), req) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to commit on clear_plane", + dev: self.fd.dev_path(), + source, + }) + }); + + if result.is_ok() { + self.used_planes.lock().unwrap().remove(&plane); + } + + result + } + + #[profiling::function] + fn clear_state(&self) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let _guard = self.span.enter(); + let prop_mapping = self.prop_mapping.read().unwrap(); + let mut req = AtomicRequest::new(&prop_mapping); + // reset all planes we used + for plane in self.used_planes.lock().unwrap().iter() { + req.reset_plane(*plane)?; + } + + // disable connectors again + let current = self.state.read().unwrap(); + for conn in current.connectors.iter() { + req.reset_connector(*conn)?; + } + + // disable crtc + req.reset_crtc(self.crtc)?; + std::mem::drop(current); + + let res = self + .fd + .atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req.build()?) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to commit on clear_state", + dev: self.fd.dev_path(), + source, + }) + }); + + if res.is_ok() { + self.used_planes.lock().unwrap().clear(); + self.state.write().unwrap().clear(); + } + + res + } + + pub(crate) fn reset_state( + &self, + fd: Option<&B>, + ) -> Result<(), Error> { + *self.state.write().unwrap() = if let Some(fd) = fd { + State::current_state(fd, self.crtc, &mut self.prop_mapping.write().unwrap())? + } else { + State::current_state(&*self.fd, self.crtc, &mut self.prop_mapping.write().unwrap())? + }; + + // Re-initialize the mode blob which might got lost after suspend/resume + let mut pending = self.pending.write().unwrap(); + let blob = self.fd.create_property_blob(&pending.mode).map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to create Property Blob for mode", + dev: self.fd.dev_path(), + source, + }) + })?; + + let old_blob = std::mem::replace(&mut pending.blob, blob); + let _ = self.fd.destroy_property_blob(old_blob.into()); + + Ok(()) + } + + pub(crate) fn device_fd(&self) -> &DrmDeviceFd { + self.fd.device_fd() + } + + pub fn clear(&self) -> Result<(), Error> { + self.clear_state() + } +} + +struct TestBuffer { + fd: Arc, + db: DumbBuffer, + fb: framebuffer::Handle, +} + +impl AsRef for TestBuffer { + fn as_ref(&self) -> &framebuffer::Handle { + &self.fb + } +} + +impl Drop for TestBuffer { + fn drop(&mut self) { + let _ = self.fd.destroy_framebuffer(self.fb); + let _ = self.fd.destroy_dumb_buffer(self.db); + } +} + +impl Drop for AtomicDrmSurface { + fn drop(&mut self) { + if !self.active.load(Ordering::SeqCst) { + // the device is gone or we are on another tty + // old state has been restored, we shouldn't touch it. + // if we are on another tty the connectors will get disabled + // by the device, when switching back + return; + } + + let _guard = self.span.enter(); + if let Err(err) = self.clear_state() { + warn!("Unable to clear state: {}", err); + } + } +} + +#[inline] +fn to_fixed(n: N) -> u32 { + f64::round(n.to_f64() * (1 << 16) as f64) as u32 +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + struct DrmRotation: u8 { + const ROTATE_0 = 0b00000001; + const ROTATE_90 = 0b00000010; + const ROTATE_180 = 0b00000100; + const ROTATE_270 = 0b00001000; + const REFLECT_X = 0b00010000; + const REFLECT_Y = 0b00100000; + } +} + +impl From for DrmRotation { + #[inline] + fn from(transform: Transform) -> Self { + match transform { + Transform::Normal => DrmRotation::ROTATE_0, + Transform::_90 => DrmRotation::ROTATE_90, + Transform::_180 => DrmRotation::ROTATE_180, + Transform::_270 => DrmRotation::ROTATE_270, + Transform::Flipped => DrmRotation::REFLECT_Y, + Transform::Flipped90 => DrmRotation::REFLECT_Y | DrmRotation::ROTATE_90, + Transform::Flipped180 => DrmRotation::REFLECT_Y | DrmRotation::ROTATE_180, + Transform::Flipped270 => DrmRotation::REFLECT_Y | DrmRotation::ROTATE_270, + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + backend::drm::surface::atomic::to_fixed, + utils::{Physical, Rectangle}, + }; + + use super::AtomicDrmSurface; + + fn is_send() {} + + #[test] + fn surface_is_send() { + is_send::(); + } + + #[test] + fn test_fixed_point() { + let geometry: Rectangle = Rectangle::from_size((1920.0, 1080.0).into()); + let fixed = to_fixed(geometry.size.w) as u64; + assert_eq!(125829120, fixed); + } + + #[test] + fn test_fractional_fixed_point() { + let geometry: Rectangle = Rectangle::from_size((1920.1, 1080.0).into()); + let fixed = to_fixed(geometry.size.w) as u64; + assert_eq!(125835674, fixed); + } +} + +#[cfg(debug_assertions)] +struct AtomicRequest<'a> { + mapping: &'a PropMapping, + crtc_props: HashMap>>, + connector_props: HashMap>>, + plane_props: HashMap>>, +} + +#[cfg(not(debug_assertions))] +#[cfg_attr(not(debug_assertions), derive(Debug))] +struct AtomicRequest<'a> { + mapping: &'a PropMapping, + request: AtomicModeReq, +} + +#[cfg(debug_assertions)] +impl fmt::Debug for AtomicRequest<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AtomicRequest") + .field("crtcs", &self.crtc_props) + .field("connectors", &self.connector_props) + .field("plane", &self.plane_props) + .finish() + } +} + +#[cfg(debug_assertions)] +impl<'a> AtomicRequest<'a> { + fn new(mapping: &'a PropMapping) -> AtomicRequest<'a> { + AtomicRequest { + mapping, + crtc_props: HashMap::new(), + connector_props: HashMap::new(), + plane_props: HashMap::new(), + } + } + + fn set_connector(&mut self, conn: connector::Handle, crtc: crtc::Handle) -> Result<(), Error> { + let connector_props = self.connector_props.entry(conn).or_default(); + connector_props.insert("CRTC_ID", property::Value::CRTC(Some(crtc))); + Ok(()) + } + + fn reset_connector(&mut self, conn: connector::Handle) -> Result<(), Error> { + let connector_props = self.connector_props.entry(conn).or_default(); + connector_props.insert("CRTC_ID", property::Value::CRTC(None)); + Ok(()) + } + + fn set_crtc( + &mut self, + crtc: crtc::Handle, + mode: Option>, + vrr: bool, + ) -> Result<(), Error> { + let crtc_props = self.crtc_props.entry(crtc).or_default(); + + crtc_props.insert("ACTIVE", property::Value::Boolean(true)); + if let Some(blob) = mode { + crtc_props.insert("MODE_ID", blob); + } + if self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED").is_ok() { + crtc_props.insert("VRR_ENABLED", property::Value::Boolean(vrr)); + } else if vrr { + return Err(Error::UnknownProperty { + handle: crtc.into(), + name: "VRR_ENABLED", + }); + } + + Ok(()) + } + + fn reset_crtc(&mut self, crtc: crtc::Handle) -> Result<(), Error> { + let crtc_props = self.crtc_props.entry(crtc).or_default(); + + crtc_props.insert("ACTIVE", property::Value::Boolean(false)); + crtc_props.insert("MODE_ID", property::Value::Blob(0)); + if self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED").is_ok() { + crtc_props.insert("VRR_ENABLED", property::Value::Boolean(false)); + } + Ok(()) + } + + fn set_plane(&mut self, crtc: crtc::Handle, plane_state: &PlaneState<'a>) -> Result<(), Error> { + let handle = plane_state.handle; + let plane_props = self.plane_props.entry(handle).or_default(); + + if let Some(config) = plane_state.config.as_ref() { + plane_props.insert("CRTC_ID", property::Value::CRTC(Some(crtc))); + plane_props.insert("FB_ID", property::Value::Framebuffer(Some(config.fb))); + // these are 16.16. fixed point + plane_props.insert( + "SRC_X", + property::Value::UnsignedRange(to_fixed(config.src.loc.x) as u64), + ); + plane_props.insert( + "SRC_Y", + property::Value::UnsignedRange(to_fixed(config.src.loc.y) as u64), + ); + plane_props.insert( + "SRC_W", + property::Value::UnsignedRange(to_fixed(config.src.size.w) as u64), + ); + plane_props.insert( + "SRC_H", + property::Value::UnsignedRange(to_fixed(config.src.size.h) as u64), + ); + + plane_props.insert("CRTC_X", property::Value::SignedRange(config.dst.loc.x as i64)); + plane_props.insert("CRTC_Y", property::Value::SignedRange(config.dst.loc.y as i64)); + plane_props.insert("CRTC_W", property::Value::UnsignedRange(config.dst.size.w as u64)); + plane_props.insert("CRTC_H", property::Value::UnsignedRange(config.dst.size.h as u64)); + + if self.mapping.plane_prop_handle(handle, "rotation").is_ok() { + plane_props.insert( + "rotation", + property::Value::Bitmask(DrmRotation::from(config.transform).bits() as u64), + ); + } else if config.transform != Transform::Normal { + // if we are missing the rotation property we can no rely on + // the driver to report a non working configuration and can + // only guarantee that Transform::Normal (no rotation) will + // work + return Err(Error::UnknownProperty { + handle: handle.into(), + name: "rotation", + }); + } + if self.mapping.plane_prop_handle(handle, "alpha").is_ok() { + plane_props.insert( + "alpha", + property::Value::UnsignedRange((config.alpha * u16::MAX as f32).round() as u64), + ); + } else if config.alpha != 1.0 { + // if we are missing the alpha property we can not display any transparent alpha values + return Err(Error::UnknownProperty { + handle: handle.into(), + name: "alpha", + }); + } + if self.mapping.plane_prop_handle(handle, "FB_DAMAGE_CLIPS").is_ok() { + if let Some(damage) = config.damage_clips.as_ref() { + plane_props.insert("FB_DAMAGE_CLIPS", *damage); + } else { + plane_props.insert("FB_DAMAGE_CLIPS", property::Value::Blob(0)); + } + } + if self.mapping.plane_prop_handle(handle, "IN_FENCE_FD").is_ok() { + if let Some(fence) = config.fence.as_ref().map(|f| f.as_raw_fd()) { + plane_props.insert("IN_FENCE_FD", property::Value::SignedRange(fence as i64)); + } else { + plane_props.insert("IN_FENCE_FD", property::Value::SignedRange(-1)); + } + } else if config.fence.is_some() { + return Err(Error::UnknownProperty { + handle: handle.into(), + name: "IN_FENCE_FD", + }); + } + } else { + self.reset_plane(handle)?; + } + + Ok(()) + } + + fn reset_plane(&mut self, plane: plane::Handle) -> Result<(), Error> { + let plane_props = self.plane_props.entry(plane).or_default(); + + plane_props.insert("CRTC_ID", property::Value::CRTC(None)); + plane_props.insert("FB_ID", property::Value::Framebuffer(None)); + // these are 16.16. fixed point + plane_props.insert("SRC_X", property::Value::UnsignedRange(0u64)); + plane_props.insert("SRC_Y", property::Value::UnsignedRange(0u64)); + plane_props.insert("SRC_W", property::Value::UnsignedRange(0u64)); + plane_props.insert("SRC_H", property::Value::UnsignedRange(0u64)); + + plane_props.insert("CRTC_X", property::Value::SignedRange(0i64)); + plane_props.insert("CRTC_Y", property::Value::SignedRange(0i64)); + plane_props.insert("CRTC_W", property::Value::UnsignedRange(0u64)); + plane_props.insert("CRTC_H", property::Value::UnsignedRange(0u64)); + + if self.mapping.plane_prop_handle(plane, "rotation").is_ok() { + plane_props.insert( + "rotation", + property::Value::Bitmask(DrmRotation::from(Transform::Normal).bits() as u64), + ); + } + if self.mapping.plane_prop_handle(plane, "alpha").is_ok() { + plane_props.insert("alpha", property::Value::UnsignedRange(0xffff)); + } + if self.mapping.plane_prop_handle(plane, "FB_DAMAGE_CLIPS").is_ok() { + plane_props.insert("FB_DAMAGE_CLIPS", property::Value::Blob(0)); + } + if self.mapping.plane_prop_handle(plane, "IN_FENCE_FD").is_ok() { + plane_props.insert("IN_FENCE_FD", property::Value::SignedRange(-1)); + } + Ok(()) + } + + fn build(&self) -> Result { + let mut req = AtomicModeReq::new(); + + for (crtc, props) in &self.crtc_props { + for (name, value) in props { + req.add_property(*crtc, self.mapping.crtc_prop_handle(*crtc, name)?, *value); + } + } + for (conn, props) in &self.connector_props { + for (name, value) in props { + req.add_property(*conn, self.mapping.conn_prop_handle(*conn, name)?, *value); + } + } + for (plane, props) in &self.plane_props { + for (name, value) in props { + req.add_property(*plane, self.mapping.plane_prop_handle(*plane, name)?, *value); + } + } + + Ok(req) + } +} + +#[cfg(not(debug_assertions))] +impl<'a> AtomicRequest<'a> { + fn new(mapping: &'a PropMapping) -> AtomicRequest<'a> { + AtomicRequest { + mapping, + request: AtomicModeReq::new(), + } + } + + fn set_connector(&mut self, conn: connector::Handle, crtc: crtc::Handle) -> Result<(), Error> { + self.request.add_property( + conn, + self.mapping.conn_prop_handle(conn, "CRTC_ID")?, + property::Value::CRTC(Some(crtc)), + ); + Ok(()) + } + + fn reset_connector(&mut self, conn: connector::Handle) -> Result<(), Error> { + self.request.add_property( + conn, + self.mapping.conn_prop_handle(conn, "CRTC_ID")?, + property::Value::CRTC(None), + ); + Ok(()) + } + + fn set_crtc( + &mut self, + crtc: crtc::Handle, + mode: Option>, + vrr: bool, + ) -> Result<(), Error> { + if let Some(blob) = mode { + self.request + .add_property(crtc, self.mapping.crtc_prop_handle(crtc, "MODE_ID")?, blob); + } + + self.request.add_property( + crtc, + self.mapping.crtc_prop_handle(crtc, "ACTIVE")?, + property::Value::Boolean(true), + ); + + if let Ok(vrr_prop) = self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED") { + self.request + .add_property(crtc, vrr_prop, property::Value::Boolean(vrr)); + } else if vrr { + return Err(Error::UnknownProperty { + handle: crtc.into(), + name: "VRR_ENABLED", + }); + } + + Ok(()) + } + + fn reset_crtc(&mut self, crtc: crtc::Handle) -> Result<(), Error> { + self.request.add_property( + crtc, + self.mapping.crtc_prop_handle(crtc, "ACTIVE")?, + property::Value::Boolean(false), + ); + self.request.add_property( + crtc, + self.mapping.crtc_prop_handle(crtc, "MODE_ID")?, + property::Value::Blob(0), + ); + if let Ok(prop) = self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED") { + self.request + .add_property(crtc, prop, property::Value::Boolean(false)); + } + Ok(()) + } + + fn set_plane(&mut self, crtc: crtc::Handle, plane_state: &PlaneState<'_>) -> Result<(), Error> { + let handle = plane_state.handle; + if let Some(config) = plane_state.config.as_ref() { + // connect the plane to the CRTC + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "CRTC_ID")?, + property::Value::CRTC(Some(crtc)), + ); + + // Set the fb for the plane + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "FB_ID")?, + property::Value::Framebuffer(Some(config.fb)), + ); + + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "SRC_X")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(to_fixed(config.src.loc.x) as u64), + ); + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "SRC_Y")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(to_fixed(config.src.loc.y) as u64), + ); + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "SRC_W")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(to_fixed(config.src.size.w) as u64), + ); + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "SRC_H")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(to_fixed(config.src.size.h) as u64), + ); + + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "CRTC_X")?, + property::Value::SignedRange(config.dst.loc.x as i64), + ); + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "CRTC_Y")?, + property::Value::SignedRange(config.dst.loc.y as i64), + ); + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "CRTC_W")?, + property::Value::UnsignedRange(config.dst.size.w as u64), + ); + self.request.add_property( + handle, + self.mapping.plane_prop_handle(handle, "CRTC_H")?, + property::Value::UnsignedRange(config.dst.size.h as u64), + ); + if let Ok(prop) = self.mapping.plane_prop_handle(handle, "rotation") { + self.request.add_property( + handle, + prop, + property::Value::Bitmask(DrmRotation::from(config.transform).bits() as u64), + ); + } else if config.transform != Transform::Normal { + // if we are missing the rotation property we can no rely on + // the driver to report a non working configuration and can + // only guarantee that Transform::Normal (no rotation) will + // work + return Err(Error::UnknownProperty { + handle: handle.into(), + name: "rotation", + }); + } + if let Ok(prop) = self.mapping.plane_prop_handle(handle, "alpha") { + self.request.add_property( + handle, + prop, + property::Value::UnsignedRange((config.alpha * u16::MAX as f32).round() as u64), + ); + } else if config.alpha != 1.0 { + // if we are missing the alpha property we can not display any transparent alpha values + return Err(Error::UnknownProperty { + handle: handle.into(), + name: "alpha", + }); + } + if let Ok(prop) = self.mapping.plane_prop_handle(handle, "FB_DAMAGE_CLIPS") { + if let Some(damage) = config.damage_clips.as_ref() { + self.request.add_property(handle, prop, *damage); + } else { + self.request.add_property(handle, prop, property::Value::Blob(0)); + } + } + if let Ok(prop) = self.mapping.plane_prop_handle(handle, "IN_FENCE_FD") { + if let Some(fence) = config.fence.as_ref().map(|f| f.as_raw_fd()) { + self.request + .add_property(handle, prop, property::Value::SignedRange(fence as i64)); + } else { + self.request + .add_property(handle, prop, property::Value::SignedRange(-1)); + } + } else if config.fence.is_some() { + return Err(Error::UnknownProperty { + handle: handle.into(), + name: "IN_FENCE_FD", + }); + } + } else { + self.reset_plane(handle)?; + } + + Ok(()) + } + + fn reset_plane(&mut self, plane: plane::Handle) -> Result<(), Error> { + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "CRTC_ID")?, + property::Value::CRTC(None), + ); + + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "FB_ID")?, + property::Value::Framebuffer(None), + ); + + // reset the plane properties + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "SRC_X")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(0u64), + ); + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "SRC_Y")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(0u64), + ); + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "SRC_W")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(0u64), + ); + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "SRC_H")?, + // these are 16.16. fixed point + property::Value::UnsignedRange(0u64), + ); + + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "CRTC_X")?, + property::Value::SignedRange(0i64), + ); + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "CRTC_Y")?, + property::Value::SignedRange(0i64), + ); + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "CRTC_W")?, + property::Value::UnsignedRange(0u64), + ); + self.request.add_property( + plane, + self.mapping.plane_prop_handle(plane, "CRTC_H")?, + property::Value::UnsignedRange(0u64), + ); + if let Ok(prop) = self.mapping.plane_prop_handle(plane, "rotation") { + self.request.add_property( + plane, + prop, + property::Value::Bitmask(DrmRotation::from(Transform::Normal).bits() as u64), + ); + } + if let Ok(prop) = self.mapping.plane_prop_handle(plane, "alpha") { + self.request + .add_property(plane, prop, property::Value::UnsignedRange(0xffff)); + } + if let Ok(prop) = self.mapping.plane_prop_handle(plane, "FB_DAMAGE_CLIPS") { + self.request.add_property(plane, prop, property::Value::Blob(0)); + } + if let Ok(prop) = self.mapping.plane_prop_handle(plane, "IN_FENCE_FD") { + self.request + .add_property(plane, prop, property::Value::SignedRange(-1)); + } + Ok(()) + } + + fn build(&self) -> Result { + Ok(self.request.clone()) + } +} + +impl<'a> AtomicRequest<'a> { + fn build_request( + mapping: &'a PropMapping, + crtc: crtc::Handle, + blob: Option>, + vrr: bool, + connectors: impl IntoIterator, + removed_connectors: impl IntoIterator, + planes: impl IntoIterator>, + ) -> Result, Error> { + let mut req = AtomicRequest::new(mapping); + + // requests consist out of a set of properties and their new values + // for different drm objects (crtc, plane, connector, ...). + + // for every connector that is new, we need to set our crtc_id + for conn in connectors { + req.set_connector(*conn, crtc)?; + } + + // for every connector that got removed, we need to set no crtc_id. + // (this is a bit problematic, because this means we need to remove, commit, add, commit + // in the right order to move a connector to another surface. otherwise we disable the + // the connector here again...) + for conn in removed_connectors { + req.reset_connector(*conn)?; + } + + // Set the crtc properties (active, mode_id, vrr_enabled). + req.set_crtc(crtc, blob, vrr)?; + + for plane_state in planes.into_iter() { + req.set_plane(crtc, plane_state)?; + } + + Ok(req) + } +} diff --git a/vendor/smithay/src/backend/drm/surface/gbm.rs b/vendor/smithay/src/backend/drm/surface/gbm.rs new file mode 100644 index 0000000..83b5ec5 --- /dev/null +++ b/vendor/smithay/src/backend/drm/surface/gbm.rs @@ -0,0 +1,582 @@ +use std::os::unix::io::AsFd; +use std::sync::Arc; + +use drm::control::{Mode, connector, crtc, plane}; +use drm::{Device, DriverCapability}; +use indexmap::IndexSet; + +use crate::backend::SwapBuffersError; +use crate::backend::allocator::dmabuf::{AsDmabuf, Dmabuf}; +use crate::backend::allocator::format::get_opaque; +use crate::backend::allocator::gbm::{GbmBuffer, GbmConvertError}; +use crate::backend::allocator::{Allocator, Format, Fourcc, Modifier, Slot, Swapchain}; +use crate::backend::drm::error::AccessError; +use crate::backend::drm::gbm::{GbmFramebuffer, framebuffer_from_bo}; +use crate::backend::drm::{DrmError, DrmSurface, plane_has_property}; +use crate::backend::renderer::sync::SyncPoint; +use crate::utils::{DevPath, Physical, Rectangle, Transform}; + +use tracing::{debug, info_span, instrument, trace, warn}; + +use super::{PlaneConfig, PlaneDamageClips, PlaneState, VrrSupport}; + +#[derive(Debug)] +struct QueuedFb { + slot: Slot, + sync: Option, + damage: Option>>, + user_data: U, +} + +/// Simplified abstraction of a swapchain for gbm-buffers displayed on a [`DrmSurface`]. +#[derive(Debug)] +pub struct GbmBufferedSurface + 'static, U> { + current_fb: Slot, + pending_fb: Option<(Slot, U)>, + queued_fb: Option>, + next_fb: Option>, + swapchain: Swapchain, + drm: Arc, + is_opaque: bool, + supports_fencing: bool, + span: tracing::Span, +} + +impl GbmBufferedSurface +where + A: Allocator, + A::Error: std::error::Error + Send + Sync, +{ + /// Create a new `GbmBufferedSurface` from a given compatible combination + /// of a surface, an allocator and renderer formats. + /// + /// The provided color_formats are tested in order until a working configuration is found. + /// + /// To successfully call this function, you need to have a renderer, + /// which can render into a Dmabuf, and a gbm allocator that can produce + /// buffers of a supported format for rendering. + pub fn new( + drm: DrmSurface, + mut allocator: A, + color_formats: &[Fourcc], + renderer_formats: impl IntoIterator, + ) -> Result, Error> { + let span = info_span!(parent: drm.span(), "drm_gbm"); + let _guard = span.enter(); + + let mut error = None; + let drm = Arc::new(drm); + let renderer_formats = renderer_formats.into_iter().collect::>(); + + for format in color_formats { + debug!("Testing color format: {}", format); + match Self::new_internal(drm.clone(), allocator, renderer_formats.clone(), *format) { + Ok((current_fb, swapchain, is_opaque)) => { + drop(_guard); + let supports_fencing = !drm.is_legacy() + && drm + .get_driver_capability(DriverCapability::SyncObj) + .map(|val| val != 0) + .map_err(|err| { + Error::DrmError(DrmError::Access(AccessError { + errmsg: "Failed to query driver capability", + dev: drm.dev_path(), + source: err, + })) + })? + && plane_has_property(&*drm, drm.plane(), "IN_FENCE_FD")?; + + return Ok(GbmBufferedSurface { + current_fb, + pending_fb: None, + queued_fb: None, + next_fb: None, + swapchain, + drm, + is_opaque, + supports_fencing, + span, + }); + } + Err((alloc, err)) => { + warn!("Preferred format {} not available: {:?}", format, err); + allocator = alloc; + error = Some(err); + } + } + } + Err(error.unwrap()) + } + + #[allow(clippy::type_complexity)] + fn new_internal( + drm: Arc, + allocator: A, + mut renderer_formats: Vec, + code: Fourcc, + ) -> Result<(Slot, Swapchain, bool), (A, Error)> { + // select a format + let mut plane_formats = drm.plane_info().formats.iter().copied().collect::>(); + let opaque_code = get_opaque(code).unwrap_or(code); + if !plane_formats + .iter() + .any(|fmt| fmt.code == code || fmt.code == opaque_code) + { + return Err((allocator, Error::NoSupportedPlaneFormat)); + } + plane_formats.retain(|fmt| fmt.code == code || fmt.code == opaque_code); + renderer_formats.retain(|fmt| fmt.code == code); + + let plane_modifiers = plane_formats + .iter() + .map(|fmt| fmt.modifier) + .collect::>(); + let renderer_modifiers = renderer_formats + .iter() + .map(|fmt| fmt.modifier) + .collect::>(); + + trace!("Plane formats: {:?}", plane_formats); + trace!("Renderer formats: {:?}", renderer_formats); + debug!( + "Remaining intersected modifiers: {:?}", + plane_modifiers + .intersection(&renderer_modifiers) + .collect::>() + ); + + if plane_formats.is_empty() { + return Err((allocator, Error::NoSupportedPlaneFormat)); + } else if renderer_formats.is_empty() { + return Err((allocator, Error::NoSupportedRendererFormat)); + } + + let formats = { + // Special case: if a format supports explicit LINEAR (but no implicit Modifiers) + // and the other doesn't support any modifier, force Implicit. + // This should at least result in a working pipeline possibly with a linear buffer, + // but we cannot be sure. + if (plane_formats.len() == 1 + && plane_formats.iter().next().unwrap().modifier == Modifier::Invalid + && renderer_formats.iter().all(|x| x.modifier != Modifier::Invalid) + && renderer_formats.iter().any(|x| x.modifier == Modifier::Linear)) + || (renderer_formats.len() == 1 + && renderer_formats.first().unwrap().modifier == Modifier::Invalid + && plane_formats.iter().all(|x| x.modifier != Modifier::Invalid) + && plane_formats.iter().any(|x| x.modifier == Modifier::Linear)) + { + vec![Format { + code, + modifier: Modifier::Invalid, + }] + } else { + plane_modifiers + .intersection(&renderer_modifiers) + .cloned() + .map(|modifier| Format { code, modifier }) + .collect::>() + } + }; + debug!("Testing Formats: {:?}", formats); + + let modifiers = formats.iter().map(|x| x.modifier).collect::>(); + let mode = drm.pending_mode(); + + let mut swapchain: Swapchain = Swapchain::new( + allocator, + mode.size().0 as u32, + mode.size().1 as u32, + code, + modifiers, + ); + + // Test format + let buffer = match swapchain.acquire() { + Ok(buffer) => buffer.unwrap(), + Err(err) => return Err((swapchain.allocator, Error::GbmError(err))), + }; + let format = Format { + code, + modifier: buffer.modifier(), // no guarantee + // that this is stable across allocations, but + // we want to print that here for debugging proposes. + // It has no further use. + }; + + let use_opaque = !plane_formats.iter().any(|f| f.code == code); + let fb = match framebuffer_from_bo(drm.device_fd(), &buffer, use_opaque) { + Ok(fb) => fb, + Err(err) => return Err((swapchain.allocator, Error::DrmError(err.into()))), + }; + match buffer.export() { + Ok(dmabuf) => dmabuf, + Err(err) => return Err((swapchain.allocator, err.into())), + }; + buffer.userdata().insert_if_missing_threadsafe(|| fb); + + let handle = buffer.userdata().get::().unwrap(); + + let plane_state = PlaneState { + handle: drm.plane(), + config: Some(PlaneConfig { + src: Rectangle::from_size((mode.size().0 as i32, mode.size().1 as i32).into()).to_f64(), + dst: Rectangle::from_size((mode.size().0 as i32, mode.size().1 as i32).into()), + alpha: 1.0, + transform: Transform::Normal, + damage_clips: None, + fb: *handle.as_ref(), + fence: None, + }), + }; + + match drm.test_state([plane_state], true) { + Ok(_) => { + debug!("Chosen format: {:?}", format); + Ok((buffer, swapchain, use_opaque)) + } + Err(err) => { + warn!( + "Mode-setting failed with automatically selected buffer format {:?}: {}", + format, err + ); + Err((swapchain.allocator, err.into())) + } + } + } + + /// Retrieves the next buffer to be rendered into and it's age. + /// + /// *Note*: This function can be called multiple times and + /// will return the same buffer until it is queued (see [`GbmBufferedSurface::queue_buffer`]). + #[instrument(level = "trace", skip_all, parent = &self.span, err)] + #[profiling::function] + pub fn next_buffer(&mut self) -> Result<(Dmabuf, u8), Error> { + if !self.drm.is_active() { + return Err(Error::::DrmError(DrmError::DeviceInactive)); + } + + if self.next_fb.is_none() { + let slot = self + .swapchain + .acquire() + .map_err(Error::GbmError)? + .ok_or(Error::NoFreeSlotsError)?; + + let maybe_buffer = slot.userdata().get::(); + if maybe_buffer.is_none() { + let fb = framebuffer_from_bo(self.drm.device_fd(), &slot, self.is_opaque) + .map_err(|err| Error::DrmError(err.into()))?; + slot.userdata().insert_if_missing_threadsafe(|| fb); + } + + self.next_fb = Some(slot); + } + + let slot = self.next_fb.as_ref().unwrap(); + Ok((slot.export()?, slot.age())) + } + + /// Queues the current buffer for rendering. + /// + /// Returns [`Error::NoBuffer`] in case [`GbmBufferedSurface::next_buffer`] has not been called + /// prior to this function. + /// + /// *Note*: This function needs to be followed up with [`GbmBufferedSurface::frame_submitted`] + /// when a vblank event is received, that denotes successful scanout of the buffer. + /// Otherwise the underlying swapchain will eventually run out of buffers. + /// + /// `user_data` can be used to attach some data to a specific buffer and later retrieved with [`GbmBufferedSurface::frame_submitted`] + #[profiling::function] + pub fn queue_buffer( + &mut self, + sync: Option, + damage: Option>>, + user_data: U, + ) -> Result<(), Error> { + if !self.drm.is_active() { + return Err(Error::::DrmError(DrmError::DeviceInactive)); + } + + let next_fb = self.next_fb.take().ok_or(Error::::NoBuffer)?; + + self.swapchain.submitted(&next_fb); + + self.queued_fb = Some(QueuedFb { + slot: next_fb, + sync, + damage, + user_data, + }); + if self.pending_fb.is_none() { + self.submit()?; + } + Ok(()) + } + + /// Marks the current frame as submitted. + /// + /// *Note*: Needs to be called, after the vblank event of the matching [`DrmDevice`](super::super::DrmDevice) + /// was received after calling [`GbmBufferedSurface::queue_buffer`] on this surface. + /// Otherwise the underlying swapchain will run out of buffers eventually. + /// + /// Returns the user data that was stored with [`GbmBufferedSurface::queue_buffer`] if a buffer was pending, otherwise + /// `None` is returned. + #[profiling::function] + pub fn frame_submitted(&mut self) -> Result, Error> { + if let Some((mut pending, user_data)) = self.pending_fb.take() { + std::mem::swap(&mut pending, &mut self.current_fb); + if self.queued_fb.is_some() { + self.submit()?; + } + Ok(Some(user_data)) + } else { + Ok(None) + } + } + + #[profiling::function] + fn submit(&mut self) -> Result<(), Error> { + // yes it does not look like it, but both of these lines should be safe in all cases. + let QueuedFb { + slot, + sync, + damage, + user_data, + } = self.queued_fb.take().unwrap(); + let handle = slot.userdata().get::().unwrap(); + let mode = self.drm.pending_mode(); + let src = Rectangle::from_size((mode.size().0 as i32, mode.size().1 as i32).into()).to_f64(); + let dst = Rectangle::from_size((mode.size().0 as i32, mode.size().1 as i32).into()); + + let damage_clips = damage.and_then(|damage| { + PlaneDamageClips::from_damage( + self.drm.device_fd(), + src, + dst, + Transform::Normal, + Transform::Normal, + damage, + ) + .ok() + .flatten() + }); + + // Try to extract a native fence out of the supplied sync point if any + // If the sync point has no native fence or the surface does not support + // fencing force a wait + let fence = if let Some(sync) = sync { + if !self.supports_fencing { + // we probably don't want to fail to submit in this case, lets hope on implicit sync + let _ = sync.wait(); + None + } else { + let fence = sync.export(); + + if fence.is_none() { + let _ = sync.wait(); + } + + fence + } + } else { + None + }; + + let plane_state = PlaneState { + handle: self.plane(), + config: Some(PlaneConfig { + src, + dst, + transform: Transform::Normal, + alpha: 1.0, + damage_clips: damage_clips.as_ref().map(|d| d.blob()), + fb: *handle.as_ref(), + fence: fence.as_ref().map(|fence| fence.as_fd()), + }), + }; + + let flip = if self.drm.commit_pending() { + self.drm.commit([plane_state], true) + } else { + self.drm.page_flip([plane_state], true) + }; + if flip.is_ok() { + self.pending_fb = Some((slot, user_data)); + } + flip.map_err(Error::DrmError) + } + + /// Reset the underlying buffers + pub fn reset_buffers(&mut self) { + self.swapchain.reset_buffers() + } + + /// Reset the age for all buffers. + /// + /// This can be used to efficiently clear the damage history without having to + /// modify the damage for each surface. + pub fn reset_buffer_ages(&mut self) { + self.swapchain.reset_buffer_ages(); + } + + /// Returns the underlying [`crtc`](drm::control::crtc) of this surface + pub fn crtc(&self) -> crtc::Handle { + self.drm.crtc() + } + + /// Returns the underlying [`plane`](drm::control::plane) of this surface + pub fn plane(&self) -> plane::Handle { + self.drm.plane() + } + + /// Currently used [`connector`](drm::control::connector)s of this `Surface` + pub fn current_connectors(&self) -> impl IntoIterator { + self.drm.current_connectors() + } + + /// Returns the pending [`connector`](drm::control::connector)s + /// used for the next frame queued via [`queue_buffer`](GbmBufferedSurface::queue_buffer). + pub fn pending_connectors(&self) -> impl IntoIterator { + self.drm.pending_connectors() + } + + /// Tries to add a new [`connector`](drm::control::connector) + /// to be used after the next commit. + /// + /// **Warning**: You need to make sure, that the connector is not used with another surface + /// or was properly removed via `remove_connector` + `commit` before adding it to another surface. + /// Behavior if failing to do so is undefined, but might result in rendering errors or the connector + /// getting removed from the other surface without updating it's internal state. + /// + /// Fails if the `connector` is not compatible with the underlying [`crtc`](drm::control::crtc) + /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) + /// or is not compatible with the currently pending + /// [`Mode`](drm::control::Mode). + pub fn add_connector(&self, connector: connector::Handle) -> Result<(), Error> { + self.drm.add_connector(connector).map_err(Error::DrmError) + } + + /// Tries to mark a [`connector`](drm::control::connector) + /// for removal on the next commit. + pub fn remove_connector(&self, connector: connector::Handle) -> Result<(), Error> { + self.drm.remove_connector(connector).map_err(Error::DrmError) + } + + /// Tries to replace the current connector set with the newly provided one on the next commit. + /// + /// Fails if one new `connector` is not compatible with the underlying [`crtc`](drm::control::crtc) + /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) + /// or is not compatible with the currently pending + /// [`Mode`](drm::control::Mode). + pub fn set_connectors(&self, connectors: &[connector::Handle]) -> Result<(), Error> { + self.drm.set_connectors(connectors).map_err(Error::DrmError) + } + + /// Returns the currently active [`Mode`](drm::control::Mode) + /// of the underlying [`crtc`](drm::control::crtc) + pub fn current_mode(&self) -> Mode { + self.drm.current_mode() + } + + /// Returns the currently pending [`Mode`](drm::control::Mode) + /// to be used after the next commit. + pub fn pending_mode(&self) -> Mode { + self.drm.pending_mode() + } + + /// Tries to set a new [`Mode`](drm::control::Mode) + /// to be used after the next commit. + /// + /// Fails if the mode is not compatible with the underlying + /// [`crtc`](drm::control::crtc) or any of the + /// pending [`connector`](drm::control::connector)s. + pub fn use_mode(&mut self, mode: Mode) -> Result<(), Error> { + self.drm.use_mode(mode).map_err(Error::DrmError)?; + let (w, h) = mode.size(); + self.swapchain.resize(w as _, h as _); + Ok(()) + } + + /// Returns if Variable Refresh Rate is advertised as supported by the given connector. + /// + /// See [`DrmSurface::vrr_supported`] for more details. + pub fn vrr_supported(&self, conn: connector::Handle) -> Result> { + self.drm.vrr_supported(conn).map_err(Error::DrmError) + } + + /// Returns whether the next frame state would set Variable Refresh Rate as enabled. + /// + /// See [`DrmSurface::vrr_enabled`] for more details. + pub fn vrr_enabled(&self) -> bool { + self.drm.vrr_enabled() + } + + /// Tries to set Variable Refresh Rate (VRR) for the next frame. + // + /// Doing so might cause the next frame to trigger a modeset. + /// Check [`GbmBufferedSurface::vrr_supported`], which indicates if VRR can be + /// used without a modeset on the attached connectors./ + pub fn use_vrr(&self, vrr: bool) -> Result<(), Error> { + self.drm.use_vrr(vrr).map_err(Error::DrmError) + } + + /// Returns a reference to the underlying drm surface + pub fn surface(&self) -> &DrmSurface { + &self.drm + } + + /// Get the format of the underlying swapchain + pub fn format(&self) -> Fourcc { + self.swapchain.format() + } +} + +/// Errors thrown by a [`GbmBufferedSurface`] +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// No supported pixel format for the given plane could be determined + #[error("No supported plane buffer format found")] + NoSupportedPlaneFormat, + /// No supported pixel format for the given renderer could be determined + #[error("No supported renderer buffer format found")] + NoSupportedRendererFormat, + /// The supported pixel formats of the renderer and plane are incompatible + #[error("Supported plane and renderer buffer formats are incompatible")] + FormatsNotCompatible, + /// The swapchain is exhausted, you need to call `frame_submitted` + #[error("Failed to allocate a new buffer")] + NoFreeSlotsError, + /// Failed to renderer using the given renderer + #[error("Failed to render test frame")] + InitialRenderingError, + /// Error accessing the drm device + #[error("The underlying drm surface encountered an error: {0}")] + DrmError(#[from] DrmError), + /// Error importing the rendered buffer to libgbm for scan-out + #[error("The underlying gbm device encountered an error: {0}")] + GbmError(#[source] E), + /// Error exporting as Dmabuf + #[error("The allocated buffer could not be exported as a dmabuf: {0}")] + AsDmabufError(#[from] GbmConvertError), + /// No buffer to queue + #[error("No buffer has been acquired to get queued")] + NoBuffer, +} + +impl From> for SwapBuffersError { + #[inline] + fn from(err: Error) -> SwapBuffersError { + match err { + x @ Error::NoSupportedPlaneFormat + | x @ Error::NoSupportedRendererFormat + | x @ Error::FormatsNotCompatible + | x @ Error::InitialRenderingError => SwapBuffersError::ContextLost(Box::new(x)), + x @ Error::NoFreeSlotsError | x @ Error::NoBuffer => { + SwapBuffersError::TemporaryFailure(Box::new(x)) + } + Error::DrmError(err) => err.into(), + Error::GbmError(err) => SwapBuffersError::ContextLost(Box::new(err)), + Error::AsDmabufError(err) => SwapBuffersError::ContextLost(Box::new(err)), + } + } +} diff --git a/vendor/smithay/src/backend/drm/surface/legacy.rs b/vendor/smithay/src/backend/drm/surface/legacy.rs new file mode 100644 index 0000000..8184acb --- /dev/null +++ b/vendor/smithay/src/backend/drm/surface/legacy.rs @@ -0,0 +1,515 @@ +use drm::control::{Device as ControlDevice, Mode, PageFlipFlags, connector, crtc, encoder, framebuffer}; + +use std::collections::HashSet; +use std::sync::{ + Arc, Mutex, RwLock, + atomic::{AtomicBool, Ordering}, +}; + +use crate::backend::drm::error::AccessError; +use crate::{ + backend::drm::{ + DrmDeviceFd, device::DrmDeviceInternal, device::legacy::set_connector_state, error::Error, + }, + utils::DevPath, +}; + +use tracing::{debug, info, info_span, instrument, trace}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct State { + pub mode: Mode, + pub connectors: HashSet, +} + +impl State { + fn current_state(fd: &A, crtc: crtc::Handle) -> Result { + // Try to enumerate the current state to set the initial state variable correctly. + // We need an accurate state to handle `commit_pending`. + let crtc_info = fd.get_crtc(crtc).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading crtc info", + dev: fd.dev_path(), + source, + }) + })?; + + let current_mode = crtc_info.mode(); + + let mut current_connectors = HashSet::new(); + let res_handles = fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading drm resources", + dev: fd.dev_path(), + source, + }) + })?; + for &con in res_handles.connectors() { + let con_info = fd.get_connector(con, false).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading connector info", + dev: fd.dev_path(), + source, + }) + })?; + if let Some(enc) = con_info.current_encoder() { + let enc_info = fd.get_encoder(enc).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading encoder info", + dev: fd.dev_path(), + source, + }) + })?; + if let Some(current_crtc) = enc_info.crtc() { + if crtc == current_crtc { + current_connectors.insert(con); + } + } + } + } + + // If we have no current mode, we create a fake one, which will not match (and thus gets overridden on the commit below). + // A better fix would probably be making mode an `Option`, but that would mean + // we need to be sure, we require a mode to always be set without relying on the compiler. + // So we cheat, because it works and is easier to handle later. + Ok(State { + mode: current_mode.unwrap_or_else(|| unsafe { std::mem::zeroed() }), + connectors: current_connectors, + }) + } +} + +#[derive(Debug)] +pub struct LegacyDrmSurface { + pub(super) fd: Arc, + pub(super) active: Arc, + crtc: crtc::Handle, + state: RwLock, + pending: RwLock, + dpms: Mutex, + pub(super) span: tracing::Span, +} + +impl LegacyDrmSurface { + pub fn new( + fd: Arc, + active: Arc, + crtc: crtc::Handle, + mode: Mode, + connectors: &[connector::Handle], + ) -> Result { + let span = info_span!("drm_legacy", crtc = ?crtc); + let _guard = span.enter(); + info!(?mode, ?connectors, ?crtc, "Initializing drm surface",); + + let state = State::current_state(&*fd, crtc)?; + let pending = State { + mode, + connectors: connectors.iter().copied().collect(), + }; + + drop(_guard); + let surface = LegacyDrmSurface { + fd, + active, + crtc, + state: RwLock::new(state), + pending: RwLock::new(pending), + dpms: Mutex::new(true), + span, + }; + + Ok(surface) + } + + pub fn current_connectors(&self) -> HashSet { + self.state.read().unwrap().connectors.clone() + } + + pub fn pending_connectors(&self) -> HashSet { + self.pending.read().unwrap().connectors.clone() + } + + pub fn current_mode(&self) -> Mode { + self.state.read().unwrap().mode + } + + pub fn pending_mode(&self) -> Mode { + self.pending.read().unwrap().mode + } + + #[instrument(parent = &self.span, skip(self))] + pub fn add_connector(&self, conn: connector::Handle) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mut pending = self.pending.write().unwrap(); + + if self.check_connector(conn, &pending.mode)? { + pending.connectors.insert(conn); + } + + Ok(()) + } + + #[instrument(parent = &self.span, skip(self))] + pub fn remove_connector(&self, connector: connector::Handle) -> Result<(), Error> { + let mut pending = self.pending.write().unwrap(); + + if pending.connectors.contains(&connector) && pending.connectors.len() == 1 { + return Err(Error::SurfaceWithoutConnectors(self.crtc)); + } + + pending.connectors.remove(&connector); + Ok(()) + } + + #[instrument(parent = &self.span, skip(self))] + pub fn set_connectors(&self, connectors: &[connector::Handle]) -> Result<(), Error> { + if connectors.is_empty() { + return Err(Error::SurfaceWithoutConnectors(self.crtc)); + } + + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mut pending = self.pending.write().unwrap(); + + if connectors + .iter() + .map(|conn| self.check_connector(*conn, &pending.mode)) + .collect::, _>>()? + .iter() + .all(|v| *v) + { + pending.connectors = connectors.iter().cloned().collect(); + } + + Ok(()) + } + + #[instrument(level = "debug", parent = &self.span, skip(self))] + pub fn use_mode(&self, mode: Mode) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mut pending = self.pending.write().unwrap(); + + // check the connectors to see if this mode is supported + for connector in &pending.connectors { + if !self + .fd + .get_connector(*connector, false) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading connector info", + dev: self.fd.dev_path(), + source, + }) + })? + .modes() + .contains(&mode) + { + return Err(Error::ModeNotSuitable(mode)); + } + } + + pending.mode = mode; + + Ok(()) + } + + pub fn commit_pending(&self) -> bool { + *self.pending.read().unwrap() != *self.state.read().unwrap() + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn commit(&self, framebuffer: framebuffer::Handle, event: bool) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let mut current = self.state.write().unwrap(); + let pending = self.pending.read().unwrap(); + let mut dpms = self.dpms.lock().unwrap(); + + if !*dpms { + let connectors = current.connectors.intersection(&pending.connectors); + set_connector_state(&*self.fd, connectors.copied(), true)?; + *dpms = true; + } + + { + let removed = current.connectors.difference(&pending.connectors); + let added = pending.connectors.difference(¤t.connectors); + + let mut conn_removed = false; + for conn in removed.clone() { + if let Ok(info) = self.fd.get_connector(*conn, false) { + info!("Removing connector: {:?}", info.interface()); + } else { + info!("Removing unknown connector"); + } + // if the connector was mapped to our crtc, we need to ack the disconnect. + // the graphics pipeline will not be freed otherwise + conn_removed = true; + } + set_connector_state(&*self.fd, removed.copied(), false)?; + + if conn_removed { + // null commit (necessary to trigger removal on the kernel side with the legacy api.) + self.fd + .set_crtc(self.crtc, None, (0, 0), &[], None) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error setting crtc", + dev: self.fd.dev_path(), + source, + }) + })?; + } + + for conn in added.clone() { + if let Ok(info) = self.fd.get_connector(*conn, false) { + info!("Adding connector: {:?}", info.interface()); + } else { + info!("Adding unknown connector"); + } + } + set_connector_state(&*self.fd, added.copied(), true)?; + + if current.mode != pending.mode { + info!("Setting new mode: {:?}", pending.mode.name()); + } + } + + debug!("Setting screen"); + // do a modeset and attach the given framebuffer + self.fd + .set_crtc( + self.crtc, + Some(framebuffer), + (0, 0), + &pending + .connectors + .iter() + .copied() + .collect::>(), + Some(pending.mode), + ) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Error setting crtc", + dev: self.fd.dev_path(), + source, + }) + })?; + + *current = pending.clone(); + + if event { + // set crtc does not trigger page_flip events, so we immediately queue a flip + // with the same framebuffer. + // this will result in wasting a frame, because this flip will need to wait + // for `set_crtc`, but is necessary to drive the event loop and thus provide + // a more consistent api. + ControlDevice::page_flip(&*self.fd, self.crtc, framebuffer, PageFlipFlags::EVENT, None).map_err( + |source| { + Error::Access(AccessError { + errmsg: "Failed to queue page flip", + dev: self.fd.dev_path(), + source, + }) + }, + )?; + } + + Ok(()) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn page_flip(&self, framebuffer: framebuffer::Handle, event: bool) -> Result<(), Error> { + trace!("Queueing Page flip"); + + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let current = self.state.read().unwrap(); + let mut dpms = self.dpms.lock().unwrap(); + if !*dpms { + set_connector_state(&*self.fd, current.connectors.iter().copied(), true)?; + *dpms = true; + } + + ControlDevice::page_flip( + &*self.fd, + self.crtc, + framebuffer, + if event { + PageFlipFlags::EVENT + } else { + PageFlipFlags::empty() + }, + None, + ) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to page flip", + dev: self.fd.dev_path(), + source, + }) + }) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn test_buffer(&self, fb: framebuffer::Handle, mode: &Mode) -> Result<(), Error> { + if !self.active.load(Ordering::SeqCst) { + return Err(Error::DeviceInactive); + } + + let pending = self.pending.read().unwrap(); + + debug!("Setting screen for buffer *testing*"); + self.fd + .set_crtc( + self.crtc, + Some(fb), + (0, 0), + &pending + .connectors + .iter() + .copied() + .collect::>(), + Some(*mode), + ) + .map_err(|source| { + Error::Access(AccessError { + errmsg: "Failed to test buffer", + dev: self.fd.dev_path(), + source, + }) + }) + } + + // we use this function to verify, if a certain connector/mode combination + // is valid on our crtc. We do this with the most basic information we have: + // - is there a matching encoder + // - does the connector support the provided Mode. + // + // Better would be some kind of test commit to ask the driver, + // but that only exists for the atomic api. + fn check_connector(&self, conn: connector::Handle, mode: &Mode) -> Result { + let info = self.fd.get_connector(conn, false).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading connector info", + dev: self.fd.dev_path(), + source, + }) + })?; + + // check if the connector can handle the current mode + if info.modes().contains(mode) { + // check if there is a valid encoder + let encoders = info + .encoders() + .iter() + .map(|encoder| { + self.fd.get_encoder(*encoder).map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading encoder info", + dev: self.fd.dev_path(), + source, + }) + }) + }) + .collect::, _>>()?; + + // and if any encoder supports the selected crtc + let resource_handles = self.fd.resource_handles().map_err(|source| { + Error::Access(AccessError { + errmsg: "Error loading resources", + dev: self.fd.dev_path(), + source, + }) + })?; + if !encoders + .iter() + .map(|encoder| encoder.possible_crtcs()) + .all(|crtc_list| resource_handles.filter_crtcs(crtc_list).contains(&self.crtc)) + { + Ok(false) + } else { + Ok(true) + } + } else { + Ok(false) + } + } + + pub(crate) fn reset_state( + &self, + fd: Option<&B>, + ) -> Result<(), Error> { + *self.state.write().unwrap() = if let Some(fd) = fd { + State::current_state(fd, self.crtc)? + } else { + State::current_state(&*self.fd, self.crtc)? + }; + Ok(()) + } + + pub(crate) fn device_fd(&self) -> &DrmDeviceFd { + self.fd.device_fd() + } + + pub fn clear(&self) -> Result<(), Error> { + let current = self.state.read().unwrap(); + let mut dpms = self.dpms.lock().unwrap(); + if *dpms { + set_connector_state(&*self.fd, current.connectors.iter().copied(), false)?; + *dpms = false; + } + Ok(()) + } +} + +impl Drop for LegacyDrmSurface { + fn drop(&mut self) { + let _guard = self.span.enter(); + // ignore failure at this point + + if !self.active.load(Ordering::SeqCst) { + // the device is gone or we are on another tty + // old state has been restored, we shouldn't touch it. + // if we are on another tty the connectors will get disabled + // by the device, when switching back + return; + } + + // disable connectors again + let current = self.state.read().unwrap(); + if set_connector_state(&*self.fd, current.connectors.iter().copied(), false).is_ok() { + // null commit + let _ = self.fd.set_crtc(self.crtc, None, (0, 0), &[], None); + } + } +} + +#[cfg(test)] +mod test { + use super::LegacyDrmSurface; + + fn is_send() {} + + #[test] + fn surface_is_send() { + is_send::(); + } +} diff --git a/vendor/smithay/src/backend/drm/surface/mod.rs b/vendor/smithay/src/backend/drm/surface/mod.rs new file mode 100644 index 0000000..677fa13 --- /dev/null +++ b/vendor/smithay/src/backend/drm/surface/mod.rs @@ -0,0 +1,555 @@ +use std::io; +use std::os::unix::io::{AsFd, BorrowedFd}; +use std::sync::Arc; +use std::sync::atomic::Ordering; + +use drm::Device as BasicDevice; +use drm::control::{Device as ControlDevice, Mode, connector, crtc, framebuffer, plane}; + +use libc::dev_t; + +pub(super) mod atomic; +#[cfg(feature = "backend_gbm")] +pub(super) mod gbm; +pub(super) mod legacy; +use super::{ + DrmDeviceFd, PlaneClaim, PlaneInfo, PlaneType, Planes, device::PlaneClaimStorage, error::Error, + plane_type, +}; +use crate::utils::DevPath; +use crate::utils::{Buffer, Physical, Point, Rectangle, Transform}; +use atomic::AtomicDrmSurface; +use legacy::LegacyDrmSurface; + +/// An open crtc + plane combination that can be used for scan-out +#[derive(Debug)] +pub struct DrmSurface { + // This field is only read when 'backend_session' is enabled + #[allow(dead_code)] + pub(super) dev_id: dev_t, + pub(super) crtc: crtc::Handle, + pub(super) planes: Planes, + pub(super) internal: Arc, + pub(super) plane_claim_storage: PlaneClaimStorage, + pub(super) primary_plane: (PlaneInfo, PlaneClaim), +} + +#[derive(Debug)] +struct PlaneDamageInner { + drm: DrmDeviceFd, + blob: Option>, +} + +impl Drop for PlaneDamageInner { + fn drop(&mut self) { + // There is nothing we can do if that fails + if let Some(drm::control::property::Value::Blob(id)) = self.blob.take() { + let _ = self.drm.destroy_property_blob(id); + } + } +} + +#[derive(Debug)] +/// Helper for `FB_DAMAGE_CLIPS` +pub struct PlaneDamageClips { + inner: Arc, +} + +impl PlaneDamageClips { + /// Returns the underlying blob + pub fn blob(&self) -> drm::control::property::Value<'_> { + self.inner.blob.unwrap() + } +} + +impl PlaneDamageClips { + /// Initialize damage clips for a a plane + #[profiling::function] + pub fn from_damage( + device: &DrmDeviceFd, + src: Rectangle, + dst: Rectangle, + dst_transform: Transform, + damage_transform: Transform, + damage: impl IntoIterator>, + ) -> io::Result> { + let scale = src.size / dst.size.to_logical(1).to_buffer(1, dst_transform).to_f64(); + + let mut rects = damage + .into_iter() + .map(|rect| { + let mut rect = rect + .to_f64() + .to_logical(1f64) + .to_buffer( + 1f64, + damage_transform, + &src.size.to_logical(scale, damage_transform), + ) + .upscale(scale); + rect.loc += src.loc; + let rect = rect.to_i32_up(); + + drm_ffi::drm_mode_rect { + x1: rect.loc.x, + y1: rect.loc.y, + x2: rect.loc.x.saturating_add(rect.size.w), + y2: rect.loc.y.saturating_add(rect.size.h), + } + }) + .collect::>(); + + if rects.is_empty() { + return Ok(None); + } + + let data = unsafe { + std::slice::from_raw_parts_mut( + rects.as_mut_ptr() as *mut u8, + std::mem::size_of::() * rects.len(), + ) + }; + + let blob = drm_ffi::mode::create_property_blob(device.as_fd(), data)?; + + Ok(Some(PlaneDamageClips { + inner: Arc::new(PlaneDamageInner { + drm: device.clone(), + blob: Some(drm::control::property::Value::Blob(blob.blob_id as u64)), + }), + })) + } +} + +impl Clone for PlaneDamageClips { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +/// State of a single plane +#[derive(Debug, Clone)] +pub struct PlaneState<'a> { + /// Handle of the plane + pub handle: plane::Handle, + /// Configuration for that plane + /// + /// Can be `None` if nothing is attached + pub config: Option>, +} + +/// Configuration for a single plane +#[derive(Debug, Clone)] +pub struct PlaneConfig<'a> { + /// Source [`Rectangle`] of the attached framebuffer + pub src: Rectangle, + /// Destination [`Rectangle`] on the CRTC + pub dst: Rectangle, + /// Transform for the attached framebuffer + pub transform: Transform, + /// Alpha value for the plane + pub alpha: f32, + /// Damage clips of the attached framebuffer + pub damage_clips: Option>, + /// Framebuffer handle + pub fb: framebuffer::Handle, + /// Optional fence + pub fence: Option>, +} + +/// VRR support state +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum VrrSupport { + /// VRR is generally not supported + NotSupported, + /// VRR is supported, but changing it requires a modeset + RequiresModeset, + /// VRR is supported and can be changed without a modeset + Supported, +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum DrmSurfaceInternal { + Atomic(AtomicDrmSurface), + Legacy(LegacyDrmSurface), +} + +impl AsFd for DrmSurface { + fn as_fd(&self) -> BorrowedFd<'_> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.fd.as_fd(), + DrmSurfaceInternal::Legacy(surf) => surf.fd.as_fd(), + } + } +} +impl BasicDevice for DrmSurface {} +impl ControlDevice for DrmSurface {} + +impl DrmSurface { + /// Returns the underlying [`DrmDeviceFd`] + pub fn device_fd(&self) -> &DrmDeviceFd { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.device_fd(), + DrmSurfaceInternal::Legacy(surf) => surf.device_fd(), + } + } + + /// Returns whether this surface is using the legacy api + pub fn is_legacy(&self) -> bool { + matches!(&*self.internal, DrmSurfaceInternal::Legacy(_)) + } + + /// Returns the underlying [`crtc`](drm::control::crtc) of this surface + pub fn crtc(&self) -> crtc::Handle { + self.crtc + } + + /// Returns the underlying primary [`plane`](drm::control::plane) of this surface + pub fn plane(&self) -> plane::Handle { + self.primary_plane.0.handle + } + + /// Returns the [`PlaneInfo`] of the underlying primary [`plane`](drm::control::plane) of this surface + pub fn plane_info(&self) -> &PlaneInfo { + &self.primary_plane.0 + } + + /// Currently used [`connector`](drm::control::connector)s of this surface + pub fn current_connectors(&self) -> impl IntoIterator { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.current_connectors(), + DrmSurfaceInternal::Legacy(surf) => surf.current_connectors(), + } + } + + /// Returns the pending [`connector`](drm::control::connector)s + /// used after the next [`commit`](DrmSurface::commit) of this surface + pub fn pending_connectors(&self) -> impl IntoIterator { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.pending_connectors(), + DrmSurfaceInternal::Legacy(surf) => surf.pending_connectors(), + } + } + + /// Tries to add a new [`connector`](drm::control::connector) + /// to be used after the next commit. + /// + /// **Warning**: You need to make sure, that the connector is not used with another surface + /// or was properly removed via `remove_connector` + `commit` before adding it to another surface. + /// Behavior if failing to do so is undefined, but might result in rendering errors or the connector + /// getting removed from the other surface without updating it's internal state. + /// + /// Fails if the `connector` is not compatible with the underlying [`crtc`](drm::control::crtc) + /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) + /// or is not compatible with the currently pending + /// [`Mode`](drm::control::Mode). + pub fn add_connector(&self, connector: connector::Handle) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.add_connector(connector), + DrmSurfaceInternal::Legacy(surf) => surf.add_connector(connector), + } + } + + /// Tries to mark a [`connector`](drm::control::connector) + /// for removal on the next commit. + pub fn remove_connector(&self, connector: connector::Handle) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.remove_connector(connector), + DrmSurfaceInternal::Legacy(surf) => surf.remove_connector(connector), + } + } + + /// Tries to replace the current connector set with the newly provided one on the next commit. + /// + /// Fails if one new `connector` is not compatible with the underlying [`crtc`](drm::control::crtc) + /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) + /// or is not compatible with the currently pending + /// [`Mode`](drm::control::Mode). + pub fn set_connectors(&self, connectors: &[connector::Handle]) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.set_connectors(connectors), + DrmSurfaceInternal::Legacy(surf) => surf.set_connectors(connectors), + } + } + + /// Returns the currently active [`Mode`](drm::control::Mode) + /// of the underlying [`crtc`](drm::control::crtc) + pub fn current_mode(&self) -> Mode { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.current_mode(), + DrmSurfaceInternal::Legacy(surf) => surf.current_mode(), + } + } + + /// Returns the currently pending [`Mode`](drm::control::Mode) + /// to be used after the next commit. + pub fn pending_mode(&self) -> Mode { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.pending_mode(), + DrmSurfaceInternal::Legacy(surf) => surf.pending_mode(), + } + } + + /// Tries to set a new [`Mode`](drm::control::Mode) + /// to be used after the next commit. + /// + /// Fails if the mode is not compatible with the underlying + /// [`crtc`](drm::control::crtc) or any of the + /// pending [`connector`](drm::control::connector)s. + pub fn use_mode(&self, mode: Mode) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.use_mode(mode), + DrmSurfaceInternal::Legacy(surf) => surf.use_mode(mode), + } + } + + /// Returns if Variable Refresh Rate is advertised as supported by the given connector. + /// + /// Note: This will always return [`VrrSupport::NotSupported`] if the underlying + /// implementation is using the legacy DRM api. + pub fn vrr_supported(&self, conn: connector::Handle) -> Result { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.vrr_supported(conn), + DrmSurfaceInternal::Legacy(_) => Ok(VrrSupport::NotSupported), + } + } + + /// Returns whether the next frame state would set Variable Refresh Rate as enabled. + /// + /// Note: This will always return `false` if the underlying implementation is using the + /// legacy DRM api. + pub fn vrr_enabled(&self) -> bool { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.vrr_enabled(), + DrmSurfaceInternal::Legacy(_) => false, + } + } + + /// Tries to set Variable Refresh Rate (VRR) for the next frame. + /// + /// Doing so might cause [`DrmSurface::commit_pending`] to return `true`. + /// Check [`DrmSurface::vrr_supported`], which indicates if VRR can be + /// used without a modeset on the attached connectors. + /// + /// Disabling VRR on legacy DRM is a no-op. Enabling VRR returns [`Error::UnknownProperty`]. + pub fn use_vrr(&self, vrr: bool) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.use_vrr(vrr), + DrmSurfaceInternal::Legacy(_) => { + if vrr { + Err(Error::UnknownProperty { + handle: self.crtc.into(), + name: "VRR_ENABLED", + }) + } else { + Ok(()) + } + } + } + } + + /// Disables the given plane. + /// + /// Errors if the plane is not supported by this crtc or if the underlying + /// implementation does not support the use of planes. + pub fn clear_plane(&self, plane: plane::Handle) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.clear_plane(plane), + DrmSurfaceInternal::Legacy(_) => Err(Error::NonPrimaryPlane(plane)), + } + } + + /// Returns true whenever any state changes are pending to be committed + /// + /// The following functions may trigger a pending commit: + /// - [`add_connector`](DrmSurface::add_connector) + /// - [`remove_connector`](DrmSurface::remove_connector) + /// - [`use_mode`](DrmSurface::use_mode) + pub fn commit_pending(&self) -> bool { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.commit_pending(), + DrmSurfaceInternal::Legacy(surf) => surf.commit_pending(), + } + } + + /// Test a state given a set of framebuffers. + /// + /// *Note*: This will always return `Ok` for legacy devices if `allow_modeset = false`. + /// The legacy drm api has no way to test a buffer without triggering a modeset. + #[profiling::function] + pub fn test_state<'a>( + &self, + planes: impl IntoIterator>, + allow_modeset: bool, + ) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.test_state(planes, allow_modeset), + DrmSurfaceInternal::Legacy(surf) => { + let fb = ensure_legacy_planes(self, planes)?; + + if allow_modeset { + surf.test_buffer(fb, &self.pending_mode()) + } else { + // Legacy can not test a buffer without triggering a modeset, so we can + // only assume it works and hope for the best. A later call to commit or + // page_flip will show the correct result + Ok(()) + } + } + } + } + + /// Commit the pending state rendering a given set of framebuffers. + /// + /// *Note*: This will trigger a full modeset on the underlying device, + /// potentially causing some flickering. Check before performing this + /// operation if a commit really is necessary using [`commit_pending`](DrmSurface::commit_pending). + /// + /// This operation is not necessarily blocking until the crtc is in the desired state, + /// but will trigger a `vblank` event once done. + /// Make sure to have the device registered in your event loop prior to invoking this, to not miss + /// any generated event. + #[profiling::function] + pub fn commit<'a>( + &self, + planes: impl IntoIterator>, + event: bool, + ) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.commit(planes, event), + DrmSurfaceInternal::Legacy(surf) => { + let fb = ensure_legacy_planes(self, planes)?; + surf.commit(fb, event) + } + } + } + + /// Page-flip the underlying [`crtc`](drm::control::crtc) + /// to a new given set of [`framebuffer`]s. + /// + /// This will not cause the crtc to modeset. + /// + /// This operation is not blocking and will produce a `vblank` event once swapping is done. + /// Make sure to have the device registered in your event loop to not miss the event. + #[profiling::function] + pub fn page_flip<'a>( + &self, + planes: impl IntoIterator>, + event: bool, + ) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.page_flip(planes, event), + DrmSurfaceInternal::Legacy(surf) => { + let fb = ensure_legacy_planes(self, planes)?; + surf.page_flip(fb, event) + } + } + } + + /// Returns a set of available planes for this surface + pub fn planes(&self) -> &Planes { + &self.planes + } + + /// Claim a plane so that it won't be used by a different crtc + /// + /// Returns `None` if the plane could not be claimed + pub fn claim_plane(&self, plane: plane::Handle) -> Option { + // Validate that we are called with an plane that belongs to us + if self.planes.primary.iter().any(|p| p.handle == plane) + || self.planes.cursor.iter().any(|p| p.handle == plane) + || self.planes.overlay.iter().any(|p| p.handle == plane) + { + self.plane_claim_storage.claim(plane, self.crtc) + } else { + None + } + } + + /// Re-evaluates the current state of the crtc. + /// + /// It is recommended to call this function after this used [`Session`](crate::backend::session::Session) + /// gets re-activated / VT switched to. + /// + /// Usually you do not need to call this in other circumstances, but if + /// the state of the crtc is modified elsewhere, you may call this function + /// to reset it's internal state. + pub fn reset_state(&self) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.reset_state::(None), + DrmSurfaceInternal::Legacy(surf) => surf.reset_state::(None), + } + } + + /// Returns if the underlying device is currently paused or not. + pub fn is_active(&self) -> bool { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.active.load(Ordering::SeqCst), + DrmSurfaceInternal::Legacy(surf) => surf.active.load(Ordering::SeqCst), + } + } + + #[cfg(feature = "backend_gbm")] + pub(super) fn span(&self) -> &tracing::Span { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => &surf.span, + DrmSurfaceInternal::Legacy(surf) => &surf.span, + } + } + + /// Clear the surface, setting DPMS state to off and disabling all planes. + /// + /// The surface will be re-enabled on the next [`page_flip`][Self::page_flip] or + /// [`commit`][Self::commit]. + pub fn clear(&self) -> Result<(), Error> { + match &*self.internal { + DrmSurfaceInternal::Atomic(surf) => surf.clear(), + DrmSurfaceInternal::Legacy(surf) => surf.clear(), + } + } +} + +fn ensure_legacy_planes<'a>( + dev: &(impl ControlDevice + DevPath), + planes: impl IntoIterator>, +) -> Result { + let state = planes.into_iter().next().ok_or(Error::NoPlane)?; + + if plane_type(dev, state.handle)? != PlaneType::Primary { + return Err(Error::NonPrimaryPlane(state.handle)); + } + + let Some(config) = state.config else { + // we need a config on the primary plane + return Err(Error::NoFramebuffer(state.handle)); + }; + + if config.dst.loc != Point::default() { + // legacy does not support crtc position (technically we could do it, + // but the position can only be changed by commit, not by page-flip, + // so we just not allow it) + return Err(Error::UnsupportedPlaneConfiguration(state.handle)); + } + + if config.src.loc != Point::default() + || config + .src + .size + .to_logical(1.0, Transform::Normal) + .to_physical(1.0) + != config.dst.size.to_f64() + { + // legacy does not support crop nor scale + return Err(Error::UnsupportedPlaneConfiguration(state.handle)); + } + + if config.transform != Transform::Normal { + // legacy does not support transform + return Err(Error::UnsupportedPlaneConfiguration(state.handle)); + } + + Ok(config.fb) +} diff --git a/vendor/smithay/src/backend/egl/context.rs b/vendor/smithay/src/backend/egl/context.rs new file mode 100644 index 0000000..3e534d4 --- /dev/null +++ b/vendor/smithay/src/backend/egl/context.rs @@ -0,0 +1,650 @@ +//! EGL context related structs +use std::{ + os::raw::c_int, + sync::{Arc, atomic::Ordering}, +}; + +use libc::c_void; + +use super::{EGLError, Error, MakeCurrentError, ffi, wrap_egl_call_bool, wrap_egl_call_ptr}; +use crate::{ + backend::{ + allocator::format::FormatSet, + egl::{ + EGLSurface, + display::{EGLDisplay, PixelFormat}, + }, + }, + utils::user_data::UserDataMap, +}; + +use tracing::{info, info_span, instrument, trace, warn}; + +/// EGL context for rendering +#[derive(Debug)] +pub struct EGLContext { + context: ffi::egl::types::EGLContext, + display: EGLDisplay, + config_id: ffi::egl::types::EGLConfig, + pixel_format: Option, + user_data: Arc, + externally_managed: bool, + pub(crate) span: tracing::Span, +} + +/// Defines the priority for an [`EGLContext`] +/// +/// see: +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ContextPriority { + /// High priority + /// + /// Note: This might require special system privileges like `CAP_SYS_NICE` to succeed. + High, + /// Medium priority + /// + /// Default if not specified + #[default] + Medium, + /// Low priority + Low, +} + +impl TryFrom for ContextPriority { + type Error = EGLError; + + #[inline] + fn try_from(value: ffi::egl::types::EGLenum) -> Result { + let priority = match value { + ffi::egl::CONTEXT_PRIORITY_HIGH_IMG => ContextPriority::High, + ffi::egl::CONTEXT_PRIORITY_MEDIUM_IMG => ContextPriority::Medium, + ffi::egl::CONTEXT_PRIORITY_LOW_IMG => ContextPriority::Low, + _ => return Err(EGLError::BadParameter), + }; + Ok(priority) + } +} + +impl From for ffi::egl::types::EGLenum { + #[inline] + fn from(value: ContextPriority) -> Self { + match value { + ContextPriority::High => ffi::egl::CONTEXT_PRIORITY_HIGH_IMG, + ContextPriority::Medium => ffi::egl::CONTEXT_PRIORITY_MEDIUM_IMG, + ContextPriority::Low => ffi::egl::CONTEXT_PRIORITY_LOW_IMG, + } + } +} + +// SAFETY: A context can be sent to another thread when this context is not current. +// +// If the context is made current, the safety requirements for calling `EGLDisplay::make_current` must be +// upheld. This means a context which is made current must be unbound (using `unbind`) in order to send this +// context to another thread. +unsafe impl Send for EGLContext {} + +impl EGLContext { + /// Creates a new `EGLContext` from raw handles. + /// + /// # Safety + /// + /// - The context must be created from the system default EGL library (`dlopen("libEGL.so")`) + /// - The `display`, `config`, and `context` must be valid for the lifetime of the returned context. + pub unsafe fn from_raw( + display: *const c_void, + config_id: *const c_void, + context: *const c_void, + ) -> Result { + assert!(!display.is_null(), "EGLDisplay pointer is null"); + assert!(!config_id.is_null(), "EGL configuration id pointer is null"); + assert!(!context.is_null(), "EGLContext pointer is null"); + + let display = unsafe { EGLDisplay::from_raw(display, config_id)? }; + let pixel_format = unsafe { display.get_pixel_format(config_id)? }; + + let span = info_span!(parent: &display.span, "egl_context", ptr = context as usize); + + Ok(EGLContext { + context, + display, + config_id, + pixel_format: Some(pixel_format), + user_data: Arc::new(UserDataMap::default()), + externally_managed: true, + span, + }) + } + + /// Creates a new configless `EGLContext` from a given `EGLDisplay` + pub fn new(display: &EGLDisplay) -> Result { + Self::new_internal(display, None, None, None) + } + + /// Creates a new configless `EGLContext` with the specified priority from a given `EGLDisplay` + /// + /// Note: The priority is a hint that might be ignored by the underlying platform. + /// It also requires `EGL_IMG_context_priority` to be available, otherwise the priority will be + /// ignored. + pub fn new_with_priority(display: &EGLDisplay, priority: ContextPriority) -> Result { + Self::new_internal(display, None, None, Some(priority)) + } + + /// Create a new [`EGLContext`] from a given `EGLDisplay` and configuration requirements + pub fn new_with_config( + display: &EGLDisplay, + attributes: GlAttributes, + reqs: PixelFormatRequirements, + ) -> Result { + Self::new_internal(display, None, Some((attributes, reqs)), None) + } + + /// Create a new [`EGLContext`] from a given `EGLDisplay`, configuration requirements and priority + /// + /// Note: The priority is a hint that might be ignored by the underlying platform. + /// It also requires `EGL_IMG_context_priority` to be available, otherwise the priority will be + /// ignored. + pub fn new_with_config_and_priority( + display: &EGLDisplay, + attributes: GlAttributes, + reqs: PixelFormatRequirements, + priority: ContextPriority, + ) -> Result { + Self::new_internal(display, None, Some((attributes, reqs)), Some(priority)) + } + + /// Create a new configless `EGLContext` from a given `EGLDisplay` sharing resources with another context + pub fn new_shared(display: &EGLDisplay, share: &EGLContext) -> Result { + Self::new_internal(display, Some(share), None, None) + } + + /// Create a new configless `EGLContext` with the specified priority from a given `EGLDisplay` sharing resources with another context + /// + /// Note: The priority is a hint that might be ignored by the underlying platform. + /// It also requires `EGL_IMG_context_priority` to be available, otherwise the priority will be + /// ignored. + pub fn new_shared_with_priority( + display: &EGLDisplay, + share: &EGLContext, + priority: ContextPriority, + ) -> Result { + Self::new_internal(display, Some(share), None, Some(priority)) + } + + /// Create a new `EGLContext` from a given `EGLDisplay` and configuration requirements sharing resources with another context + pub fn new_shared_with_config( + display: &EGLDisplay, + share: &EGLContext, + attributes: GlAttributes, + reqs: PixelFormatRequirements, + ) -> Result { + Self::new_internal(display, Some(share), Some((attributes, reqs)), None) + } + + /// Create a new `EGLContext` with the specified priority from a given `EGLDisplay` and configuration requirements sharing resources with another context + /// + /// Note: The priority is a hint that might be ignored by the underlying platform. + /// It also requires `EGL_IMG_context_priority` to be available, otherwise the priority will be + /// ignored. + pub fn new_shared_with_config_and_priority( + display: &EGLDisplay, + share: &EGLContext, + attributes: GlAttributes, + reqs: PixelFormatRequirements, + priority: ContextPriority, + ) -> Result { + Self::new_internal(display, Some(share), Some((attributes, reqs)), Some(priority)) + } + + fn new_internal( + display: &EGLDisplay, + shared: Option<&EGLContext>, + config: Option<(GlAttributes, PixelFormatRequirements)>, + priority: Option, + ) -> Result { + let span = info_span!(parent: &display.span, "egl_context", ptr = tracing::field::Empty, shared = tracing::field::Empty); + let _guard = span.enter(); + + if let Some(shared) = shared { + span.record("shared", shared.context as usize); + } + + let (pixel_format, config_id) = match config { + Some((attributes, reqs)) => { + let (format, config_id) = display.choose_config(attributes, reqs)?; + (Some(format), config_id) + } + None => { + if !display + .extensions() + .iter() + .any(|x| x == "EGL_KHR_no_config_context") + && !display + .extensions() + .iter() + .any(|x| x == "EGL_MESA_configless_context") + && !display + .extensions() + .iter() + .any(|x| x == "EGL_KHR_surfaceless_context") + { + return Err(Error::EglExtensionNotSupported(&[ + "EGL_KHR_no_config_context", + "EGL_MESA_configless_context", + "EGL_KHR_surfaceless_context", + ])); + } + (None, ffi::egl::NO_CONFIG_KHR) + } + }; + + let mut context_attributes = Vec::with_capacity(12); + + if let Some((attributes, _)) = config { + let version = attributes.version; + + if display.get_egl_version() >= (1, 5) + || display.extensions().iter().any(|s| s == "EGL_KHR_create_context") + { + trace!("Setting CONTEXT_MAJOR_VERSION to {}", version.0); + context_attributes.push(ffi::egl::CONTEXT_MAJOR_VERSION as i32); + context_attributes.push(version.0 as i32); + trace!("Setting CONTEXT_MINOR_VERSION to {}", version.1); + context_attributes.push(ffi::egl::CONTEXT_MINOR_VERSION as i32); + context_attributes.push(version.1 as i32); + + if attributes.debug && display.get_egl_version() >= (1, 5) { + trace!("Setting CONTEXT_OPENGL_DEBUG to TRUE"); + context_attributes.push(ffi::egl::CONTEXT_OPENGL_DEBUG as i32); + context_attributes.push(ffi::egl::TRUE as i32); + } + + context_attributes.push(ffi::egl::CONTEXT_FLAGS_KHR as i32); + context_attributes.push(0); + } else if display.get_egl_version() >= (1, 3) { + trace!("Setting CONTEXT_CLIENT_VERSION to {}", version.0); + context_attributes.push(ffi::egl::CONTEXT_CLIENT_VERSION as i32); + context_attributes.push(version.0 as i32); + } + } else { + trace!("Setting CONTEXT_CLIENT_VERSION to 2"); + context_attributes.push(ffi::egl::CONTEXT_CLIENT_VERSION as i32); + context_attributes.push(2); + } + + let has_context_priority = display + .extensions() + .iter() + .any(|x| x == "EGL_IMG_context_priority"); + if let Some(priority) = priority { + if !has_context_priority { + warn!( + ?priority, + "ignoring requested context priority, EGL_IMG_context_priority not supported" + ); + } else { + context_attributes.push(ffi::egl::CONTEXT_PRIORITY_LEVEL_IMG as i32); + context_attributes.push(Into::::into(priority) as i32); + } + } + + context_attributes.push(ffi::egl::NONE as i32); + + trace!("Creating EGL context..."); + let context = wrap_egl_call_ptr(|| unsafe { + ffi::egl::CreateContext( + **display.get_display_handle(), + config_id, + shared + .map(|context| context.context) + .unwrap_or(ffi::egl::NO_CONTEXT), + context_attributes.as_ptr(), + ) + }) + .map_err(Error::CreationFailed)?; + span.record("ptr", context as usize); + + let context_priority = if has_context_priority { + let mut context_priority = 0; + let res = wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryContext( + **display.get_display_handle(), + context, + ffi::egl::CONTEXT_PRIORITY_LEVEL_IMG as ffi::egl::types::EGLint, + &mut context_priority, + ) + }); + + if res.is_ok() { + match ContextPriority::try_from(context_priority as ffi::egl::types::EGLenum) { + Ok(context_priority) => Some(context_priority), + Err(_) => { + warn!(context_priority, "failed to parse context priority"); + None + } + } + } else { + warn!("failed to query context priority"); + None + } + } else { + None + }; + + if priority.is_some() && has_context_priority && priority != context_priority { + warn!(requested = ?priority, got = ?context_priority, "failed to set context priority"); + } + + info!(priority = ?context_priority, "EGL context created"); + + drop(_guard); + Ok(EGLContext { + context, + display: display.clone(), + config_id, + pixel_format, + user_data: if let Some(shared) = shared { + shared.user_data.clone() + } else { + Arc::new(UserDataMap::default()) + }, + externally_managed: false, + span, + }) + } + + /// Makes the OpenGL context the current context in the current thread with no surface bound. + /// + /// # Safety + /// + /// This function is marked unsafe, because the context cannot be made current on another thread without + /// being unbound again (see [`EGLContext::unbind`]). + #[instrument(level = "trace", skip_all, parent = &self.span, err)] + #[profiling::function] + pub unsafe fn make_current(&self) -> Result<(), MakeCurrentError> { + wrap_egl_call_bool(|| unsafe { + ffi::egl::MakeCurrent( + **self.display.get_display_handle(), + ffi::egl::NO_SURFACE, + ffi::egl::NO_SURFACE, + self.context, + ) + }) + .map(|_| ()) + .map_err(Into::into) + } + + /// Makes the OpenGL context the current context in the current thread with a surface to + /// read/draw to. + /// + /// # Safety + /// + /// This function is marked unsafe, because the context cannot be made current on another thread without + /// being unbound again (see [`EGLContext::unbind`]). + #[profiling::function] + pub unsafe fn make_current_with_surface(&self, surface: &EGLSurface) -> Result<(), MakeCurrentError> { + unsafe { self.make_current_with_draw_and_read_surface(surface, surface) } + } + + /// Makes the OpenGL context the current context in the current thread with surfaces to + /// read/draw to. + /// + /// # Safety + /// + /// This function is marked unsafe, because the context cannot be made current on another thread without + /// being unbound again (see [`EGLContext::unbind`]). + #[instrument(level = "trace", skip_all, parent = &self.span, err)] + #[profiling::function] + pub unsafe fn make_current_with_draw_and_read_surface( + &self, + draw_surface: &EGLSurface, + read_surface: &EGLSurface, + ) -> Result<(), MakeCurrentError> { + let draw_surface_ptr = draw_surface.surface.load(Ordering::SeqCst); + let read_surface_ptr = read_surface.surface.load(Ordering::SeqCst); + wrap_egl_call_bool(|| unsafe { + ffi::egl::MakeCurrent( + **self.display.get_display_handle(), + draw_surface_ptr, + read_surface_ptr, + self.context, + ) + }) + .map(|_| ()) + .map_err(Into::into) + } + + /// Returns true if the OpenGL context is the current one in the thread. + pub fn is_current(&self) -> bool { + unsafe { std::ptr::eq(ffi::egl::GetCurrentContext(), self.context as *const _) } + } + + /// Returns true if the OpenGL context is (possibly) shared with another. + /// + /// Externally managed contexts created with `EGLContext::from_raw` + /// are always considered shared by this function. + pub fn is_shared(&self) -> bool { + self.externally_managed || Arc::strong_count(&self.user_data) > 1 + } + + /// Returns the egl config for this context + pub fn config_id(&self) -> ffi::egl::types::EGLConfig { + self.config_id + } + + /// Returns the pixel format of the main framebuffer of the context. + pub fn pixel_format(&self) -> Option { + self.pixel_format + } + + /// Unbinds this context from the current thread, if set. + /// + /// This does nothing if this context is not the current context. + #[instrument(level = "trace", skip_all, parent = &self.span, err)] + #[profiling::function] + pub fn unbind(&self) -> Result<(), MakeCurrentError> { + if self.is_current() { + wrap_egl_call_bool(|| unsafe { + ffi::egl::MakeCurrent( + **self.display.get_display_handle(), + ffi::egl::NO_SURFACE, + ffi::egl::NO_SURFACE, + ffi::egl::NO_CONTEXT, + ) + })?; + } + Ok(()) + } + + /// Returns the display which created this context. + pub fn display(&self) -> &EGLDisplay { + &self.display + } + + /// Returns a list of formats for dmabufs that can be rendered to. + pub fn dmabuf_render_formats(&self) -> &FormatSet { + self.display.dmabuf_render_formats() + } + + /// Returns a list of formats for dmabufs that can be used as textures. + pub fn dmabuf_texture_formats(&self) -> &FormatSet { + self.display.dmabuf_texture_formats() + } + + /// Retrieve user_data associated with this context + /// + /// *Note:* UserData is shared between shared context, if constructed with + /// [`new_shared`](EGLContext::new_shared) or [`new_shared_with_config`](EGLContext::new_shared_with_config). + pub fn user_data(&self) -> &UserDataMap { + &self.user_data + } + + /// Get a raw handle to the underlying context. + /// + /// The pointer will become invalid, when this struct is destroyed. + pub fn get_context_handle(&self) -> ffi::egl::types::EGLContext { + self.context + } +} + +impl Drop for EGLContext { + fn drop(&mut self) { + if !self.externally_managed { + let _guard = self.span.enter(); + unsafe { + // We need to ensure the context is unbound, otherwise it egl stalls the destroy call + // ignore failures at this point + let _ = self.unbind(); + ffi::egl::DestroyContext(**self.display.get_display_handle(), self.context); + } + } + } +} + +/// Attributes to use when creating an OpenGL context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GlAttributes { + /// Describes the OpenGL API and version that are being requested when a context is created. + /// + /// `(3, 0)` will request a OpenGL ES 3.0 context for example. + /// `(2, 0)` is the minimum. + pub version: (u8, u8), + /// OpenGL profile to use. + pub profile: Option, + /// Whether to enable the debug flag of the context. + /// + /// Debug contexts are usually slower but give better error reporting. + pub debug: bool, + /// Whether to use vsync. If vsync is enabled, calling `swap_buffers` will block until the screen refreshes. + /// This is typically used to prevent screen tearing. + pub vsync: bool, +} + +/// Describes the requested OpenGL context profiles. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GlProfile { + /// Include all the immediate functions and definitions. + Compatibility, + /// Include all the future-compatible functions and definitions. + Core, +} + +/// Describes how the backend should choose a pixel format. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PixelFormatRequirements { + /// If `true`, only hardware-accelerated formats will be considered. If `false`, only software renderers. + /// `None` means "don't care". Default is `None`. + pub hardware_accelerated: Option, + /// Minimum number of bits for the color buffer, excluding alpha. `None` means "don't care". The default is `None`. + pub color_bits: Option, + /// If `true`, the color buffer must be in a floating point format. Default is `false`. + /// + /// Using floating points allows you to write values outside the `[0.0, 1.0]` range. + pub float_color_buffer: bool, + /// Minimum number of bits for the alpha in the color buffer. `None` means "don't care". The default is `None`. + pub alpha_bits: Option, + /// Minimum number of bits for the depth buffer. `None` means "don't care". The default value is `None`. + pub depth_bits: Option, + /// Minimum number of bits for the depth buffer. `None` means "don't care". The default value is `None`. + pub stencil_bits: Option, + /// Contains the minimum number of samples per pixel in the color, depth and stencil buffers. + /// `None` means "don't care". Default is `None`. A value of `Some(0)` indicates that multisampling must not be enabled. + pub multisampling: Option, +} + +impl PixelFormatRequirements { + /// Format selection to get a 8-bit color format with alpha, depth and stencil bits + pub fn _8_bit() -> Self { + PixelFormatRequirements { + hardware_accelerated: Some(true), + color_bits: Some(24), + float_color_buffer: false, + alpha_bits: Some(8), + depth_bits: Some(24), + stencil_bits: Some(8), + multisampling: None, + } + } + + /// Format selection to get a 10-bit color format with alpha, depth and stencil bits + pub fn _10_bit() -> Self { + PixelFormatRequirements { + hardware_accelerated: Some(true), + color_bits: Some(30), + float_color_buffer: false, + alpha_bits: Some(2), + depth_bits: Some(24), + stencil_bits: Some(8), + multisampling: None, + } + } + + /// Format selection to get a 10-bit color format based on floating point values with alpha, depth and stencil bits + pub fn _10f_bit() -> Self { + PixelFormatRequirements { + hardware_accelerated: Some(true), + color_bits: Some(48), + float_color_buffer: true, + alpha_bits: Some(16), + depth_bits: Some(24), + stencil_bits: Some(8), + multisampling: None, + } + } +} + +impl PixelFormatRequirements { + /// Append the requirements to the given attribute list + pub fn create_attributes(&self, out: &mut Vec) { + if let Some(hardware_accelerated) = self.hardware_accelerated { + out.push(ffi::egl::CONFIG_CAVEAT as c_int); + out.push(if hardware_accelerated { + trace!("Setting CONFIG_CAVEAT to NONE"); + ffi::egl::NONE as c_int + } else { + trace!("Setting CONFIG_CAVEAT to SLOW_CONFIG"); + ffi::egl::SLOW_CONFIG as c_int + }); + } + + if let Some(color) = self.color_bits { + trace!("Setting RED_SIZE to {}", color / 3); + out.push(ffi::egl::RED_SIZE as c_int); + out.push((color / 3) as c_int); + trace!("Setting GREEN_SIZE to {}", color / 3 + u8::from(color % 3 != 0)); + out.push(ffi::egl::GREEN_SIZE as c_int); + out.push((color / 3 + u8::from(color % 3 != 0)) as c_int); + trace!("Setting BLUE_SIZE to {}", color / 3 + u8::from(color % 3 == 2)); + out.push(ffi::egl::BLUE_SIZE as c_int); + out.push((color / 3 + u8::from(color % 3 == 2)) as c_int); + } + + if self.float_color_buffer { + out.push(ffi::egl::COLOR_COMPONENT_TYPE_EXT as c_int); + out.push(ffi::egl::COLOR_COMPONENT_TYPE_FLOAT_EXT as c_int); + } + + if let Some(alpha) = self.alpha_bits { + trace!("Setting ALPHA_SIZE to {}", alpha); + out.push(ffi::egl::ALPHA_SIZE as c_int); + out.push(alpha as c_int); + } + + if let Some(depth) = self.depth_bits { + trace!("Setting DEPTH_SIZE to {}", depth); + out.push(ffi::egl::DEPTH_SIZE as c_int); + out.push(depth as c_int); + } + + if let Some(stencil) = self.stencil_bits { + trace!("Setting STENCIL_SIZE to {}", stencil); + out.push(ffi::egl::STENCIL_SIZE as c_int); + out.push(stencil as c_int); + } + + if let Some(multisampling) = self.multisampling { + trace!("Setting SAMPLES to {}", multisampling); + out.push(ffi::egl::SAMPLES as c_int); + out.push(multisampling as c_int); + } + } +} diff --git a/vendor/smithay/src/backend/egl/device.rs b/vendor/smithay/src/backend/egl/device.rs new file mode 100644 index 0000000..bad8292 --- /dev/null +++ b/vendor/smithay/src/backend/egl/device.rs @@ -0,0 +1,283 @@ +use std::{ffi::CStr, os::raw::c_void, path::PathBuf, ptr}; + +use super::{ + EGLDisplay, EGLError, Error, + ffi::{self, egl::types::EGLDeviceEXT}, + wrap_egl_call_bool, wrap_egl_call_ptr, +}; + +#[cfg(feature = "backend_drm")] +use crate::backend::drm::{DrmNode, NodeType}; + +/// safe EGLDevice wrapper +#[derive(Debug, Clone)] +pub struct EGLDevice { + pub(super) inner: EGLDeviceEXT, + device_extensions: Vec, +} + +unsafe impl Send for EGLDevice {} + +impl EGLDevice { + /// Returns an iterator which enumerates over the available [`EGLDevices`](EGLDevice) on the system. + /// + /// This function will return an error if the following extensions are not available: + /// - [`EGL_EXT_device_base`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_base.txt) + /// - [`EGL_EXT_device_enumeration`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_enumeration.txt) + /// - [`EGL_EXT_device_query`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_query.txt) + pub fn enumerate() -> Result, Error> { + // Check the required extensions are present: + let extensions = ffi::make_sure_egl_is_loaded()?; + + if !extensions.iter().any(|s| s == "EGL_EXT_device_base") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_base"])); + } + + if !extensions.iter().any(|s| s == "EGL_EXT_device_enumeration") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_enumeration"])); + } + + if !extensions.iter().any(|s| s == "EGL_EXT_device_query") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_query"])); + } + + // Yes, this is marked as `mut` even though the value is never mutated. EGL expects a mutable pointer + // for num_devices and will not modify the value if we are asking for pointers to some EGLDeviceEXT. + let mut device_amount = 0; + wrap_egl_call_bool(|| unsafe { + // Passing 0 for max devices and a null-pointer for devices is safe because we indicate we only + // want the number of devices. + ffi::egl::QueryDevicesEXT(0, ptr::null_mut(), &mut device_amount) + }) + .map_err(Error::QueryDevices)?; + + let mut devices = Vec::with_capacity(device_amount as usize); + + wrap_egl_call_bool(|| unsafe { + // SAFETY: + // - Vector used as pointer is correct size. + // - Device amount will accommodate all available devices because we have checked the size earlier. + ffi::egl::QueryDevicesEXT(device_amount, devices.as_mut_ptr(), &mut device_amount) + }) + .map_err(Error::QueryDevices)?; + + // Set the length of the vec so that rust does not think it is still empty. + + // SAFETY: + // 1) the vector is pre-allocated to the same size as the amount of returned devices. + // 2) EGL has initialized every value in the vector. + unsafe { devices.set_len(device_amount as usize) }; + + Ok(devices + .into_iter() + .map(|device| { + // SAFETY: We have queried that the extensions are valid and the device pointer is valid. + let device_extensions = unsafe { device_extensions(device) }?; + Ok(EGLDevice { + inner: device, + device_extensions, + }) + }) + .collect::, EGLError>>() + .map_err(Error::QueryDevices)? + .into_iter()) + } + + /// Returns the [`EGLDevices`](EGLDevice) related to the given `EGLDisplay`. + /// + /// This function will return an error if the following extensions are not available: + /// - [`EGL_EXT_device_base`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_base.txt) + /// - [`EGL_EXT_device_query`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_query.txt) + pub fn device_for_display(display: &EGLDisplay) -> Result { + // Check the required extensions are present: + let extensions = ffi::make_sure_egl_is_loaded()?; + + if !extensions.iter().any(|s| s == "EGL_EXT_device_base") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_base"])); + } + + if !extensions.iter().any(|s| s == "EGL_EXT_device_query") { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_query"])); + } + + let mut device: ffi::egl::types::EGLAttrib = 0; + if unsafe { + ffi::egl::QueryDisplayAttribEXT( + display.get_display_handle().handle, + ffi::egl::DEVICE_EXT as i32, + &mut device as *mut _, + ) + } != ffi::egl::TRUE + { + return Err(Error::DisplayNotSupported); + } + + let device = device as EGLDeviceEXT; + + // Per the EGL specification: + // + // > Functions with a return type of EGLDeviceEXT will return this value on failure: EGL_NO_DEVICE_EXT + if device == ffi::egl::NO_DEVICE_EXT { + return Err(Error::DisplayNotSupported); + } + + // SAFETY: We have queried that the extensions are valid and the device pointer is valid. + let device_extensions = unsafe { device_extensions(device) }.map_err(Error::QueryDevices)?; + Ok(EGLDevice { + inner: device, + device_extensions, + }) + } + + /// Returns a list of extensions the device supports. + pub fn extensions(&self) -> Vec { + self.device_extensions.clone() + } + + /// Returns the path to the drm node of this EGLDevice. + /// + /// This function will return an error if the following extensions are not available: + /// - [`EGL_EXT_device_drm`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_drm.txt) + pub fn drm_device_path(&self) -> Result { + if !self.extensions().contains(&"EGL_EXT_device_drm".to_owned()) { + Err(Error::EglExtensionNotSupported(&["EGL_EXT_device_drm"])) + } else { + let raw_path = wrap_egl_call_ptr(|| unsafe { + ffi::egl::QueryDeviceStringEXT( + self.inner, + ffi::egl::DRM_DEVICE_FILE_EXT as ffi::egl::types::EGLint, + ) + }) + .map_err(Error::QueryDeviceProperty)?; + if raw_path.is_null() { + return Err(Error::EmptyDeviceProperty); + } + + // This is safe because of the following: + // 1) The string returned by `eglQueryDeviceStringEXT` is string which will exist as long + // as the EGLDisplay is valid. Since the pointer is only used in this function, the + // lifetime of the pointer will fulfil Rust's CStr requirements on lifetime. + // 2) The string returned by EGL is null terminated. + let device_path = unsafe { CStr::from_ptr(raw_path) } + .to_str() + // EGL ensures the string is valid UTF-8 + .expect("Non-UTF8 device path name"); + + Ok(PathBuf::from(device_path)) + } + } + + /// Returns the path to the render node of this EGLDevice. + /// + /// This function will return an error if the following extensions are not available: + /// - [`EGL_EXT_device_drm_render_node`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_drm_render_node.txt) + pub fn render_device_path(&self) -> Result { + if !self + .extensions() + .contains(&"EGL_EXT_device_drm_render_node".to_owned()) + { + Err(Error::EglExtensionNotSupported(&[ + "EGL_EXT_device_drm_render_node", + ])) + } else { + let raw_path = wrap_egl_call_ptr(|| unsafe { + ffi::egl::QueryDeviceStringEXT( + self.inner, + ffi::egl::DRM_RENDER_NODE_FILE_EXT as ffi::egl::types::EGLint, + ) + }) + .map_err(Error::QueryDeviceProperty)?; + if raw_path.is_null() { + return Err(Error::EmptyDeviceProperty); + } + + // This is safe because of the following: + // 1) The string returned by `eglQueryDeviceStringEXT` is string which will exist as long + // as the EGLDisplay is valid. Since the pointer is only used in this function, the + // lifetime of the pointer will fulfil Rust's CStr requirements on lifetime. + // 2) The string returned by EGL is null terminated. + let device_path = unsafe { CStr::from_ptr(raw_path) } + .to_str() + // EGL ensures the string is valid UTF-8 + .expect("Non-UTF8 device path name"); + + Ok(PathBuf::from(device_path)) + } + } + + /// Returns the drm node belonging to this device. + /// Tries to obtain a render_node first through `EGL_EXT_device_drm_render_node` + /// (see also [`EGLDevice::render_device_path`]) and then falls back to + /// get a render_node from `EGL_EXT_device_drm` (see also [`EGLDevice::drm_device_path`]). + /// If both fail to produce a render node, whichever device returned by + /// `EGL_EXT_device_drm` is returned. + #[cfg(feature = "backend_drm")] + pub fn try_get_render_node(&self) -> Result, Error> { + // first lets try to get a render_node directly + match self + .render_device_path() + .ok() + .and_then(|path| DrmNode::from_path(path).ok()) + { + Some(node) => Ok(Some(node)), + // else we take a drm_path + None => { + let path = self.drm_device_path()?; + let node = DrmNode::from_path(path).ok(); + // and try to convert it to a render_node + Ok(node.map(|node| { + node.node_with_type(NodeType::Render) + .and_then(Result::ok) + // and otherwise go with whatever we got initially + .unwrap_or(node) + })) + } + } + } + + /// Queries if device is not backed by any actual device node and simply renders into client memory. + /// + /// Note: This simply tests presence of the [`EGL_MESA_device_software`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/docs/_extra/specs/EGL_MESA_device_software.txt) extension. + pub fn is_software(&self) -> bool { + // Note: EGL_MESA_device_software requires EGL_EXT_device_query which we already test for initializing the device, + // so there is no need to re-test it here again. + self.extensions().contains(&"EGL_MESA_device_software".to_owned()) + } + + /// Returns the pointer to the raw [`EGLDevice`]. + /// + /// The pointer will become invalid, when this struct is destroyed. + pub fn get_device_handle(&self) -> *const c_void { + self.inner + } +} + +/// Returns all device extensions a device supports. +/// +/// # Safety +/// +/// - The `device` must be a valid pointer to an `EGLDeviceEXT`. +/// - The following extensions must be supported by the display which provides the device: +/// - [`EGL_EXT_device_base`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_base.txt) +/// - [`EGL_EXT_device_query`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_device_query.txt) +unsafe fn device_extensions(device: EGLDeviceEXT) -> Result, EGLError> { + let raw_extensions = wrap_egl_call_ptr(|| unsafe { + ffi::egl::QueryDeviceStringEXT(device, ffi::egl::EXTENSIONS as ffi::egl::types::EGLint) + })?; + + // SAFETY: + // 1) The string returned by `eglQueryDeviceStringEXT` is string which will exist as long + // as the EGLDisplay is valid. Safety requirements for the function ensure this. + // 2) The string returned by EGL is null terminated. + let c_extensions = unsafe { CStr::from_ptr(raw_extensions) }; + + Ok(c_extensions + .to_str() + // EGL ensures the string is valid UTF-8 + .expect("Non-UTF8 device extension name") + // Each extension is space separated (0x20) in the pointer, so strlen cannot return an improper length. + .split_whitespace() + // Take an owned copy so we do not point to garbage if EGL somehow vanishes. + .map(ToOwned::to_owned) + .collect::>()) +} diff --git a/vendor/smithay/src/backend/egl/display.rs b/vendor/smithay/src/backend/egl/display.rs new file mode 100644 index 0000000..0b0ce91 --- /dev/null +++ b/vendor/smithay/src/backend/egl/display.rs @@ -0,0 +1,1280 @@ +//! Type safe native types for safe egl initialisation + +use std::{ + collections::HashSet, + ffi::{CStr, c_int}, + hash::{Hash, Hasher}, + mem::MaybeUninit, + ops::Deref, + os::unix::io::{AsRawFd, FromRawFd, OwnedFd}, + sync::{Arc, LazyLock, Mutex, Weak}, +}; + +use indexmap::IndexSet; +use libc::c_void; +#[cfg(all(feature = "use_system_lib", feature = "wayland_frontend"))] +use wayland_server::{DisplayHandle, Resource, protocol::wl_buffer::WlBuffer}; +#[cfg(all(feature = "use_system_lib", feature = "wayland_frontend"))] +use wayland_sys::server::wl_display; + +use crate::backend::allocator::format::FormatSet; +#[cfg(feature = "backend_drm")] +use crate::backend::egl::EGLDevice; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use crate::backend::egl::{BufferAccessError, EGLBuffer, Format}; +use crate::{ + backend::{ + allocator::{Buffer as _, Format as DrmFormat, Fourcc, Modifier, dmabuf::Dmabuf}, + egl::{ + EGLError, Error, + context::{GlAttributes, PixelFormatRequirements}, + ffi, + ffi::egl::types::EGLImage, + native::EGLNativeDisplay, + wrap_egl_call_bool, wrap_egl_call_ptr, + }, + }, + utils::{Buffer as BufferCoords, Size}, +}; + +use tracing::{debug, error, info, info_span, instrument, trace, warn}; + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +pub(crate) static BUFFER_READER: Mutex> = Mutex::new(None); +static DISPLAYS: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); + +/// Wrapper around [`ffi::EGLDisplay`](ffi::egl::types::EGLDisplay) to ensure display is only destroyed +/// once all resources bound to it have been dropped. +#[derive(Debug)] +pub struct EGLDisplayHandle { + /// ffi EGLDisplay ptr + pub handle: ffi::egl::types::EGLDisplay, + should_terminate: bool, + _native: Box, +} +// EGLDisplay has an internal Mutex +unsafe impl Send for EGLDisplayHandle {} +unsafe impl Sync for EGLDisplayHandle {} + +#[derive(Clone)] +struct WeakEGLDisplayHandle { + handle: Weak, + ptr: ffi::egl::types::EGLDisplay, +} + +unsafe impl Send for WeakEGLDisplayHandle {} +unsafe impl Sync for WeakEGLDisplayHandle {} + +impl From> for WeakEGLDisplayHandle { + #[inline] + fn from(other: Arc) -> Self { + WeakEGLDisplayHandle { + handle: Arc::downgrade(&other), + ptr: other.handle, + } + } +} + +impl Hash for WeakEGLDisplayHandle { + #[inline] + fn hash(&self, state: &mut H) { + self.ptr.hash(state); + } +} + +impl PartialEq for WeakEGLDisplayHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.ptr == other.ptr + } +} + +impl Eq for WeakEGLDisplayHandle {} + +impl Deref for EGLDisplayHandle { + type Target = ffi::egl::types::EGLDisplay; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.handle + } +} + +impl Drop for EGLDisplayHandle { + #[inline] + fn drop(&mut self) { + if self.should_terminate { + unsafe { + // ignore errors on drop + ffi::egl::Terminate(self.handle); + } + } + } +} + +#[derive(Debug, Clone)] +pub(super) struct EGLExtensions { + extensions: Vec, + + pub(super) has_fences: bool, + pub(super) has_native_fences: bool, + has_image_base: bool, + has_import_dmabuf: bool, + has_import_dmabuf_modifiers: bool, + has_export_dmabuf: bool, +} + +impl EGLExtensions { + fn from_display(egl_version: (i32, i32), display: *const c_void) -> Result { + let extensions = EGLDisplay::get_extensions(egl_version, display)?; + + let has_fences = extensions.iter().any(|s| s == "EGL_KHR_fence_sync"); + let has_native_fences = has_fences && extensions.iter().any(|s| s == "EGL_ANDROID_native_fence_sync"); + let has_image_base = extensions.iter().any(|s| s == "EGL_KHR_image_base"); + let has_import_dmabuf = extensions.iter().any(|s| s == "EGL_EXT_image_dma_buf_import"); + let has_import_dmabuf_modifiers = extensions + .iter() + .any(|s| s == "EGL_EXT_image_dma_buf_import_modifiers"); + let has_export_dmabuf = extensions.iter().any(|s| s == "EGL_MESA_image_dma_buf_export"); + + Ok(Self { + extensions, + + has_fences, + has_native_fences, + has_image_base, + has_import_dmabuf, + has_import_dmabuf_modifiers, + has_export_dmabuf, + }) + } + + fn has_extension(&self, extension: &str) -> bool { + self.extensions.iter().any(|s| s == extension) + } +} + +/// [`EGLDisplay`] represents an initialised EGL environment +#[derive(Debug, Clone)] +pub struct EGLDisplay { + display: Arc, + egl_version: (i32, i32), + pub(super) extensions: EGLExtensions, + dmabuf_import_formats: FormatSet, + dmabuf_render_formats: FormatSet, + surface_type: ffi::EGLint, + pub(super) span: tracing::Span, +} + +unsafe fn select_platform_display( + native: &N, + dp_extensions: &[String], +) -> Result<(*const c_void, &'static str), Error> { + for platform in native.supported_platforms() { + debug!("Trying EGL platform: {}", platform.platform_name); + + let missing_extensions = platform + .required_extensions + .iter() + .filter(|ext| !dp_extensions.iter().any(|x| x == *ext)) + .collect::>(); + + if !missing_extensions.is_empty() { + info!( + "Skipping EGL platform because one or more required extensions are not supported. Missing extensions: {:?}", + missing_extensions + ); + continue; + } + + let display = wrap_egl_call_ptr(|| unsafe { + ffi::egl::GetPlatformDisplayEXT( + platform.platform, + platform.native_display, + platform.attrib_list.as_ptr(), + ) + }) + .map_err(Error::DisplayCreationError); + + let display = match display { + Ok(display) => { + if display == ffi::egl::NO_DISPLAY { + info!("Skipping platform because the display is not supported"); + continue; + } + + display + } + Err(err) => { + info!( + "Skipping platform because of an display creation error: {:?}", + err + ); + continue; + } + }; + + info!("Successfully selected EGL platform: {}", platform.platform_name); + return Ok((display, platform.platform_name)); + } + + error!("Unable to find suitable EGL platform"); + Err(Error::DisplayNotSupported) +} + +impl EGLDisplay { + /// Create a new [`EGLDisplay`] from a given [`EGLNativeDisplay`]. + /// + /// # Safety + /// + /// smithay internally tracks EGLDisplay instances as + /// calls with the same parameters to `eglGetPlatformDisplay` will return + /// references to the same underlying display. + /// + /// You don't have to worry about this, when using just smithay, + /// but if other code calls `eglGetPlatformDisplay` it is possible to close the resulting + /// EGLDisplay via `eglTerminate` without invalidating smithay's instances. + /// + /// If you are using other code creating `EGLDisplay`s, don't use this method, + /// but use the external code to create the display and use `EGLDisplay::from_raw` + /// to make it usable in smithay. This way `eglTerminate` will be skipped and you + /// can clean up the display externally. + pub unsafe fn new(native: N) -> Result + where + N: EGLNativeDisplay + 'static, + { + let span = info_span!( + "egl", + platform = tracing::field::Empty, + native = tracing::field::Empty, + version = tracing::field::Empty, + ); + if let Some(value) = native.identifier() { + span.record("native", value); + } + + let dp_extensions = ffi::make_sure_egl_is_loaded()?; + debug!("Supported EGL client extensions: {:?}", dp_extensions); + let surface_type = native.surface_type(); + + // we create an EGLDisplay + let (display, platform) = unsafe { select_platform_display(&native, &dp_extensions)? }; + span.record("platform", platform); + + let display = { + let new_display = Arc::new(EGLDisplayHandle { + handle: display, + should_terminate: true, + _native: Box::new(native), + }); + let weak_disp = WeakEGLDisplayHandle::from(new_display.clone()); + + let mut displays = DISPLAYS.lock().unwrap(); + displays.retain(|handle| handle.handle.strong_count() != 0); + if displays.insert(weak_disp.clone()) { + new_display + } else { + Arc::try_unwrap(new_display).unwrap().should_terminate = false; + displays.get(&weak_disp).unwrap().handle.upgrade().unwrap() + } + }; + + // We can then query the egl api version + let egl_version = unsafe { + let mut major: MaybeUninit = MaybeUninit::uninit(); + let mut minor: MaybeUninit = MaybeUninit::uninit(); + + wrap_egl_call_bool(|| { + ffi::egl::Initialize(display.handle, major.as_mut_ptr(), minor.as_mut_ptr()) + }) + .map_err(Error::InitFailed)?; + + let major = major.assume_init(); + let minor = minor.assume_init(); + + info!("EGL Initialized"); + info!("EGL Version: {:?}", (major, minor)); + + (major, minor) + }; + span.record("version", tracing::field::debug(egl_version)); + + // the list of extensions supported by the client once initialized is different from the + // list of extensions obtained earlier + let extensions = EGLExtensions::from_display(egl_version, display.handle)?; + info!("Supported EGL display extensions: {:?}", extensions); + + let (dmabuf_import_formats, dmabuf_render_formats) = + get_dmabuf_formats(&display.handle, &extensions).map_err(Error::DisplayCreationError)?; + + // egl <= 1.2 does not support OpenGL ES (maybe we want to support OpenGL in the future?) + if egl_version <= (1, 2) { + return Err(Error::OpenGlesNotSupported(None)); + } + wrap_egl_call_bool(|| unsafe { ffi::egl::BindAPI(ffi::egl::OPENGL_ES_API) }) + .map_err(|source| Error::OpenGlesNotSupported(Some(source)))?; + + Ok(EGLDisplay { + display, + surface_type, + egl_version, + extensions, + dmabuf_import_formats, + dmabuf_render_formats, + span, + }) + } + + /// Create a new [`EGLDisplay`] from an already initialized EGLDisplay and EGLConfig handle + /// + /// # Safety + /// + /// - The display must be created from the system default EGL library (`dlopen("libEGL.so")`) + /// - The `display` and `config` must be valid for the lifetime of the returned display and any handles created by this display (using [`EGLDisplay::get_display_handle`]) + /// - smithay can't track the parameters used to create this display, which may cause two Displays to point to the same underlying instance. For this reason displays created + /// using this method will not call `eglTerminate` on destruction. You will have to cleanup manually. + pub unsafe fn from_raw(display: *const c_void, config_id: *const c_void) -> Result { + let span = info_span!("egl", platform = "unknown/raw", version = tracing::field::Empty); + + assert!(!display.is_null(), "EGLDisplay pointer is null"); + assert!(!config_id.is_null(), "EGL configuration id pointer is null"); + + let dp_extensions = ffi::make_sure_egl_is_loaded()?; + debug!("Supported EGL client extensions: {:?}", dp_extensions); + + let egl_version = { + let p = unsafe { + CStr::from_ptr( + wrap_egl_call_ptr(|| ffi::egl::QueryString(display, ffi::egl::VERSION as i32)) + .map_err(|_| Error::DisplayQueryResultInvalid)?, + ) + }; + + let version_string = String::from_utf8(p.to_bytes().to_vec()).unwrap_or_else(|_| String::new()); + let mut version_iterator = version_string + .split(' ') + .next() + .ok_or(Error::DisplayQueryResultInvalid)? + .split('.'); + + let major = version_iterator + .next() + .and_then(|v| v.parse::().ok()) + .ok_or(Error::DisplayQueryResultInvalid)?; + let minor = version_iterator + .next() + .and_then(|v| v.parse::().ok()) + .ok_or(Error::DisplayQueryResultInvalid)?; + + info!("EGL Version: {:?}", (major, minor)); + (major, minor) + }; + span.record("version", tracing::field::debug(egl_version)); + + let extensions = EGLExtensions::from_display(egl_version, display)?; + info!("Supported EGL display extensions: {:?}", extensions); + + let (dmabuf_import_formats, dmabuf_render_formats) = + get_dmabuf_formats(&display, &extensions).map_err(Error::DisplayCreationError)?; + + let egl_api = unsafe { ffi::egl::QueryAPI() }; + if egl_api != ffi::egl::OPENGL_ES_API { + return Err(Error::OpenGlesNotSupported(None)); + } + + let surface_type = { + let mut surface_type: MaybeUninit = MaybeUninit::uninit(); + + wrap_egl_call_bool(|| unsafe { + ffi::egl::GetConfigAttrib( + display, + config_id, + ffi::egl::SURFACE_TYPE as i32, + surface_type.as_mut_ptr(), + ) + }) + .map_err(|_| Error::OpenGlesNotSupported(None))?; + + unsafe { surface_type.assume_init() } + }; + + Ok(EGLDisplay { + display: Arc::new(EGLDisplayHandle { + handle: display, + should_terminate: false, + _native: Box::new(()), + }), + surface_type, + egl_version, + extensions, + dmabuf_import_formats, + dmabuf_render_formats, + span, + }) + } + + fn get_extensions(egl_version: (i32, i32), display: *const c_void) -> Result, Error> { + if egl_version >= (1, 2) { + let p = unsafe { + CStr::from_ptr( + wrap_egl_call_ptr(|| ffi::egl::QueryString(display, ffi::egl::EXTENSIONS as i32)) + .map_err(Error::InitFailed)?, + ) + }; + let list = String::from_utf8(p.to_bytes().to_vec()).unwrap_or_else(|_| String::new()); + Ok(list.split(' ').map(|e| e.to_string()).collect::>()) + } else { + Ok(vec![]) + } + } + + /// Finds a compatible EGLConfig for a given set of requirements + pub fn choose_config( + &self, + attributes: GlAttributes, + reqs: PixelFormatRequirements, + ) -> Result<(PixelFormat, ffi::egl::types::EGLConfig), Error> { + let descriptor = { + let mut out: Vec = Vec::with_capacity(37); + + if self.egl_version >= (1, 2) { + trace!("Setting COLOR_BUFFER_TYPE to RGB_BUFFER"); + out.push(ffi::egl::COLOR_BUFFER_TYPE as c_int); + out.push(ffi::egl::RGB_BUFFER as c_int); + } + + trace!("Setting SURFACE_TYPE to {}", self.surface_type); + + out.push(ffi::egl::SURFACE_TYPE as c_int); + out.push(self.surface_type); + + match attributes.version { + (3, _) => { + if self.egl_version < (1, 3) { + error!("OpenglES 3.* is not supported on EGL Versions lower then 1.3"); + return Err(Error::NoAvailablePixelFormat); + } + trace!("Setting RENDERABLE_TYPE to OPENGL_ES3"); + out.push(ffi::egl::RENDERABLE_TYPE as c_int); + out.push(ffi::egl::OPENGL_ES3_BIT as c_int); + trace!("Setting CONFORMANT to OPENGL_ES3"); + out.push(ffi::egl::CONFORMANT as c_int); + out.push(ffi::egl::OPENGL_ES3_BIT as c_int); + } + (2, _) => { + if self.egl_version < (1, 3) { + error!("OpenglES 2.* is not supported on EGL Versions lower then 1.3"); + return Err(Error::NoAvailablePixelFormat); + } + trace!("Setting RENDERABLE_TYPE to OPENGL_ES2"); + out.push(ffi::egl::RENDERABLE_TYPE as c_int); + out.push(ffi::egl::OPENGL_ES2_BIT as c_int); + trace!("Setting CONFORMANT to OPENGL_ES2"); + out.push(ffi::egl::CONFORMANT as c_int); + out.push(ffi::egl::OPENGL_ES2_BIT as c_int); + } + ver => { + return Err(Error::OpenGlVersionNotSupported(ver)); + } + }; + + reqs.create_attributes(&mut out); + out.push(ffi::egl::NONE as c_int); + out + }; + + // Try to find configs that match out criteria + let mut num_configs = 0; + wrap_egl_call_bool(|| unsafe { + ffi::egl::ChooseConfig( + **self.display, + descriptor.as_ptr(), + std::ptr::null_mut(), + 0, + &mut num_configs, + ) + }) + .map_err(Error::ConfigFailed)?; + if num_configs == 0 { + return Err(Error::NoAvailablePixelFormat); + } + + let mut config_ids: Vec = Vec::with_capacity(num_configs as usize); + wrap_egl_call_bool(|| unsafe { + ffi::egl::ChooseConfig( + **self.display, + descriptor.as_ptr(), + config_ids.as_mut_ptr(), + num_configs, + &mut num_configs, + ) + }) + .map_err(Error::ConfigFailed)?; + unsafe { + config_ids.set_len(num_configs as usize); + } + + if config_ids.is_empty() { + return Err(Error::NoAvailablePixelFormat); + } + + let desired_swap_interval = i32::from(attributes.vsync); + // try to select a config with the desired_swap_interval + // (but don't fail, as the margin might be very small on some cards and most configs are fine) + let config_id = config_ids + .iter() + .copied() + .map(|config| unsafe { + let mut min_swap_interval = 0; + wrap_egl_call_bool(|| { + ffi::egl::GetConfigAttrib( + **self.display, + config, + ffi::egl::MIN_SWAP_INTERVAL as ffi::egl::types::EGLint, + &mut min_swap_interval, + ) + })?; + + if desired_swap_interval < min_swap_interval { + return Ok(None); + } + + let mut max_swap_interval = 0; + wrap_egl_call_bool(|| { + ffi::egl::GetConfigAttrib( + **self.display, + config, + ffi::egl::MAX_SWAP_INTERVAL as ffi::egl::types::EGLint, + &mut max_swap_interval, + ) + })?; + + if desired_swap_interval > max_swap_interval { + return Ok(None); + } + + Ok(Some(config)) + }) + .collect::>, EGLError>>() + .map_err(Error::ConfigFailed)? + .into_iter() + .flatten() + .next() + .unwrap_or_else(|| config_ids[0]); + + // return the format that was selected for our config + let desc = unsafe { self.get_pixel_format(config_id)? }; + info!("Selected color format: {:?}", desc); + + Ok((desc, config_id)) + } + + /// Gets a PixelFormat from a configured EGLConfig + pub(super) unsafe fn get_pixel_format(&self, config_id: *const c_void) -> Result { + // analyzing each config + macro_rules! attrib { + ($display:expr, $config:expr, $attr:expr) => {{ + let mut value = MaybeUninit::uninit(); + wrap_egl_call_bool(|| unsafe { + ffi::egl::GetConfigAttrib( + **$display, + $config, + $attr as ffi::egl::types::EGLint, + value.as_mut_ptr(), + ) + }) + .map_err(Error::ConfigFailed)?; + unsafe { value.assume_init() } + }}; + } + + Ok(PixelFormat { + hardware_accelerated: attrib!(self.display, config_id, ffi::egl::CONFIG_CAVEAT) + != ffi::egl::SLOW_CONFIG as i32, + color_bits: attrib!(self.display, config_id, ffi::egl::RED_SIZE) as u8 + + attrib!(self.display, config_id, ffi::egl::BLUE_SIZE) as u8 + + attrib!(self.display, config_id, ffi::egl::GREEN_SIZE) as u8, + alpha_bits: attrib!(self.display, config_id, ffi::egl::ALPHA_SIZE) as u8, + depth_bits: attrib!(self.display, config_id, ffi::egl::DEPTH_SIZE) as u8, + stencil_bits: attrib!(self.display, config_id, ffi::egl::STENCIL_SIZE) as u8, + stereoscopy: false, + multisampling: match attrib!(self.display, config_id, ffi::egl::SAMPLES) { + 0 | 1 => None, + a => Some(a as u16), + }, + srgb: false, // TODO: use EGL_KHR_gl_colorspace to know that + }) + } + + /// Get a handle to the underlying raw EGLDisplay handle + pub fn get_display_handle(&self) -> Arc { + self.display.clone() + } + + /// Returns the runtime egl version of this display + pub fn get_egl_version(&self) -> (i32, i32) { + self.egl_version + } + + /// Returns the supported extensions of this display + pub fn extensions(&self) -> &[String] { + &self.extensions.extensions[..] + } + + /// Returns a list of formats for dmabufs that can be rendered to. + pub fn dmabuf_render_formats(&self) -> &FormatSet { + &self.dmabuf_render_formats + } + + /// Returns a list of formats for dmabufs that can be used as textures. + pub fn dmabuf_texture_formats(&self) -> &FormatSet { + &self.dmabuf_import_formats + } + + /// Returns when the display supports extensions required for smithays + /// damage tracking helpers. + pub fn supports_damage(&self) -> bool { + self.supports_damage_impl().supported() + } + + pub(super) fn supports_damage_impl(&self) -> DamageSupport { + if !self.extensions.has_extension("EGL_EXT_buffer_age") { + return DamageSupport::No; + }; + + if self.extensions.has_extension("EGL_KHR_swap_buffers_with_damage") { + DamageSupport::KHR + } else if self.extensions.has_extension("EGL_EXT_swap_buffers_with_damage") { + DamageSupport::EXT + } else { + DamageSupport::No + } + } + + /// Exports an [`EGLImage`] as a [`Dmabuf`] + #[allow(clippy::not_unsafe_ptr_arg_deref)] + #[instrument(level = "trace", skip(self), parent = &self.span, err)] + #[profiling::function] + pub fn create_dmabuf_from_image( + &self, + image: EGLImage, + size: Size, + y_inverted: bool, + ) -> Result { + use crate::backend::allocator::dmabuf::DmabufFlags; + + if !self.extensions.has_image_base && !self.extensions.has_export_dmabuf { + return Err(Error::EglExtensionNotSupported(&[ + "EGL_KHR_image_base", + "EGL_MESA_image_dma_buf_export", + ])); + } + + let mut format: c_int = 0; + let mut num_planes: c_int = 0; + let mut modifier: ffi::egl::types::EGLuint64KHR = 0; + wrap_egl_call_bool(|| unsafe { + // TODO: clippy warns us here that we might dereference a raw pointer in a non unsafe public function + // For now we add a allow rule, but this should be addressed in the future + ffi::egl::ExportDMABUFImageQueryMESA( + **self.display, + image, + &mut format as *mut _, + &mut num_planes as *mut _, + &mut modifier as *mut _, + ) + }) + .map_err(Error::DmabufExportFailed)?; + + let mut fds: Vec = Vec::with_capacity(num_planes as usize); + let mut strides: Vec = Vec::with_capacity(num_planes as usize); + let mut offsets: Vec = Vec::with_capacity(num_planes as usize); + unsafe { + // TODO: clippy warns us here that we might dereference a raw pointer in a non unsafe public function + // For now we add a allow rule, but this should be addressed in the future + wrap_egl_call_bool(|| { + ffi::egl::ExportDMABUFImageMESA( + **self.display, + image, + fds.as_mut_ptr(), + strides.as_mut_ptr(), + offsets.as_mut_ptr(), + ) + }) + .map_err(Error::DmabufExportFailed)?; + + fds.set_len(num_planes as usize); + strides.set_len(num_planes as usize); + offsets.set_len(num_planes as usize); + } + + let mut dma = Dmabuf::builder( + size, + Fourcc::try_from(format as u32).expect("Unknown format"), + Modifier::from(modifier), + if y_inverted { + DmabufFlags::Y_INVERT + } else { + DmabufFlags::empty() + }, + ); + for i in 0..num_planes { + dma.add_plane( + // SAFETY: The fds returned by `ExportDMABUFImageMESA` are defined to be owned by + // the caller. + unsafe { OwnedFd::from_raw_fd(fds[i as usize]) }, + offsets[i as usize] as u32, + strides[i as usize] as u32, + ); + } + + #[cfg(feature = "backend_drm")] + if let Some(node) = EGLDevice::device_for_display(self) + .ok() + .and_then(|device| device.try_get_render_node().ok().flatten()) + { + dma.set_node(node); + } + + dma.build().ok_or(Error::DmabufExportFailed(EGLError::BadAlloc)) + } + + /// Imports a [`Dmabuf`] as an [`EGLImage`] + #[instrument(level = "trace", skip(self), parent = &self.span, err)] + #[profiling::function] + pub fn create_image_from_dmabuf(&self, dmabuf: &Dmabuf) -> Result { + if !self.extensions.has_image_base && !self.extensions.has_import_dmabuf { + return Err(Error::EglExtensionNotSupported(&[ + "EGL_KHR_image_base", + "EGL_EXT_image_dma_buf_import", + ])); + } + + if dmabuf.has_modifier() && !self.extensions.has_import_dmabuf_modifiers { + return Err(Error::EglExtensionNotSupported(&[ + "EGL_EXT_image_dma_buf_import_modifiers", + ])); + }; + + let format = dmabuf.format(); + let mut out: Vec = Vec::with_capacity(50); + + out.extend([ + ffi::egl::WIDTH as i32, + dmabuf.width() as i32, + ffi::egl::HEIGHT as i32, + dmabuf.height() as i32, + ffi::egl::LINUX_DRM_FOURCC_EXT as i32, + format.code as u32 as i32, + ]); + + let names = [ + [ + ffi::egl::DMA_BUF_PLANE0_FD_EXT, + ffi::egl::DMA_BUF_PLANE0_OFFSET_EXT, + ffi::egl::DMA_BUF_PLANE0_PITCH_EXT, + ffi::egl::DMA_BUF_PLANE0_MODIFIER_LO_EXT, + ffi::egl::DMA_BUF_PLANE0_MODIFIER_HI_EXT, + ], + [ + ffi::egl::DMA_BUF_PLANE1_FD_EXT, + ffi::egl::DMA_BUF_PLANE1_OFFSET_EXT, + ffi::egl::DMA_BUF_PLANE1_PITCH_EXT, + ffi::egl::DMA_BUF_PLANE1_MODIFIER_LO_EXT, + ffi::egl::DMA_BUF_PLANE1_MODIFIER_HI_EXT, + ], + [ + ffi::egl::DMA_BUF_PLANE2_FD_EXT, + ffi::egl::DMA_BUF_PLANE2_OFFSET_EXT, + ffi::egl::DMA_BUF_PLANE2_PITCH_EXT, + ffi::egl::DMA_BUF_PLANE2_MODIFIER_LO_EXT, + ffi::egl::DMA_BUF_PLANE2_MODIFIER_HI_EXT, + ], + [ + ffi::egl::DMA_BUF_PLANE3_FD_EXT, + ffi::egl::DMA_BUF_PLANE3_OFFSET_EXT, + ffi::egl::DMA_BUF_PLANE3_PITCH_EXT, + ffi::egl::DMA_BUF_PLANE3_MODIFIER_LO_EXT, + ffi::egl::DMA_BUF_PLANE3_MODIFIER_HI_EXT, + ], + ]; + + for (i, ((fd, offset), stride)) in dmabuf + .handles() + .zip(dmabuf.offsets()) + .zip(dmabuf.strides()) + .enumerate() + { + out.extend([ + names[i][0] as i32, + fd.as_raw_fd(), + names[i][1] as i32, + offset as i32, + names[i][2] as i32, + stride as i32, + ]); + if format.modifier != Modifier::Invalid && self.extensions.has_import_dmabuf_modifiers { + out.extend([ + names[i][3] as i32, + (Into::::into(format.modifier) & 0xFFFFFFFF) as i32, + names[i][4] as i32, + (Into::::into(format.modifier) >> 32) as i32, + ]) + } + } + + out.push(ffi::egl::NONE as i32); + + unsafe { + let image = ffi::egl::CreateImageKHR( + **self.display, + ffi::egl::NO_CONTEXT, + ffi::egl::LINUX_DMA_BUF_EXT, + std::ptr::null(), + out.as_ptr(), + ); + + if image == ffi::egl::NO_IMAGE_KHR { + Err(Error::EGLImageCreationFailed) + } else { + Ok(image) + } + } + } + + /// Binds this EGL display to the given Wayland display. + /// + /// This will allow clients to utilize EGL to create hardware-accelerated + /// surfaces. The server will need to be able to handle EGL-[`WlBuffer`]s. + /// + /// ## Errors + /// + /// This might return [`EglExtensionNotSupported`](Error::EglExtensionNotSupported) + /// if binding is not supported by the EGL implementation. + /// + /// This might return [`OtherEGLDisplayAlreadyBound`](Error::OtherEGLDisplayAlreadyBound) + /// if called for the same [`DisplayHandle`] multiple times, as only one egl display may be bound at any given time. + #[cfg(all(feature = "use_system_lib", feature = "wayland_frontend"))] + pub fn bind_wl_display(&self, display: &DisplayHandle) -> Result { + let display_ptr = display.backend_handle().display_ptr(); + if !self.extensions.has_extension("EGL_WL_bind_wayland_display") { + return Err(Error::EglExtensionNotSupported(&["EGL_WL_bind_wayland_display"])); + } + wrap_egl_call_bool(|| unsafe { + ffi::egl::BindWaylandDisplayWL(**self.display, display_ptr as *mut _) + }) + .map_err(Error::OtherEGLDisplayAlreadyBound)?; + let reader = EGLBufferReader::new(self.display.clone(), display_ptr); + let mut global = BUFFER_READER.lock().unwrap(); + if global.as_ref().and_then(|x| x.upgrade()).is_some() { + warn!("Double bind_wl_display, smithay does not support this, please report"); + } + *global = Some(WeakBufferReader { + display: Arc::downgrade(&self.display), + }); + Ok(reader) + } +} + +fn get_dmabuf_formats( + display: &ffi::egl::types::EGLDisplay, + extensions: &EGLExtensions, +) -> Result<(FormatSet, FormatSet), EGLError> { + if !extensions.has_import_dmabuf { + warn!("Dmabuf import extension not available"); + return Ok((FormatSet::default(), FormatSet::default())); + } + + let formats = { + // when we only have the image_dmabuf_import extension we can't query + // which formats are supported. These two are on almost always + // supported; it's the intended way to just try to create buffers. + // Just a guess but better than not supporting dmabufs at all, + // given that the modifiers extension isn't supported everywhere. + if !extensions.has_import_dmabuf_modifiers { + vec![Fourcc::Argb8888, Fourcc::Xrgb8888] + } else { + let mut num = 0i32; + wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryDmaBufFormatsEXT(*display, 0, std::ptr::null_mut(), &mut num as *mut _) + })?; + if num == 0 { + return Ok((FormatSet::default(), FormatSet::default())); + } + let mut formats: Vec = Vec::with_capacity(num as usize); + wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryDmaBufFormatsEXT( + *display, + num, + formats.as_mut_ptr() as *mut _, + &mut num as *mut _, + ) + })?; + unsafe { + formats.set_len(num as usize); + } + formats + .into_iter() + .flat_map(|x| Fourcc::try_from(x).ok()) + .collect::>() + } + }; + + let mut texture_formats = IndexSet::new(); + let mut render_formats = IndexSet::new(); + + for fourcc in formats { + let mut num = 0i32; + // Some drivers return EGL_BAD_PARAMETER here for formats + // they themselves returned for the query above.. *sigh* + // + // - NVIDIA proprietary since 520 + // + // So lets ignore any errors of this call on purpose(!), + // which will let `num` stay at `0` and handle the format + // as unsupported by explicit modifiers. + // Which is probably what the error is suppose to indicate + // although the spec doesn't seem to demand it... + match wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryDmaBufModifiersEXT( + *display, + fourcc as i32, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut num as *mut _, + ) + }) { + Ok(_) => {} + Err(EGLError::BadParameter) => { + debug!( + "eglQueryDmaBufModifiersEXT returned BadParameter for {:?}", + fourcc + ); + num = 0; + } + Err(x) => { + return Err(x); + } + }; + + if num != 0 { + let mut mods: Vec = Vec::with_capacity(num as usize); + let mut external: Vec = Vec::with_capacity(num as usize); + + wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryDmaBufModifiersEXT( + *display, + fourcc as i32, + num, + mods.as_mut_ptr(), + external.as_mut_ptr(), + &mut num as *mut _, + ) + })?; + + unsafe { + mods.set_len(num as usize); + external.set_len(num as usize); + } + + for (modifier, external_only) in mods.into_iter().zip(external) { + let format = DrmFormat { + code: fourcc, + modifier: Modifier::from(modifier), + }; + texture_formats.insert(format); + if external_only == 0 { + render_formats.insert(format); + } + } + } + + texture_formats.insert(DrmFormat { + code: fourcc, + modifier: Modifier::Invalid, + }); + render_formats.insert(DrmFormat { + code: fourcc, + modifier: Modifier::Invalid, + }); + } + + trace!("Supported dmabuf import formats: {:?}", texture_formats); + trace!("Supported dmabuf render formats: {:?}", render_formats); + + Ok(( + FormatSet::from_formats(texture_formats), + FormatSet::from_formats(render_formats), + )) +} + +/// Type to receive [`EGLBuffer`] for EGL-based [`WlBuffer`]s. +/// +/// Can be created by using [`EGLDisplay::bind_wl_display`]. +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +#[derive(Debug, Clone)] +pub struct EGLBufferReader { + display: Arc, + wayland: Option>, +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +pub(crate) struct WeakBufferReader { + display: Weak, +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +impl WeakBufferReader { + pub fn upgrade(&self) -> Option { + Some(EGLBufferReader { + display: self.display.upgrade()?, + wayland: None, + }) + } +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +unsafe impl Send for EGLBufferReader {} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +impl EGLBufferReader { + fn new(display: Arc, wayland: *mut wl_display) -> Self { + #[allow(clippy::arc_with_non_send_sync)] + Self { + display, + wayland: Some(Arc::new(wayland)), + } + } + + /// Try to receive [`EGLBuffer`] from a given [`WlBuffer`]. + /// + /// In case the buffer is not managed by EGL (but e.g. the [`wayland::shm` module](crate::wayland::shm)) + /// a [`BufferAccessError::NotManaged`] is returned. + #[profiling::function] + pub fn egl_buffer_contents( + &self, + buffer: &WlBuffer, + ) -> ::std::result::Result { + if !buffer.is_alive() { + return Err(BufferAccessError::Destroyed); + } + + let mut format: i32 = 0; + let query = wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryWaylandBufferWL( + **self.display, + buffer.id().as_ptr() as _, + ffi::egl::EGL_TEXTURE_FORMAT, + &mut format, + ) + }) + .map_err(BufferAccessError::NotManaged)?; + if query == ffi::egl::FALSE { + return Err(BufferAccessError::NotManaged(EGLError::BadParameter)); + } + + let format = match format { + x if x == ffi::egl::TEXTURE_RGB as i32 => Format::RGB, + x if x == ffi::egl::TEXTURE_RGBA as i32 => Format::RGBA, + ffi::egl::TEXTURE_EXTERNAL_WL => Format::External, + ffi::egl::TEXTURE_Y_UV_WL => { + return Err(BufferAccessError::UnsupportedMultiPlanarFormat(Format::Y_UV)); + } + ffi::egl::TEXTURE_Y_U_V_WL => { + return Err(BufferAccessError::UnsupportedMultiPlanarFormat(Format::Y_U_V)); + } + ffi::egl::TEXTURE_Y_XUXV_WL => { + return Err(BufferAccessError::UnsupportedMultiPlanarFormat(Format::Y_XUXV)); + } + x => panic!("EGL returned invalid texture type: {x}"), + }; + + let mut width: i32 = 0; + wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryWaylandBufferWL( + **self.display, + buffer.id().as_ptr() as _, + ffi::egl::WIDTH as i32, + &mut width, + ) + }) + .map_err(BufferAccessError::NotManaged)?; + + let mut height: i32 = 0; + wrap_egl_call_bool(|| unsafe { + ffi::egl::QueryWaylandBufferWL( + **self.display, + buffer.id().as_ptr() as _, + ffi::egl::HEIGHT as i32, + &mut height, + ) + }) + .map_err(BufferAccessError::NotManaged)?; + + let y_inverted = { + let mut inverted: i32 = 0; + + // Query the egl buffer with EGL_WAYLAND_Y_INVERTED_WL to retrieve the + // buffer orientation. + // The call can either fail, succeed with EGL_TRUE or succeed with EGL_FALSE. + // The specification for eglQuery defines that unsupported attributes shall return + // EGL_FALSE. In case of EGL_WAYLAND_Y_INVERTED_WL the specification defines that + // if EGL_FALSE is returned the value of inverted should be assumed as EGL_TRUE. + // + // see: https://www.khronos.org/registry/EGL/extensions/WL/EGL_WL_bind_wayland_display.txt + match ( + unsafe { + ffi::egl::QueryWaylandBufferWL( + **self.display, + buffer.id().as_ptr() as _, + ffi::egl::WAYLAND_Y_INVERTED_WL, + &mut inverted, + ) + }, + EGLError::from_last_call(), + ) { + (ffi::egl::TRUE, None) => inverted != 0, + (ffi::egl::FALSE, None) => true, + (_, Some(e)) => return Err(BufferAccessError::NotManaged(e)), + _ => unreachable!(), + } + }; + + let mut images = Vec::with_capacity(format.num_planes()); + for i in 0..format.num_planes() { + let out = [ffi::egl::WAYLAND_PLANE_WL as i32, i as i32, ffi::egl::NONE as i32]; + + images.push({ + wrap_egl_call_ptr(|| unsafe { + ffi::egl::CreateImageKHR( + **self.display, + ffi::egl::NO_CONTEXT, + ffi::egl::WAYLAND_BUFFER_WL, + buffer.id().as_ptr() as *mut _, + out.as_ptr(), + ) + }) + .map_err(BufferAccessError::EGLImageCreationFailed)? + }); + } + + Ok(EGLBuffer { + display: self.display.clone(), + size: (width, height).into(), + // y_inverted is negated here because the gles2 renderer + // already inverts the buffer during rendering. + y_inverted: !y_inverted, + format, + images, + }) + } + + /// Try to receive the dimensions of a given [`WlBuffer`]. + /// + /// In case the buffer is not managed by EGL (but e.g. the [`wayland::shm` module](crate::wayland::shm)) or the + /// context has been lost, `None` is returned. + #[profiling::function] + pub fn egl_buffer_dimensions( + &self, + buffer: &WlBuffer, + ) -> Option> { + if !buffer.is_alive() { + return None; + } + + let mut width: i32 = 0; + if unsafe { + ffi::egl::QueryWaylandBufferWL( + **self.display, + buffer.id().as_ptr() as _, + ffi::egl::WIDTH as _, + &mut width, + ) == 0 + } { + return None; + } + + let mut height: i32 = 0; + if unsafe { + ffi::egl::QueryWaylandBufferWL( + **self.display, + buffer.id().as_ptr() as _, + ffi::egl::HEIGHT as _, + &mut height, + ) == 0 + } { + return None; + } + + Some((width, height).into()) + } + + /// Returns if the [`EGLBuffer`] has an alpha channel + /// + /// Note: This will always return `true` for buffers with a + /// `TEXTURE_EXTERNAL_WL` format + pub fn egl_buffer_has_alpha(format: Format) -> bool { + !matches!( + format, + Format::RGB | Format::Y_UV | Format::Y_U_V | Format::Y_XUXV + ) + } +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +impl Drop for EGLBufferReader { + fn drop(&mut self) { + if let Some(wayland) = self.wayland.take().and_then(|x| Arc::try_unwrap(x).ok()) { + if !wayland.is_null() { + unsafe { + // ignore errors on drop + ffi::egl::UnbindWaylandDisplayWL(**self.display, wayland as _); + } + } + } + } +} + +/// Describes the pixel format of a framebuffer +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PixelFormat { + /// is the format hardware accelerated + pub hardware_accelerated: bool, + /// number of bits used for colors + pub color_bits: u8, + /// number of bits used for alpha channel + pub alpha_bits: u8, + /// number of bits used for depth channel + pub depth_bits: u8, + /// number of bits used for stencil buffer + pub stencil_bits: u8, + /// is stereoscopy enabled + pub stereoscopy: bool, + /// number of samples used for multisampling if enabled + pub multisampling: Option, + /// is srgb enabled + pub srgb: bool, +} + +/// Denotes if damage tracking is supported. +/// +/// Additionally notes which variant of the `EGL_*_swap_buffers_with_damage` extension was found. +/// Prefers `KHR` over `EXT`, if both are available. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DamageSupport { + /// `EGL_KHR_swap_buffers_with_damage` + KHR, + /// `EGL_EXT_swap_buffers_with_damage` + EXT, + /// Some required extensions are missing + No, +} + +impl DamageSupport { + /// Returns true, if any valid combination of required extensions was found. + pub fn supported(&self) -> bool { + self != &DamageSupport::No + } +} diff --git a/vendor/smithay/src/backend/egl/error.rs b/vendor/smithay/src/backend/egl/error.rs new file mode 100644 index 0000000..859cc98 --- /dev/null +++ b/vendor/smithay/src/backend/egl/error.rs @@ -0,0 +1,209 @@ +use super::ffi; + +#[derive(thiserror::Error, Debug)] +/// EGL errors +pub enum Error { + /// The requested OpenGL version is not supported + #[error("The requested OpenGL version {0:?} is not supported")] + OpenGlVersionNotSupported((u8, u8)), + /// The EGL implementation does not support creating OpenGL ES contexts + #[error("The EGL implementation does not support creating OpenGL ES contexts. Err: {0:?}")] + OpenGlesNotSupported(#[source] Option), + /// No available pixel format matched the criteria + #[error("No available pixel format matched the criteria")] + NoAvailablePixelFormat, + /// Backend does not match the context type + #[error("The expected backend '{0:?}' does not match the runtime")] + NonMatchingBackend(&'static str), + /// Display creation failed + #[error("Display creation failed with error: {0:}")] + DisplayCreationError(#[source] EGLError), + /// Display query result invalid + #[error("Display query result invalid")] + DisplayQueryResultInvalid, + /// Unable to obtain a valid EGL Display + #[error("Unable to obtain a valid EGL Display.")] + DisplayNotSupported, + /// `eglInitialize` returned an error + #[error("Failed to initialize EGL. Err: {0:}")] + InitFailed(#[source] EGLError), + /// Failed to configure the EGL context + #[error("Failed to configure the EGL context")] + ConfigFailed(#[source] EGLError), + /// Context creation failed as one or more requirements could not be met. Try removing some gl attributes or pixel format requirements + #[error( + "Context creation failed as one or more requirements could not be met. Try removing some gl attributes or pixel format requirements. Err: {0:}" + )] + CreationFailed(#[source] EGLError), + /// The required EGL extension is not supported by the underlying EGL implementation + #[error( + "None of the following EGL extensions is supported by the underlying EGL implementation, at least one is required: {0:?}" + )] + EglExtensionNotSupported(&'static [&'static str]), + /// Only one EGLDisplay may be bound to a given `WlDisplay` at any time + #[error("Only one EGLDisplay may be bound to a given `WlDisplay` at any time")] + OtherEGLDisplayAlreadyBound(#[source] EGLError), + /// No EGLDisplay is currently bound to this `WlDisplay` + #[error("No EGLDisplay is currently bound to this `WlDisplay`")] + NoEGLDisplayBound, + /// Index of plane is out of bounds for `EGLImage` + #[error("Index of plane is out of bounds for `EGLBuffer`")] + PlaneIndexOutOfBounds, + /// Failed to create `EGLImage` from the buffer + #[error("Failed to create `EGLImage` from the buffer")] + EGLImageCreationFailed, + /// Failed to create `Dmabuf` from the image + #[error("Faiedl to create `Dmabuf` from the image")] + DmabufExportFailed(#[source] EGLError), + /// Failed to query the available `EGLDevice`s + #[error("Failed to query the available `EGLDevice`s")] + QueryDevices(#[source] EGLError), + /// Failed to query device properties + #[error("Failed to query the requested device property")] + QueryDeviceProperty(#[source] EGLError), + /// The device does not have the given property + #[error("The device does not have the given property")] + EmptyDeviceProperty, + /// Waiting on a fence failed + #[error("Waiting on a fence failed. Err: {0:}")] + WaitFailed(#[source] EGLError), +} + +/// Raw EGL error +#[derive(thiserror::Error, Debug)] +pub enum EGLError { + /// EGL is not initialized, or could not be initialized, for the specified EGL display connection. + #[error("EGL is not initialized, or could not be initialized, for the specified EGL display connection.")] + NotInitialized, + /// EGL cannot access a requested resource (for example a context is bound in another thread). + #[error("EGL cannot access a requested resource (for example a context is bound in another thread).")] + BadAccess, + /// EGL failed to allocate resources for the requested operation. + #[error("EGL failed to allocate resources for the requested operation.")] + BadAlloc, + /// An unrecognized attribute or attribute value was passed in the attribute list. + #[error("An unrecognized attribute or attribute value was passed in the attribute list.")] + BadAttribute, + /// An EGLContext argument does not name a valid EGL rendering context. + #[error("An EGLContext argument does not name a valid EGL rendering context.")] + BadContext, + /// An EGLConfig argument does not name a valid EGL frame buffer configuration. + #[error("An EGLConfig argument does not name a valid EGL frame buffer configuration.")] + BadConfig, + /// The current surface of the calling thread is a window, pixel buffer or pixmap that is no longer valid. + #[error( + "The current surface of the calling thread is a window, pixel buffer or pixmap that is no longer valid." + )] + BadCurrentSurface, + /// An EGLDevice argument is not valid for this display. + #[error("An EGLDevice argument is not valid for this display.")] + BadDevice, + /// An EGLDisplay argument does not name a valid EGL display connection. + #[error("An EGLDisplay argument does not name a valid EGL display connection.")] + BadDisplay, + /// An EGLSurface argument does not name a valid surface (window, pixel buffer or pixmap) configured for GL rendering. + #[error( + "An EGLSurface argument does not name a valid surface (window, pixel buffer or pixmap) configured for GL rendering." + )] + BadSurface, + /// Arguments are inconsistent (for example, a valid context requires buffers not supplied by a valid surface). + #[error( + "Arguments are inconsistent (for example, a valid context requires buffers not supplied by a valid surface)." + )] + BadMatch, + /// One or more argument values are invalid. + #[error("One or more argument values are invalid.")] + BadParameter, + /// A NativePixmapType argument does not refer to a valid native pixmap. + #[error("A NativePixmapType argument does not refer to a valid native pixmap.")] + BadNativePixmap, + /// A NativeWindowType argument does not refer to a valid native window. + #[error("A NativeWindowType argument does not refer to a valid native window.")] + BadNativeWindow, + /// The EGL operation failed due to temporary unavailability of a requested resource, but the arguments were otherwise valid, and a subsequent attempt may succeed. + #[error( + "The EGL operation failed due to temporary unavailability of a requested resource, but the arguments were otherwise valid, and a subsequent attempt may succeed." + )] + ResourceBusy, + /// A power management event has occurred. The application must destroy all contexts and reinitialise OpenGL ES state and objects to continue rendering. + #[error( + "A power management event has occurred. The application must destroy all contexts and reinitialise OpenGL ES state and objects to continue rendering." + )] + ContextLost, + /// An unknown error + #[error("An unknown error ({0:x})")] + Unknown(u32), +} + +impl From for EGLError { + #[inline] + fn from(value: u32) -> Self { + match value { + ffi::egl::NOT_INITIALIZED => EGLError::NotInitialized, + ffi::egl::BAD_ACCESS => EGLError::BadAccess, + ffi::egl::BAD_ALLOC => EGLError::BadAlloc, + ffi::egl::BAD_ATTRIBUTE => EGLError::BadAttribute, + ffi::egl::BAD_CONTEXT => EGLError::BadContext, + ffi::egl::BAD_CURRENT_SURFACE => EGLError::BadCurrentSurface, + ffi::egl::BAD_DEVICE_EXT => EGLError::BadDevice, + ffi::egl::BAD_DISPLAY => EGLError::BadDisplay, + ffi::egl::BAD_SURFACE => EGLError::BadSurface, + ffi::egl::BAD_MATCH => EGLError::BadMatch, + ffi::egl::BAD_PARAMETER => EGLError::BadParameter, + ffi::egl::BAD_NATIVE_PIXMAP => EGLError::BadNativePixmap, + ffi::egl::BAD_NATIVE_WINDOW => EGLError::BadNativeWindow, + ffi::egl::RESOURCE_BUSY_EXT => EGLError::ResourceBusy, + ffi::egl::CONTEXT_LOST => EGLError::ContextLost, + x => EGLError::Unknown(x), + } + } +} + +impl EGLError { + #[inline] + pub(super) fn from_last_call() -> Option { + match unsafe { ffi::egl::GetError() as u32 } { + ffi::egl::SUCCESS => None, + x => Some(EGLError::from(x)), + } + } +} + +/// Wraps a raw egl call and returns error codes from `eglGetError()`, only if the result of the +/// call is different from the `err` value. +#[inline] +pub fn wrap_egl_call R>(call: F, err: R) -> Result { + let res = call(); + if res != err { + Ok(res) + } else { + Err(EGLError::from_last_call().unwrap_or_else(|| { + tracing::warn!("Erroneous EGL call didn't set EGLError"); + EGLError::Unknown(0) + })) + } +} + +/// Wraps a raw egl call and returns error codes from `eglGetError()`, only if the pointer returned +/// is null. +#[inline] +pub fn wrap_egl_call_ptr *const R>(call: F) -> Result<*const R, EGLError> { + let res = call(); + if !res.is_null() { + Ok(res) + } else { + Err(EGLError::from_last_call().unwrap_or_else(|| { + tracing::warn!("Erroneous EGL call didn't set EGLError"); + EGLError::Unknown(0) + })) + } +} + +/// Wraps a raw egl call and returns error codes from `eglGetError()`, only if the `EGLBoolean` +/// returned is `EGL_FALSE`. +#[inline] +pub fn wrap_egl_call_bool ffi::egl::types::EGLBoolean>( + call: F, +) -> Result { + wrap_egl_call(call, ffi::egl::FALSE) +} diff --git a/vendor/smithay/src/backend/egl/fence.rs b/vendor/smithay/src/backend/egl/fence.rs new file mode 100644 index 0000000..2f8c805 --- /dev/null +++ b/vendor/smithay/src/backend/egl/fence.rs @@ -0,0 +1,181 @@ +//! EGL fence related structs + +use std::os::unix::io::{FromRawFd, IntoRawFd, OwnedFd}; +use std::sync::Arc; +use std::time::Duration; + +use crate::backend::egl::{ + EGLDisplay, EGLDisplayHandle, Error, ffi, wrap_egl_call, wrap_egl_call_bool, wrap_egl_call_ptr, +}; + +#[derive(Debug)] +struct InnerEGLFence { + display_handle: Arc, + handle: ffi::egl::types::EGLSync, + native: bool, +} +unsafe impl Send for InnerEGLFence {} +unsafe impl Sync for InnerEGLFence {} + +impl Drop for InnerEGLFence { + fn drop(&mut self) { + unsafe { + // Not much we can do about when this fails + ffi::egl::DestroySync(**self.display_handle, self.handle); + } + } +} + +/// A EGL fence +#[derive(Debug, Clone)] +pub struct EGLFence(Arc); + +impl EGLFence { + /// Returns whether the display supports creating fences from native fences + pub fn supports_importing(display: &EGLDisplay) -> bool { + display.extensions.has_native_fences + } + + /// Import a native fence fd + #[profiling::function] + pub fn import(display: &EGLDisplay, native: OwnedFd) -> Result { + // SAFETY: we do not have to test for EGL_KHR_fence_sync as EGL_ANDROID_native_fence_sync + // requires it already + if !display.extensions.has_native_fences { + return Err(Error::EglExtensionNotSupported(&[ + "EGL_ANDROID_native_fence_sync", + ])); + } + + let display_handle = display.get_display_handle(); + let fd = native.into_raw_fd(); + let attributes: [ffi::egl::types::EGLAttrib; 3] = [ + ffi::egl::SYNC_NATIVE_FENCE_FD_ANDROID as ffi::egl::types::EGLAttrib, + fd as ffi::egl::types::EGLAttrib, + ffi::egl::NONE as ffi::egl::types::EGLAttrib, + ]; + + let res = wrap_egl_call_ptr(|| unsafe { + ffi::egl::CreateSync( + **display_handle, + ffi::egl::SYNC_NATIVE_FENCE_ANDROID, + attributes.as_ptr(), + ) + }) + .map_err(Error::CreationFailed); + + // In case of an error we have to close the fd to not leak it + if res.is_err() { + unsafe { + rustix::io::close(fd); + } + } + + let handle = res?; + + Ok(Self(Arc::new(InnerEGLFence { + display_handle, + handle, + native: true, + }))) + } + + /// Create a new egl fence + /// + /// This will create a `EGL_SYNC_NATIVE_FENCE_ANDROID` or a `EGL_SYNC_FENCE_KHR` depending + /// on the availability of the `EGL_ANDROID_native_fence_sync` extension. + /// + /// Returns `Err` if the `EGL_KHR_fence_sync` is not present or creating the fence failed. + /// + /// Note: It is the callers responsibility to make sure the current bound client API supports + /// fences. For OpenglES the `GL_OES_EGL_sync` extension indicates support for fences. + #[profiling::function] + pub fn create(display: &EGLDisplay) -> Result { + if !display.extensions.has_fences { + return Err(Error::EglExtensionNotSupported(&["EGL_KHR_fence_sync"])); + } + + let (type_, native) = if display.extensions.has_native_fences { + (ffi::egl::SYNC_NATIVE_FENCE_ANDROID, true) + } else { + (ffi::egl::SYNC_FENCE, false) + }; + + let display_handle = display.get_display_handle(); + let handle = + wrap_egl_call_ptr(|| unsafe { ffi::egl::CreateSync(**display_handle, type_, std::ptr::null()) }) + .map_err(Error::CreationFailed)?; + + Ok(Self(Arc::new(InnerEGLFence { + display_handle, + handle, + native, + }))) + } + + /// Indicates if this fence represents a native fence + pub fn is_native(&self) -> bool { + self.0.native + } + + /// Export this [`EGLFence`] as a native fence fd + #[profiling::function] + pub fn export(&self) -> Result { + if !self.0.native { + return Err(Error::EglExtensionNotSupported(&[ + "EGL_ANDROID_native_fence_sync", + ])); + } + + let fd = wrap_egl_call( + || unsafe { ffi::egl::DupNativeFenceFDANDROID(**self.0.display_handle, self.0.handle) }, + ffi::egl::NO_NATIVE_FENCE_FD_ANDROID, + ) + .map_err(Error::CreationFailed)?; + // SAFETY: eglDupNativeFenceFDANDROID generates EGL_BAD_PARAMETER in case of an error, so fd should always + // contain a valid fd + Ok(unsafe { OwnedFd::from_raw_fd(fd) }) + } + + /// Tries to insert the fence in the current bound client context + /// + /// Returns `Err` if the supplied display does not match the display + /// used at creation time of the fence. + #[profiling::function] + pub fn wait(&self, display: &EGLDisplay) -> Result<(), Error> { + if **display.get_display_handle() != **self.0.display_handle { + return Err(Error::DisplayNotSupported); + } + + wrap_egl_call_bool(|| unsafe { ffi::egl::WaitSync(**self.0.display_handle, self.0.handle, 0) }) + .map_err(Error::CreationFailed)?; + Ok(()) + } + + /// Blocks the current thread until the fence is signaled or the supplied + /// timeout is reached. + /// + /// If the timeout is reached `false` is returned + #[profiling::function] + pub fn client_wait(&self, timeout: Option, flush: bool) -> Result { + let timeout = timeout + .map(|t| t.as_nanos() as ffi::egl::types::EGLuint64KHR) + .unwrap_or(ffi::egl::FOREVER); + let flags = if flush { + ffi::egl::SYNC_FLUSH_COMMANDS_BIT as ffi::egl::types::EGLint + } else { + 0 + }; + // SAFETY: eglClientWaitSyncKHR only defines two errors + // 1. If is not a valid sync object for , EGL_FALSE is returned and an EGL_BAD_PARAMETER error is generated. + // 2. If does not match the EGLDisplay passed to eglCreateSyncKHR when was created, the behaviour is undefined. + // Both should not be possible as we own both, the handle and the display the handle was created on + let status = wrap_egl_call( + || unsafe { ffi::egl::ClientWaitSync(**self.0.display_handle, self.0.handle, flags, timeout) }, + ffi::egl::FALSE as ffi::egl::types::EGLint, + ) + .map_err(Error::WaitFailed)?; + + Ok(status == ffi::egl::CONDITION_SATISFIED as ffi::EGLint) + } +} diff --git a/vendor/smithay/src/backend/egl/ffi.rs b/vendor/smithay/src/backend/egl/ffi.rs new file mode 100644 index 0000000..c59851f --- /dev/null +++ b/vendor/smithay/src/backend/egl/ffi.rs @@ -0,0 +1,363 @@ +#![allow(missing_docs)] + +use tracing::{error, info, warn}; + +use super::Error; +use std::ffi::{c_long, c_uint, c_void}; + +pub type khronos_utime_nanoseconds_t = khronos_uint64_t; +pub type khronos_uint64_t = u64; +pub type khronos_ssize_t = c_long; +pub type EGLint = i32; +pub type EGLchar = char; +pub type EGLLabelKHR = *const c_void; +pub type EGLNativeDisplayType = NativeDisplayType; +pub type EGLNativePixmapType = NativePixmapType; +pub type EGLNativeWindowType = NativeWindowType; +pub type NativeDisplayType = *const c_void; +pub type NativePixmapType = *const c_void; +pub type NativeWindowType = *const c_void; + +fn error_str(error: egl::types::EGLenum) -> &'static str { + match error { + egl::SUCCESS => "SUCCESS", + egl::NOT_INITIALIZED => "NOT_INITIALIZED", + egl::BAD_ACCESS => "BAD_ACCESS", + egl::BAD_ALLOC => "BAD_ALLOC", + egl::BAD_ATTRIBUTE => "BAD_ATTRIBUTE", + egl::BAD_CONFIG => "BAD_CONFIG", + egl::BAD_CONTEXT => "BAD_CONTEXT", + egl::BAD_CURRENT_SURFACE => "BAD_CURRENT_SURFACE", + egl::BAD_DISPLAY => "BAD_DISPLAY", + egl::BAD_MATCH => "BAD_MATCH", + egl::BAD_NATIVE_PIXMAP => "BAD_NATIVE_PIXMAP", + egl::BAD_NATIVE_WINDOW => "BAD_NATIVE_WINDOW", + egl::BAD_PARAMETER => "BAD_PARAMETER", + egl::BAD_SURFACE => "BAD_SURFACE", + egl::CONTEXT_LOST => "CONTEXT_LOST", + _ => "UNKNOWN", + } +} + +extern "system" fn egl_debug_log( + error: egl::types::EGLenum, + command: *const EGLchar, + message_type: EGLint, + _thread: EGLLabelKHR, + _obj: EGLLabelKHR, + message: *const EGLchar, +) { + let _ = std::panic::catch_unwind(move || unsafe { + let mut text = format!("[EGL] 0x{:x} ({})", error, error_str(error)); + if !command.is_null() { + let cmd = std::ffi::CStr::from_ptr(command as *const _); + text.push(' '); + text.push_str(&cmd.to_string_lossy()); + } + if !message.is_null() { + let msg = std::ffi::CStr::from_ptr(message as *const _); + text.push_str(": "); + text.push_str(&msg.to_string_lossy()); + }; + match message_type { + egl::DEBUG_MSG_CRITICAL_KHR => error!("{}", text), + egl::DEBUG_MSG_ERROR_KHR => error!("{}", text), + egl::DEBUG_MSG_WARN_KHR => warn!("{}", text), + egl::DEBUG_MSG_INFO_KHR => info!("{}", text), + _ => {} + } + }); +} + +/// Loads libEGL symbols, if not loaded already. +/// This normally happens automatically during [`EGLDisplay`](super::EGLDisplay) initialization. +pub fn make_sure_egl_is_loaded() -> Result, Error> { + use std::{ + ffi::{CStr, CString}, + ptr, + }; + + fn constrain(f: F) -> F + where + F: for<'a> Fn(&'a str) -> *const ::std::os::raw::c_void, + { + f + } + let proc_address = constrain(|sym| unsafe { super::get_proc_address(sym) }); + + egl::LOAD.call_once(|| unsafe { + egl::load_with(|sym| { + let name = CString::new(sym).unwrap(); + let symbol = egl::LIB.get::<*mut c_void>(name.as_bytes()); + match symbol { + Ok(x) => *x as *const _, + Err(_) => ptr::null(), + } + }); + egl::load_with(&proc_address); + egl::BindWaylandDisplayWL::load_with(&proc_address); + egl::UnbindWaylandDisplayWL::load_with(&proc_address); + egl::QueryWaylandBufferWL::load_with(&proc_address); + egl::DebugMessageControlKHR::load_with(&proc_address); + }); + + let extensions = unsafe { + let p = super::wrap_egl_call_ptr(|| egl::QueryString(egl::NO_DISPLAY, egl::EXTENSIONS as i32)) + .map_err(Error::InitFailed)?; //TODO EGL_EXT_client_extensions not supported + + // this possibility is available only with EGL 1.5 or EGL_EXT_platform_base, otherwise + // `eglQueryString` returns an error + if p.is_null() { + return Err(Error::EglExtensionNotSupported(&["EGL_EXT_platform_base"])); + } else { + let p = CStr::from_ptr(p); + let list = String::from_utf8(p.to_bytes().to_vec()).unwrap_or_else(|_| String::new()); + list.split(' ').map(|e| e.to_string()).collect::>() + } + }; + + egl::DEBUG.call_once(|| unsafe { + if extensions.iter().any(|ext| ext == "EGL_KHR_debug") { + let debug_attribs = [ + egl::DEBUG_MSG_CRITICAL_KHR as isize, + egl::TRUE as isize, + egl::DEBUG_MSG_ERROR_KHR as isize, + egl::TRUE as isize, + egl::DEBUG_MSG_WARN_KHR as isize, + egl::TRUE as isize, + egl::DEBUG_MSG_INFO_KHR as isize, + egl::TRUE as isize, + egl::NONE as isize, + ]; + // we do not check for success, because there is not much we can do otherwise. + egl::DebugMessageControlKHR(Some(egl_debug_log), debug_attribs.as_ptr()); + } + }); + + Ok(extensions) +} + +/// Module containing raw egl function bindings +#[allow(clippy::all, missing_debug_implementations, unsafe_op_in_unsafe_fn)] +pub mod egl { + use super::*; + use libloading::Library; + use std::sync::{LazyLock, Once}; + + pub static LIB: LazyLock = + LazyLock::new(|| unsafe { Library::new("libEGL.so.1") }.expect("Failed to load LibEGL")); + + pub static LOAD: Once = Once::new(); + pub static DEBUG: Once = Once::new(); + + include!(concat!(env!("OUT_DIR"), "/egl_bindings.rs")); + + pub const RESOURCE_BUSY_EXT: u32 = 0x3353; + pub const DRM_RENDER_NODE_FILE_EXT: u32 = 0x3377; + + type EGLDEBUGPROCKHR = Option< + extern "system" fn( + _error: egl::types::EGLenum, + command: *const EGLchar, + _id: EGLint, + _thread: EGLLabelKHR, + _obj: EGLLabelKHR, + message: *const EGLchar, + ), + >; + #[allow(dead_code, non_upper_case_globals)] + pub const DEBUG_MSG_CRITICAL_KHR: types::EGLint = 0x33B9; + #[allow(dead_code, non_upper_case_globals)] + pub const DEBUG_MSG_ERROR_KHR: types::EGLint = 0x33BA; + #[allow(dead_code, non_upper_case_globals)] + pub const DEBUG_MSG_INFO_KHR: types::EGLint = 0x33BC; + #[allow(dead_code, non_upper_case_globals)] + pub const DEBUG_MSG_WARN_KHR: types::EGLint = 0x33BB; + + #[allow(non_snake_case, unused_variables, dead_code)] + #[inline] + pub unsafe fn DebugMessageControlKHR( + callback: EGLDEBUGPROCKHR, + attrib_list: *const types::EGLAttrib, + ) -> types::EGLint { + __gl_imports::mem::transmute::< + _, + extern "system" fn(EGLDEBUGPROCKHR, *const types::EGLAttrib) -> types::EGLint, + >(wayland_storage::DebugMessageControlKHR.f)(callback, attrib_list) + } + /* + * `gl_generator` cannot generate bindings for the `EGL_WL_bind_wayland_display` extension. + * Lets do it ourselves... + */ + + #[allow(non_snake_case, unused_variables, dead_code)] + #[inline] + pub unsafe fn BindWaylandDisplayWL( + dpy: types::EGLDisplay, + display: *mut __gl_imports::raw::c_void, + ) -> types::EGLBoolean { + __gl_imports::mem::transmute::< + _, + extern "system" fn(types::EGLDisplay, *mut __gl_imports::raw::c_void) -> types::EGLBoolean, + >(wayland_storage::BindWaylandDisplayWL.f)(dpy, display) + } + + #[allow(non_snake_case, unused_variables, dead_code)] + #[inline] + pub unsafe fn UnbindWaylandDisplayWL( + dpy: types::EGLDisplay, + display: *mut __gl_imports::raw::c_void, + ) -> types::EGLBoolean { + __gl_imports::mem::transmute::< + _, + extern "system" fn(types::EGLDisplay, *mut __gl_imports::raw::c_void) -> types::EGLBoolean, + >(wayland_storage::UnbindWaylandDisplayWL.f)(dpy, display) + } + + #[allow(non_snake_case, unused_variables, dead_code)] + #[inline] + pub unsafe fn QueryWaylandBufferWL( + dpy: types::EGLDisplay, + buffer: *mut __gl_imports::raw::c_void, + attribute: types::EGLint, + value: *mut types::EGLint, + ) -> types::EGLBoolean { + __gl_imports::mem::transmute::< + _, + extern "system" fn( + types::EGLDisplay, + *mut __gl_imports::raw::c_void, + types::EGLint, + *mut types::EGLint, + ) -> types::EGLBoolean, + >(wayland_storage::QueryWaylandBufferWL.f)(dpy, buffer, attribute, value) + } + + mod wayland_storage { + use super::{__gl_imports::raw, FnPtr}; + pub static mut BindWaylandDisplayWL: FnPtr = FnPtr { + f: super::missing_fn_panic as *const raw::c_void, + is_loaded: false, + }; + pub static mut UnbindWaylandDisplayWL: FnPtr = FnPtr { + f: super::missing_fn_panic as *const raw::c_void, + is_loaded: false, + }; + pub static mut QueryWaylandBufferWL: FnPtr = FnPtr { + f: super::missing_fn_panic as *const raw::c_void, + is_loaded: false, + }; + pub static mut DebugMessageControlKHR: FnPtr = FnPtr { + f: super::missing_fn_panic as *const raw::c_void, + is_loaded: false, + }; + } + + #[allow(non_snake_case)] + pub mod DebugMessageControlKHR { + use super::__gl_imports::raw; + use super::FnPtr; + use super::{metaloadfn, wayland_storage}; + + #[inline] + #[allow(dead_code)] + pub fn is_loaded() -> bool { + unsafe { wayland_storage::DebugMessageControlKHR.is_loaded } + } + + #[allow(dead_code)] + pub fn load_with(mut loadfn: F) + where + F: FnMut(&'static str) -> *const raw::c_void, + { + unsafe { + wayland_storage::DebugMessageControlKHR = + FnPtr::new(metaloadfn(&mut loadfn, "eglDebugMessageControlKHR", &[])) + } + } + } + + #[allow(non_snake_case)] + pub mod BindWaylandDisplayWL { + use super::{__gl_imports::raw, FnPtr, metaloadfn, wayland_storage}; + + #[inline] + #[allow(dead_code)] + pub fn is_loaded() -> bool { + unsafe { wayland_storage::BindWaylandDisplayWL.is_loaded } + } + + #[allow(dead_code)] + pub fn load_with(mut loadfn: F) + where + F: FnMut(&str) -> *const raw::c_void, + { + unsafe { + wayland_storage::BindWaylandDisplayWL = + FnPtr::new(metaloadfn(&mut loadfn, "eglBindWaylandDisplayWL", &[])) + } + } + } + + #[allow(non_snake_case)] + pub mod UnbindWaylandDisplayWL { + use super::{__gl_imports::raw, FnPtr, metaloadfn, wayland_storage}; + + #[inline] + #[allow(dead_code)] + pub fn is_loaded() -> bool { + unsafe { wayland_storage::UnbindWaylandDisplayWL.is_loaded } + } + + #[allow(dead_code)] + pub fn load_with(mut loadfn: F) + where + F: FnMut(&str) -> *const raw::c_void, + { + unsafe { + wayland_storage::UnbindWaylandDisplayWL = + FnPtr::new(metaloadfn(&mut loadfn, "eglUnbindWaylandDisplayWL", &[])) + } + } + } + + #[allow(non_snake_case)] + pub mod QueryWaylandBufferWL { + use super::{__gl_imports::raw, FnPtr, metaloadfn, wayland_storage}; + + #[inline] + #[allow(dead_code)] + pub fn is_loaded() -> bool { + unsafe { wayland_storage::QueryWaylandBufferWL.is_loaded } + } + + #[allow(dead_code)] + pub fn load_with(mut loadfn: F) + where + F: FnMut(&str) -> *const raw::c_void, + { + unsafe { + wayland_storage::QueryWaylandBufferWL = + FnPtr::new(metaloadfn(&mut loadfn, "eglQueryWaylandBufferWL", &[])) + } + } + } + + // Accepted as in eglCreateImageKHR + pub const WAYLAND_BUFFER_WL: c_uint = 0x31D5; + // Accepted in the parameter of eglCreateImageKHR: + pub const WAYLAND_PLANE_WL: c_uint = 0x31D6; + // Possible values for EGL_TEXTURE_FORMAT: + pub const TEXTURE_Y_U_V_WL: i32 = 0x31D7; + pub const TEXTURE_Y_UV_WL: i32 = 0x31D8; + pub const TEXTURE_Y_XUXV_WL: i32 = 0x31D9; + pub const TEXTURE_EXTERNAL_WL: i32 = 0x31DA; + // Accepted in the parameter of eglQueryWaylandBufferWL: + pub const EGL_TEXTURE_FORMAT: i32 = 0x3080; + pub const WAYLAND_Y_INVERTED_WL: i32 = 0x31DB; + + pub const PLATFORM_ANGLE_ANGLE: u32 = 0x3202; + pub const PLATFORM_ANGLE_TYPE_ANGLE: i32 = 0x3203; + pub const PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE: i32 = 0x348F; + pub const PLATFORM_ANGLE_TYPE_VULKAN_ANGLE: i32 = 0x3450; +} diff --git a/vendor/smithay/src/backend/egl/mod.rs b/vendor/smithay/src/backend/egl/mod.rs new file mode 100644 index 0000000..32cb829 --- /dev/null +++ b/vendor/smithay/src/backend/egl/mod.rs @@ -0,0 +1,277 @@ +//! Common traits and types for egl rendering +//! +//! This module has multiple responsibilities related to functionality provided by libEGL: +//! Initializing EGL objects to: +//! - initialize usage of EGL based [`WlBuffer`](wayland_server::protocol::wl_buffer::WlBuffer)s via `wl_drm`. +//! - initialize OpenGL contexts from. +//! - Import/Export external resources to/from OpenGL +//! +//! To use this module, you first need to create an [`EGLDisplay`] through a supported EGL platform +//! as indicated by an implementation of the [`native::EGLNativeDisplay`] trait. +//! +//! You may bind the [`EGLDisplay`], that shall be used by clients for rendering (so pick one initialized by a fast platform) +//! to the [`wayland_server::Display`] of your compositor. Note only one backend may be bound to any [`Display`](wayland_server::Display) at any time. +//! +//! You may then use the resulting [`display::EGLBufferReader`] to receive [`EGLBuffer`] +//! of an EGL-based [`WlBuffer`](wayland_server::protocol::wl_buffer::WlBuffer) for rendering. +//! Renderers implementing the [`ImportEgl`](crate::backend::renderer::ImportEgl)-trait can manage the buffer reader for you. +//! +//! **Note:** The support for binding the [`EGLDisplay`] for use by clients requires the `use_system_lib` cargo feature on Smithay. +//! +//! To create OpenGL contexts you may create [`EGLContext`]s from the display and if the context is initialized with a config +//! it may also be used to initialize an [`EGLSurface`], which can be [bound](crate::backend::renderer::Bind) to some renderers. +//! +//! Alternatively you may import [`dmabuf`](crate::backend::allocator::dmabuf)s using the display, which result +//! in an [`EGLImage`], which can be rendered into by OpenGL. This is preferable to using surfaces as the dmabuf can be +//! passed around freely making resource-management and more complex use-cases like Multi-GPU rendering easier to manage. +//! Renderers based on EGL may support doing this for you by allowing you to [`Bind`](crate::backend::renderer::Bind) a dmabuf directly. +//! + +use std::ffi::c_void; +use std::fmt; + +pub mod context; +pub use self::context::EGLContext; +mod device; +mod error; +pub use self::error::*; +use crate::backend::SwapBuffersError as GraphicsSwapBuffersError; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use crate::utils::{Buffer, Size}; + +#[allow(non_camel_case_types, dead_code, unused_mut, non_upper_case_globals)] +pub mod ffi; +use self::display::EGLDisplayHandle; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use self::ffi::egl::types::EGLImage; + +pub mod display; +pub mod fence; +pub mod native; +pub mod surface; +pub use self::device::EGLDevice; +pub use self::display::EGLDisplay; +pub use self::surface::EGLSurface; + +use std::ffi::CString; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use std::sync::Arc; + +/// Error that can happen on optional EGL features +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EglExtensionNotSupportedError(pub &'static [&'static str]); + +impl fmt::Display for EglExtensionNotSupportedError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> ::std::result::Result<(), fmt::Error> { + write!( + formatter, + "None of the following EGL extensions is supported by the underlying EGL implementation, + at least one is required: {:?}", + self.0 + ) + } +} + +impl ::std::error::Error for EglExtensionNotSupportedError {} + +/// Returns the address of an OpenGL function. +/// +/// Result is independent of displays and does not guarantee an extension is actually supported at runtime. +/// +/// # Safety +/// +/// This function should only be invoked while an EGL context is active. +pub unsafe fn get_proc_address(symbol: &str) -> *const c_void { + let addr = CString::new(symbol.as_bytes()).unwrap(); + let addr = addr.as_ptr(); + unsafe { ffi::egl::GetProcAddress(addr) as *const _ } +} + +/// Error that can occur when accessing an EGL buffer +#[cfg(feature = "wayland_frontend")] +#[derive(thiserror::Error)] +pub enum BufferAccessError { + /// The corresponding Context is not alive anymore + #[error("The corresponding context was lost")] + ContextLost, + /// This buffer is not managed by the EGL buffer + #[error("This buffer is not managed by EGL. Err: {0:}")] + NotManaged(#[source] EGLError), + /// Failed to create `EGLBuffer` from the buffer + #[error("Failed to create EGLImages from the buffer. Err: {0:}")] + EGLImageCreationFailed(#[source] EGLError), + /// The required EGL extension is not supported by the underlying EGL implementation + #[error("{0}")] + EglExtensionNotSupported(#[from] EglExtensionNotSupportedError), + /// We currently do not support multi-planar buffers + #[error("Multi-planar formats (like {0:?}) are unsupported")] + UnsupportedMultiPlanarFormat(Format), + /// This buffer has been destroyed + #[error("This buffer has been destroyed")] + Destroyed, +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for BufferAccessError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> ::std::result::Result<(), fmt::Error> { + match *self { + BufferAccessError::ContextLost => write!(formatter, "BufferAccessError::ContextLost"), + BufferAccessError::NotManaged(_) => write!(formatter, "BufferAccessError::NotManaged"), + BufferAccessError::EGLImageCreationFailed(_) => { + write!(formatter, "BufferAccessError::EGLImageCreationFailed") + } + BufferAccessError::EglExtensionNotSupported(ref err) => write!(formatter, "{err:?}"), + BufferAccessError::UnsupportedMultiPlanarFormat(ref fmt) => write!( + formatter, + "BufferAccessError::UnsupportedMultiPlanerFormat({fmt:?})", + ), + BufferAccessError::Destroyed => write!(formatter, "BufferAccessError::Destroyed"), + } + } +} + +/// Error that can happen when swapping buffers. +#[derive(Debug, thiserror::Error)] +pub enum SwapBuffersError { + /// EGL error during `eglSwapBuffers` + #[error("{0:}")] + EGLSwapBuffers(#[source] EGLError), + /// EGL error during surface creation + #[error("{0:}")] + EGLCreateSurface(#[source] EGLError), +} + +impl std::convert::From for GraphicsSwapBuffersError { + #[inline] + fn from(value: SwapBuffersError) -> Self { + match value { + // bad surface is answered with a surface recreation in `swap_buffers` + x @ SwapBuffersError::EGLSwapBuffers(EGLError::BadSurface) => { + GraphicsSwapBuffersError::TemporaryFailure(Box::new(x)) + } + // the rest is either never happening or are unrecoverable + x @ SwapBuffersError::EGLSwapBuffers(_) => GraphicsSwapBuffersError::ContextLost(Box::new(x)), + x @ SwapBuffersError::EGLCreateSurface(_) => GraphicsSwapBuffersError::ContextLost(Box::new(x)), + } + } +} + +/// Error that can happen when making a context (and surface) current on the active thread. +#[derive(thiserror::Error, Debug)] +#[error("`eglMakeCurrent` failed: {0}")] +pub struct MakeCurrentError(#[from] EGLError); + +impl From for GraphicsSwapBuffersError { + #[inline] + fn from(err: MakeCurrentError) -> GraphicsSwapBuffersError { + match err { + /* + From khronos docs: + If draw or read are not compatible with context, then an EGL_BAD_MATCH error is generated. + If context is current to some other thread, or if either draw or read are bound to contexts in another thread, an EGL_BAD_ACCESS error is generated. + If binding context would exceed the number of current contexts of that client API type supported by the implementation, an EGL_BAD_ACCESS error is generated. + If either draw or read are pbuffers created with eglCreatePbufferFromClientBuffer, and the underlying bound client API buffers are in use by the client API that created them, an EGL_BAD_ACCESS error is generated. + + Except for the first case all of these recoverable. This conversation is mostly used in winit & EglSurface, where compatible context and surfaces are build. + */ + x @ MakeCurrentError(EGLError::BadAccess) => { + GraphicsSwapBuffersError::TemporaryFailure(Box::new(x)) + } + // BadSurface would result in a recreation in `eglSwapBuffers` -> recoverable + x @ MakeCurrentError(EGLError::BadSurface) => { + GraphicsSwapBuffersError::TemporaryFailure(Box::new(x)) + } + /* + From khronos docs: + If the previous context of the calling thread has unflushed commands, and the previous surface is no longer valid, an EGL_BAD_CURRENT_SURFACE error is generated. + + This does not concern this or future `makeCurrent`-calls. + */ + x @ MakeCurrentError(EGLError::BadCurrentSurface) => { + GraphicsSwapBuffersError::TemporaryFailure(Box::new(x)) + } + // the rest is either never happening or are unrecoverable + x => GraphicsSwapBuffersError::ContextLost(Box::new(x)), + } + } +} + +/// Texture format types +#[repr(i32)] +#[allow(non_camel_case_types)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Format { + /// RGB format + RGB = ffi::egl::TEXTURE_RGB as i32, + /// RGB + alpha channel format + RGBA = ffi::egl::TEXTURE_RGBA as i32, + /// External format + External = ffi::egl::TEXTURE_EXTERNAL_WL, + /// 2-plane Y and UV format + Y_UV = ffi::egl::TEXTURE_Y_UV_WL, + /// 3-plane Y, U and V format + Y_U_V = ffi::egl::TEXTURE_Y_U_V_WL, + /// 2-plane Y and XUXV format + Y_XUXV = ffi::egl::TEXTURE_Y_XUXV_WL, +} + +impl Format { + /// Amount of planes this format uses + #[inline] + pub fn num_planes(&self) -> usize { + match *self { + Format::RGB | Format::RGBA | Format::External => 1, + Format::Y_UV | Format::Y_XUXV => 2, + Format::Y_U_V => 3, + } + } +} + +/// Images of the EGL-based [`WlBuffer`](wayland_server::protocol::wl_buffer::WlBuffer). +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +#[derive(Debug)] +pub struct EGLBuffer { + display: Arc, + /// Size of the buffer + pub size: Size, + /// If the y-axis is inverted or not + pub y_inverted: bool, + /// Format of these images + pub format: Format, + images: Vec, +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +impl EGLBuffer { + /// Amount of planes of this EGLBuffer + #[inline] + pub fn num_planes(&self) -> usize { + self.format.num_planes() + } + + /// Returns the `EGLImage` handle for a given plane + pub fn image(&self, plane: usize) -> Option { + if plane > self.format.num_planes() { + None + } else { + Some(self.images[plane]) + } + } + + /// Returns the underlying images + pub fn into_images(mut self) -> Vec { + std::mem::take(&mut self.images) + } +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +impl Drop for EGLBuffer { + fn drop(&mut self) { + for image in self.images.drain(..) { + // ignore result on drop + unsafe { + ffi::egl::DestroyImageKHR(**self.display, image); + } + } + } +} diff --git a/vendor/smithay/src/backend/egl/native.rs b/vendor/smithay/src/backend/egl/native.rs new file mode 100644 index 0000000..b98b94f --- /dev/null +++ b/vendor/smithay/src/backend/egl/native.rs @@ -0,0 +1,425 @@ +//! Type safe native types for safe context/surface creation + +#[cfg(feature = "backend_winit")] +use super::wrap_egl_call_ptr; +use super::{ + EGLDevice, SwapBuffersError, + display::{DamageSupport, EGLDisplayHandle}, + ffi, wrap_egl_call_bool, +}; +#[cfg(feature = "backend_gbm")] +use crate::utils::DevPath; +use crate::utils::{Physical, Rectangle}; +#[cfg(feature = "backend_winit")] +use std::os::raw::c_int; +use std::os::raw::c_void; +#[cfg(feature = "backend_gbm")] +use std::os::unix::io::AsFd; +use std::{fmt::Debug, marker::PhantomData, sync::Arc}; + +#[cfg(feature = "backend_winit")] +use wayland_egl as wegl; +#[cfg(feature = "backend_winit")] +use winit::window::Window as WinitWindow; + +#[cfg(feature = "backend_gbm")] +use gbm::{AsRaw, Device as GbmDevice}; + +/// Create a `EGLPlatform<'a>` for the provided platform. +/// +/// # Arguments +/// +/// * `platform` - The platform defined in `ffi::egl::` +/// * `native_display` - The native display raw pointer which can be casted to `*mut c_void` +/// * `required_extensions` - The name of the required EGL Extension for this platform +/// +/// # Optional Arguments +/// * `attrib_list` - A list of `ffi::EGLint` like defined in the EGL Extension +/// +/// # Examples +/// +/// ```ignore +/// use smithay::backend::egl::{ffi, native::EGLPlatform}; +/// use smithay::egl_platform; +/// +/// // see: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_platform_gbm.txt +/// egl_platform!(PLATFORM_GBM_KHR, native_display, &["EGL_KHR_platform_gbm"]); +/// ``` +#[macro_export] +macro_rules! egl_platform { + ($platform:ident, $native_display:expr, $required_extensions:expr) => { + egl_platform!( + $platform, + $native_display, + $required_extensions, + vec![ffi::egl::NONE as ffi::EGLint] + ) + }; + ($platform:ident, $native_display:expr, $required_extensions:expr, $attrib_list:expr) => { + EGLPlatform::new( + ffi::egl::$platform, + stringify!($platform), + $native_display as *mut _, + $attrib_list, + $required_extensions, + ) + }; +} + +/// Type, Raw handle and attributes used to call [`eglGetPlatformDisplayEXT`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_base.txt) +pub struct EGLPlatform<'a> { + /// Required extensions to use this platform + pub required_extensions: &'static [&'static str], + /// Human readable name of the platform + pub platform_name: &'static str, + /// Platform type used to call [`eglGetPlatformDisplayEXT`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_base.txt) + pub platform: ffi::egl::types::EGLenum, + /// Raw native display handle used to call [`eglGetPlatformDisplayEXT`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_base.txt) + pub native_display: *mut c_void, + /// Attributes used to call [`eglGetPlatformDisplayEXT`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_base.txt) + pub attrib_list: Vec, + _phantom: PhantomData<&'a c_void>, +} + +impl EGLPlatform<'_> { + /// Create a `EGLPlatform<'a>` for the provided platform. + /// + /// # Arguments + /// + /// * `platform` - The platform defined in `ffi::egl::` + /// * `platform_name` - A human readable representation of the platform + /// * `native_display` - The native display raw pointer which can be cast to `*mut c_void` + /// * `attrib_list` - A list of `ffi::EGLint` like defined in the EGL Extension + /// * `required_extensions` - The names of the required EGL Extensions for this platform + /// + /// # Examples + /// + /// ```ignore + /// use smithay::backend::egl::{ffi, native::EGLPlatform}; + /// + /// EGLPlatform::new(ffi::egl::PLATFORM_GBM_KHR, "PLATFORM_GBM_KHR", native_display as *mut _, vec![ffi::egl::NONE as ffi::EGLint], &["EGL_KHR_platform_gbm"]); + /// ``` + pub fn new( + platform: ffi::egl::types::EGLenum, + platform_name: &'static str, + native_display: *mut c_void, + attrib_list: Vec, + required_extensions: &'static [&'static str], + ) -> Self { + EGLPlatform { + platform, + platform_name, + native_display, + attrib_list, + required_extensions, + _phantom: PhantomData, + } + } +} + +impl Debug for EGLPlatform<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EGLPlatform") + .field("platform_name", &self.platform_name) + .field("required_extensions", &self.required_extensions) + .finish() + } +} + +/// Trait describing platform specific functionality to create a valid `EGLDisplay` using the +/// [`EGL_EXT_platform_base`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_base.txt) extension. +pub trait EGLNativeDisplay: Send { + /// List of supported platforms that can be used to create a display using + /// [`eglGetPlatformDisplayEXT`](https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_base.txt) + fn supported_platforms(&self) -> Vec>; + + /// Type of surfaces created + fn surface_type(&self) -> ffi::EGLint { + ffi::egl::WINDOW_BIT as ffi::EGLint + } + + /// String identifying this native display from its counterparts of the same platform, if applicable. + fn identifier(&self) -> Option { + None + } +} + +#[cfg(feature = "backend_gbm")] +impl EGLNativeDisplay for GbmDevice { + fn supported_platforms(&self) -> Vec> { + vec![ + // see: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_platform_gbm.txt + egl_platform!(PLATFORM_GBM_KHR, self.as_raw(), &["EGL_KHR_platform_gbm"]), + // see: https://www.khronos.org/registry/EGL/extensions/MESA/EGL_MESA_platform_gbm.txt + egl_platform!(PLATFORM_GBM_MESA, self.as_raw(), &["EGL_MESA_platform_gbm"]), + ] + } + + fn identifier(&self) -> Option { + self.dev_path().map(|p| p.to_string_lossy().into_owned()) + } +} + +#[cfg(feature = "backend_winit")] +impl EGLNativeDisplay for Arc { + fn supported_platforms(&self) -> Vec> { + use winit::raw_window_handle::{self, HasDisplayHandle}; + + match self.display_handle().map(|handle| handle.as_raw()) { + Ok(raw_window_handle::RawDisplayHandle::Xlib(handle)) => { + let display = handle.display.unwrap().as_ptr(); + vec![ + // see: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_platform_x11.txt + egl_platform!(PLATFORM_X11_KHR, display, &["EGL_KHR_platform_x11"]), + // see: https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_x11.txt + egl_platform!(PLATFORM_X11_EXT, display, &["EGL_EXT_platform_x11"]), + // see: https://raw.githubusercontent.com/google/angle/main/extensions/EGL_ANGLE_platform_angle.txt + egl_platform!( + PLATFORM_ANGLE_ANGLE, + display, + &["EGL_ANGLE_platform_angle", "EGL_ANGLE_platform_angle_vulkan"], + vec![ + ffi::egl::PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, + ffi::egl::PLATFORM_X11_EXT as _, + ffi::egl::PLATFORM_ANGLE_TYPE_ANGLE, + ffi::egl::PLATFORM_ANGLE_TYPE_VULKAN_ANGLE, + ffi::egl::NONE as ffi::EGLint + ] + ), + ] + } + Ok(raw_window_handle::RawDisplayHandle::Wayland(handle)) => { + let display = handle.display.as_ptr(); + vec![ + // see: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_platform_wayland.txt + egl_platform!(PLATFORM_WAYLAND_KHR, display, &["EGL_KHR_platform_wayland"]), + // see: https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_wayland.txt + egl_platform!(PLATFORM_WAYLAND_EXT, display, &["EGL_EXT_platform_wayland"]), + // see: https://raw.githubusercontent.com/google/angle/main/extensions/EGL_ANGLE_platform_angle.txt + egl_platform!( + PLATFORM_ANGLE_ANGLE, + display, + &[ + "EGL_ANGLE_platform_angle", + "EGL_ANGLE_platform_angle_vulkan", + "EGL_EXT_platform_wayland", + ], + vec![ + ffi::egl::PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, + ffi::egl::PLATFORM_WAYLAND_EXT as _, + ffi::egl::PLATFORM_ANGLE_TYPE_ANGLE, + ffi::egl::PLATFORM_ANGLE_TYPE_VULKAN_ANGLE, + ffi::egl::NONE as ffi::EGLint + ] + ), + ] + } + _ => unreachable!("No backends for winit other then Wayland and X11 are supported"), + } + } +} + +/// Shallow type for EGL_PLATFORM_X11_EXT with the default X11 display +#[derive(Debug)] +pub struct X11DefaultDisplay; + +impl EGLNativeDisplay for X11DefaultDisplay { + fn supported_platforms(&self) -> Vec> { + vec![egl_platform!( + PLATFORM_X11_EXT, + // We pass DEFAULT_DISPLAY (null pointer) because the driver should open a connection to the X server. + ffi::egl::DEFAULT_DISPLAY, + &["EGL_EXT_platform_x11"] + )] + } +} + +impl EGLNativeDisplay for EGLDevice { + fn supported_platforms(&self) -> Vec> { + // see: https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_platform_device.txt + vec![egl_platform!( + PLATFORM_DEVICE_EXT, + self.inner, + &["EGL_EXT_platform_device"] + )] + } + + fn surface_type(&self) -> ffi::EGLint { + // EGLDisplays based on EGLDevices do not support normal windowed surfaces. + // But they may support streams, so lets allow users to create them themselves. + ffi::egl::STREAM_BIT_KHR as ffi::EGLint + } + + fn identifier(&self) -> Option { + self.render_device_path() + .ok() + .or_else(|| self.drm_device_path().ok()) + .map(|p| p.to_string_lossy().into_owned()) + } +} + +/// Shallow type for the EGL_PLATFORM_SURFACELESS with default EGL display +#[derive(Debug)] +pub struct EGLSurfacelessDisplay; + +impl EGLNativeDisplay for EGLSurfacelessDisplay { + fn supported_platforms(&self) -> Vec> { + vec![ + // see: https://www.khronos.org/registry/EGL/extensions/MESA/EGL_MESA_platform_surfaceless.txt + egl_platform!( + PLATFORM_SURFACELESS_MESA, + ffi::egl::DEFAULT_DISPLAY, + &["EGL_MESA_platform_surfaceless"] + ), + ] + } +} + +/// Trait for types returning valid surface pointers for initializing egl. +/// +/// ## Safety +/// +/// The returned [`NativeWindowType`](ffi::NativeWindowType) must be valid for EGL +/// and there is no way to test that. +pub unsafe trait EGLNativeSurface: Send { + /// Create an EGLSurface from the internal native type. + /// + /// Must be able to deal with re-creation of existing resources, + /// if `needs_recreation` can return `true`. + /// + /// # Safety + /// + /// - `config_id` has to represent a valid config + unsafe fn create( + &self, + display: &Arc, + config_id: ffi::egl::types::EGLConfig, + ) -> Result<*const c_void, super::EGLError>; + + /// Will be called to check if any internal resources will need + /// to be recreated. Old resources must be used until `create` + /// was called again and a new surface was obtained. + /// + /// Only needs to be recreated, if this may return true. + /// The default implementation always returns false. + fn needs_recreation(&self) -> bool { + false + } + + /// If the surface supports resizing you may implement and use this function. + /// + /// The two first arguments (width, height) are the new size of the surface, + /// the two others (dx, dy) represent the displacement of the top-left corner of the surface. + /// It allows you to control the direction of the resizing if necessary. + /// + /// Implementations may ignore the dx and dy arguments. + /// + /// Returns true if the resize was successful. + fn resize(&self, _width: i32, _height: i32, _dx: i32, _dy: i32) -> bool { + false + } + + /// Adds additional semantics when calling + /// [EGLSurface::swap_buffers](crate::backend::egl::surface::EGLSurface::swap_buffers) + /// + /// Only implement if required by the backend. + #[profiling::function] + fn swap_buffers( + &self, + display: &Arc, + surface: ffi::egl::types::EGLSurface, + damage: Option<&mut [Rectangle]>, + damage_impl: DamageSupport, + ) -> Result<(), SwapBuffersError> { + wrap_egl_call_bool(|| unsafe { + if let Some(damage) = damage { + match damage_impl { + DamageSupport::KHR => ffi::egl::SwapBuffersWithDamageKHR( + ***display, + surface as *const _, + damage.as_mut_ptr() as *mut _, + damage.len() as i32, + ), + DamageSupport::EXT => ffi::egl::SwapBuffersWithDamageEXT( + ***display, + surface as *const _, + damage.as_mut_ptr() as *mut _, + damage.len() as i32, + ), + DamageSupport::No => ffi::egl::SwapBuffers(***display, surface as *const _), + } + } else { + ffi::egl::SwapBuffers(***display, surface as *const _) + } + }) + .map_err(SwapBuffersError::EGLSwapBuffers) + .map(|_| ()) + } + + /// String identifying this native surface from its counterparts of the same platform, if applicable. + fn identifier(&self) -> Option { + None + } +} + +#[cfg(feature = "backend_winit")] +static WINIT_SURFACE_ATTRIBUTES: [c_int; 3] = [ + ffi::egl::RENDER_BUFFER as c_int, + ffi::egl::BACK_BUFFER as c_int, + ffi::egl::NONE as c_int, +]; + +#[cfg(feature = "backend_winit")] +/// Typed Xlib window for the `X11` backend +#[derive(Debug)] +pub struct XlibWindow(pub std::os::raw::c_ulong); + +#[cfg(feature = "backend_winit")] +unsafe impl EGLNativeSurface for XlibWindow { + unsafe fn create( + &self, + display: &Arc, + config_id: ffi::egl::types::EGLConfig, + ) -> Result<*const c_void, super::EGLError> { + wrap_egl_call_ptr(|| unsafe { + let mut id = self.0; + ffi::egl::CreatePlatformWindowSurfaceEXT( + display.handle, + config_id, + (&mut id) as *mut std::os::raw::c_ulong as *mut _, + WINIT_SURFACE_ATTRIBUTES.as_ptr(), + ) + }) + } + + fn identifier(&self) -> Option { + Some("Winit/X11".into()) + } +} + +#[cfg(feature = "backend_winit")] +unsafe impl EGLNativeSurface for wegl::WlEglSurface { + unsafe fn create( + &self, + display: &Arc, + config_id: ffi::egl::types::EGLConfig, + ) -> Result<*const c_void, super::EGLError> { + wrap_egl_call_ptr(|| unsafe { + ffi::egl::CreatePlatformWindowSurfaceEXT( + display.handle, + config_id, + self.ptr() as *mut _, + WINIT_SURFACE_ATTRIBUTES.as_ptr(), + ) + }) + } + + fn resize(&self, width: i32, height: i32, dx: i32, dy: i32) -> bool { + wegl::WlEglSurface::resize(self, width, height, dx, dy); + true + } + + fn identifier(&self) -> Option { + Some("Winit/Wayland".into()) + } +} diff --git a/vendor/smithay/src/backend/egl/surface.rs b/vendor/smithay/src/backend/egl/surface.rs new file mode 100644 index 0000000..0985c75 --- /dev/null +++ b/vendor/smithay/src/backend/egl/surface.rs @@ -0,0 +1,265 @@ +//! EGL surface related structs + +use std::fmt; +use std::sync::{ + Arc, + atomic::{AtomicPtr, Ordering}, +}; + +use crate::backend::egl::{ + EGLError, SwapBuffersError, + display::{DamageSupport, EGLDisplay, EGLDisplayHandle, PixelFormat}, + ffi, + native::EGLNativeSurface, +}; +use crate::utils::{Physical, Rectangle, Size}; + +use tracing::{debug, info_span, instrument}; + +/// EGL surface of a given EGL context for rendering +pub struct EGLSurface { + pub(crate) display: Arc, + native: Box, + pub(crate) surface: AtomicPtr, + config_id: ffi::egl::types::EGLConfig, + pixel_format: PixelFormat, + damage_impl: DamageSupport, + span: tracing::Span, +} + +impl fmt::Debug for EGLSurface { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EGLSurface") + .field("display", &self.display) + // native does not necessarily implement Debug + .field("surface", &self.surface) + .field("config_id", &self.config_id) + .field("pixel_format", &self.pixel_format) + .finish() + } +} + +// safe because EGLConfig can be moved between threads +// and the other types are thread-safe +unsafe impl Send for EGLSurface {} + +impl EGLSurface { + /// Create a new `EGLSurface`. + /// + /// Requires: + /// - A EGLDisplay supported by the corresponding platform matching the surface type + /// - A pixel format + /// - A valid `EGLConfig` (see `EGLContext::config_id()`) + /// - A native type backing the surface matching the used platform + /// - An (optional) Logger + /// + /// # Safety + /// + /// - `config_id` has to represent a valid config + pub unsafe fn new( + display: &EGLDisplay, + pixel_format: PixelFormat, + config: ffi::egl::types::EGLConfig, + native: N, + ) -> Result + where + N: EGLNativeSurface + Send + 'static, + { + let span = info_span!( + parent: &display.span, + "egl_surface", + native = tracing::field::Empty + ); + if let Some(value) = native.identifier() { + span.record("native", value); + } + + let surface = unsafe { native.create(&display.get_display_handle(), config)? }; + if surface == ffi::egl::NO_SURFACE { + return Err(EGLError::BadSurface); + } + + Ok(EGLSurface { + display: display.get_display_handle(), + native: Box::new(native), + surface: AtomicPtr::new(surface as *mut _), + config_id: config, + pixel_format, + damage_impl: display.supports_damage_impl(), + span, + }) + } + + /// Returns the buffer age of the underlying back buffer + #[profiling::function] + pub fn buffer_age(&self) -> Option { + let surface = self.surface.load(Ordering::SeqCst); + let mut age = 0; + let ret = unsafe { + ffi::egl::QuerySurface( + **self.display, + surface as *const _, + ffi::egl::BUFFER_AGE_EXT as i32, + &mut age as *mut _, + ) + }; + if ret == ffi::egl::FALSE { + debug!( + "Failed to query buffer age value for surface {:?}: {}", + self, + EGLError::from_last_call().unwrap_or_else(|| { + tracing::warn!("Erroneous EGL call didn't set EGLError"); + EGLError::Unknown(0) + }) + ); + None + } else { + Some(age) + } + } + + /// Returns the size of the underlying back buffer + #[profiling::function] + pub fn get_size(&self) -> Option> { + let surface = self.surface.load(Ordering::SeqCst); + let mut height = 0; + let ret_h = unsafe { + ffi::egl::QuerySurface( + **self.display, + surface as *const _, + ffi::egl::HEIGHT as i32, + &mut height as *mut _, + ) + }; + let mut width = 0; + let ret_w = unsafe { + ffi::egl::QuerySurface( + **self.display, + surface as *const _, + ffi::egl::WIDTH as i32, + &mut width as *mut _, + ) + }; + if ret_h == ffi::egl::FALSE || ret_w == ffi::egl::FALSE { + debug!( + parent: &self.span, + "Failed to query size value for surface {:?}: {}", + self, + EGLError::from_last_call().unwrap_or_else(|| { + tracing::warn!("Erroneous EGL call didn't set EGLError"); + EGLError::Unknown(0) + }) + ); + None + } else { + Some(Size::from((width, height))) + } + } + + /// Swaps buffers at the end of a frame. + #[instrument(level = "trace", parent = &self.span, skip(self), err)] + #[profiling::function] + pub fn swap_buffers( + &self, + damage: Option<&mut [Rectangle]>, + ) -> ::std::result::Result<(), SwapBuffersError> { + let surface = self.surface.load(Ordering::SeqCst); + + let result = if !surface.is_null() { + self.native + .swap_buffers(&self.display, surface, damage, self.damage_impl) + } else { + Err(SwapBuffersError::EGLSwapBuffers(EGLError::BadSurface)) + }; + + // workaround for missing `PartialEq` impl + let is_bad_surface = matches!( + result, + Err(SwapBuffersError::EGLSwapBuffers(EGLError::BadSurface)) + ); + + if self.native.needs_recreation() || surface.is_null() || is_bad_surface { + let previous = self + .surface + .compare_exchange( + surface, + unsafe { + self.native + .create(&self.display, self.config_id) + .map_err(SwapBuffersError::EGLCreateSurface)? as *mut _ + }, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .expect("The surface pointer changed in between?"); + if previous == surface && !surface.is_null() { + let _ = unsafe { ffi::egl::DestroySurface(**self.display, surface as *const _) }; + } + + // if a recreation is pending anyway, ignore page-flip errors. + // lets see if we still fail after the next commit. + result.map_err(|err| { + debug!("Hiding page-flip error *before* recreation: {}", err); + SwapBuffersError::EGLSwapBuffers(EGLError::BadSurface) + }) + } else { + result + } + } + + /// Returns true if the OpenGL surface is the current one in the thread. + #[profiling::function] + pub fn is_current(&self) -> bool { + let surface = self.surface.load(Ordering::SeqCst); + unsafe { + std::ptr::eq( + ffi::egl::GetCurrentSurface(ffi::egl::DRAW as _), + surface as *const _, + ) && std::ptr::eq( + ffi::egl::GetCurrentSurface(ffi::egl::READ as _), + surface as *const _, + ) + } + } + + /// Returns the egl config for this context + pub fn config_id(&self) -> ffi::egl::types::EGLConfig { + self.config_id + } + + /// Returns the pixel format of the main framebuffer of the context. + pub fn pixel_format(&self) -> PixelFormat { + self.pixel_format + } + + /// Tries to resize the underlying native surface. + /// + /// The two first arguments (width, height) are the new size of the surface, + /// the two others (dx, dy) represent the displacement of the top-left corner of the surface. + /// It allows you to control the direction of the resizing if necessary. + /// + /// Implementations may ignore the dx and dy arguments. + /// + /// Returns true if the resize was successful. + pub fn resize(&self, width: i32, height: i32, dx: i32, dy: i32) -> bool { + self.native.resize(width, height, dx, dy) + } + + /// Get a raw handle to the underlying surface + /// + /// *Note*: The surface might get dynamically recreated during swap-buffers + /// causing the pointer to become invalid. + /// + /// The pointer will become invalid, when this struct is destroyed. + pub fn get_surface_handle(&self) -> ffi::egl::types::EGLSurface { + self.surface.load(Ordering::SeqCst) + } +} + +impl Drop for EGLSurface { + fn drop(&mut self) { + unsafe { + ffi::egl::DestroySurface(**self.display, *self.surface.get_mut() as *const _); + } + } +} diff --git a/vendor/smithay/src/backend/input/mod.rs b/vendor/smithay/src/backend/input/mod.rs new file mode 100644 index 0000000..75723f6 --- /dev/null +++ b/vendor/smithay/src/backend/input/mod.rs @@ -0,0 +1,868 @@ +//! Common traits for input backends to receive input from. + +use std::path::PathBuf; + +pub use xkbcommon::xkb::Keycode; + +mod tablet; + +pub use tablet::{ + ProximityState, TabletToolAxisEvent, TabletToolButtonEvent, TabletToolCapabilities, TabletToolDescriptor, + TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TabletToolType, +}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_pointer; + +use crate::utils::{Clock, Logical, Monotonic, Point, Raw, Size}; + +/// Input timestamp in microseconds, with an undefined base. +/// +/// Libinput does not guarantee that timestamps always increase monotonically. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct InputTime(u64); + +impl InputTime { + /// Current input time, for synthesizing input events that do not come from + /// the input backend. + /// + /// This assumes the input backend uses `CLOCK_MONOTONIC` timestamps. This + /// is guaranteed by `libinput` and `libei` timestamps, but is not required + /// by Wayland itself. In practice, this should be true across backends. + pub fn now() -> Self { + Self::from_millis(Clock::::new().now().as_millis()) + } + + /// Input time from milliseconds + pub fn from_millis(milliseconds: u32) -> Self { + Self(u64::from(milliseconds) * 1000) + } + + /// Input time from microseconds + pub fn from_micros(microseconds: u64) -> Self { + Self(microseconds) + } + + /// Input time in milliseconds + pub fn millis(&self) -> u32 { + (self.0 / 1000) as u32 + } + + /// Input time in microseconds + pub fn micros(&self) -> u64 { + self.0 + } +} + +/// Trait for generic functions every input device does provide +pub trait Device: PartialEq + Eq + std::hash::Hash { + /// Backend-specific identifier for this device. + /// + /// Unique within a single backend instance at a point in time, but not + /// guaranteed to be globally unique across different backends. + fn id(&self) -> String; + /// Human-readable name of the device + fn name(&self) -> String; + /// Test if this device has a specific capability + fn has_capability(&self, capability: DeviceCapability) -> bool; + + /// Returns device USB (product,vendor) id + fn usb_id(&self) -> Option<(u32, u32)>; + + /// Returns the syspath of the device. + /// + /// The path is an absolute path and includes the sys mount point. + fn syspath(&self) -> Option; +} + +/// Set of input types a device may provide +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] // self explanatory +pub enum DeviceCapability { + Keyboard, + Pointer, + Touch, + TabletTool, + TabletPad, + Gesture, + Switch, +} + +/// Trait for generic functions every input event does provide +pub trait Event { + /// Timestamp in microseconds, with an undefined base. + /// + /// Libinput does not guarantee that timestamps always increase monotonically. + // # TODO: + // - check if events can even arrive out of order. + // - Make stronger time guarantees, if possible + fn time(&self) -> InputTime; + + /// Returns the device, that generated this event + fn device(&self) -> B::Device; +} + +/// Used to mark events never emitted by an [`InputBackend`] implementation. +/// +/// Implements all event types and can be used in place for any [`Event`] type, +/// that is not used by an [`InputBackend`] implementation. Initialization is not +/// possible, making accidental use impossible and enabling a lot of possible +/// compiler optimizations. +#[derive(Debug)] +pub enum UnusedEvent {} + +impl Event for UnusedEvent { + fn time(&self) -> InputTime { + match *self {} + } + + fn device(&self) -> B::Device { + match *self {} + } +} + +/// State of key on a keyboard. Either pressed or released +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum KeyState { + /// Key is released + Released, + /// Key is pressed + Pressed, +} + +/// Trait for keyboard event +pub trait KeyboardKeyEvent: Event { + /// Returns the numerical button code of the keyboard button. + /// + /// The value will correspond to one `KEY_` constants from the Linux [input event codes] inside + /// `input-event-codes.h`. + /// + /// [input event codes]: https://gitlab.freedesktop.org/libinput/libinput/-/blob/main/include/linux/linux/input-event-codes.h + fn key_code(&self) -> Keycode; + + /// State of the key + fn state(&self) -> KeyState; + + /// Total number of keys pressed on all devices on the associated [`Seat`](crate::input::Seat) + fn count(&self) -> u32; +} + +impl KeyboardKeyEvent for UnusedEvent { + fn key_code(&self) -> Keycode { + match *self {} + } + + fn state(&self) -> KeyState { + match *self {} + } + + fn count(&self) -> u32 { + match *self {} + } +} + +/// A particular mouse button +#[non_exhaustive] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum MouseButton { + /// Left mouse button + Left, + /// Middle mouse button + Middle, + /// Right mouse button + Right, + /// Back mouse button. + Back, + /// Forward mouse button. + Forward, +} + +/// State of a button on a pointer device, like mouse or tablet tool. Either pressed or released +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum ButtonState { + /// Button is released + Released, + /// Button is pressed + Pressed, +} + +/// Common methods pointer event generated by pressed buttons do implement +pub trait PointerButtonEvent: Event { + /// Pressed button of the event. + /// + /// This may return [`None`] if the button pressed in the event is not a standard mouse button. You may + /// obtain the button code using [`PointerButtonEvent::button_code`]. + fn button(&self) -> Option { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + match self.button_code() { + BTN_LEFT => Some(MouseButton::Left), + BTN_RIGHT => Some(MouseButton::Right), + BTN_MIDDLE => Some(MouseButton::Middle), + BTN_BACK | BTN_SIDE => Some(MouseButton::Back), + BTN_FORWARD | BTN_EXTRA => Some(MouseButton::Forward), + _ => None, + } + } + + /// Returns the numerical button code of the mouse button. + /// + /// The value will correspond to one `BTN_` constants from the Linux [input event codes] inside + /// `input-event-codes.h`. + /// + /// [input event codes]: https://gitlab.freedesktop.org/libinput/libinput/-/blob/main/include/linux/linux/input-event-codes.h + fn button_code(&self) -> u32; + + /// State of the button + fn state(&self) -> ButtonState; +} + +impl PointerButtonEvent for UnusedEvent { + fn button_code(&self) -> u32 { + match *self {} + } + + fn state(&self) -> ButtonState { + match *self {} + } +} +/// Trait for gesture begin events. +pub trait GestureBeginEvent: Event { + /// Number of fingers. + fn fingers(&self) -> u32; +} + +impl GestureBeginEvent for UnusedEvent { + fn fingers(&self) -> u32 { + match *self {} + } +} + +/// Trait for gesture end events. +pub trait GestureEndEvent: Event { + /// True if event was cancelled. + fn cancelled(&self) -> bool; +} + +impl GestureEndEvent for UnusedEvent { + fn cancelled(&self) -> bool { + match *self {} + } +} + +/// Trait for gesture swipe begin event. +pub trait GestureSwipeBeginEvent: GestureBeginEvent {} + +impl GestureSwipeBeginEvent for UnusedEvent {} + +/// Trait for gesture swipe update event. +pub trait GestureSwipeUpdateEvent: Event { + /// Delta of center on the x axis from last begin/update. + fn delta_x(&self) -> f64; + + /// Delta of center on the y axis from last begin/update. + fn delta_y(&self) -> f64; + + /// Delta between the last and new gesture center. + fn delta(&self) -> Point { + (self.delta_x(), self.delta_y()).into() + } +} + +impl GestureSwipeUpdateEvent for UnusedEvent { + fn delta_x(&self) -> f64 { + match *self {} + } + + fn delta_y(&self) -> f64 { + match *self {} + } +} + +/// Trait for gesture swipe end event. +pub trait GestureSwipeEndEvent: GestureEndEvent {} + +impl GestureSwipeEndEvent for UnusedEvent {} + +/// Trait for gesture pinch begin event. +pub trait GesturePinchBeginEvent: GestureBeginEvent {} + +impl GesturePinchBeginEvent for UnusedEvent {} + +/// Trait for gesture pinch update event. +pub trait GesturePinchUpdateEvent: Event { + /// Delta of center on the x axis from last begin/update. + fn delta_x(&self) -> f64; + + /// Delta of center on the y axis from last begin/update. + fn delta_y(&self) -> f64; + + /// Absolute scale compared to begin event. + fn scale(&self) -> f64; + + /// Relative angle in degrees from last begin/update. + fn rotation(&self) -> f64; + + /// Delta between the last and new gesture center. + fn delta(&self) -> Point { + (self.delta_x(), self.delta_y()).into() + } +} + +impl GesturePinchUpdateEvent for UnusedEvent { + fn delta_x(&self) -> f64 { + match *self {} + } + + fn delta_y(&self) -> f64 { + match *self {} + } + + fn scale(&self) -> f64 { + match *self {} + } + + fn rotation(&self) -> f64 { + match *self {} + } +} + +/// Trait for gesture pinch end event. +pub trait GesturePinchEndEvent: GestureEndEvent {} + +impl GesturePinchEndEvent for UnusedEvent {} + +/// Trait for gesture hold begin event. +pub trait GestureHoldBeginEvent: GestureBeginEvent {} + +impl GestureHoldBeginEvent for UnusedEvent {} + +/// Trait for gesture hold end event. +pub trait GestureHoldEndEvent: GestureEndEvent {} + +impl GestureHoldEndEvent for UnusedEvent {} + +/// Axis when scrolling +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum Axis { + /// Vertical axis + Vertical, + /// Horizontal axis + Horizontal, +} + +/// Source of an axis when scrolling +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum AxisSource { + /// Finger. Mostly used for trackpads. + /// + /// Guarantees that a scroll sequence is terminated with a scroll value of 0. + /// A caller may use this information to decide on whether kinetic scrolling should + /// be triggered on this scroll sequence. + /// + /// The coordinate system is identical to the + /// cursor movement, i.e. a scroll value of 1 represents the equivalent relative + /// motion of 1. + Finger, + /// Continuous scrolling device. Almost identical to [`Finger`](AxisSource::Finger) + /// + /// No terminating event is guaranteed (though it may happen). + /// + /// The coordinate system is identical to + /// the cursor movement, i.e. a scroll value of 1 represents the equivalent relative + /// motion of 1. + Continuous, + /// Scroll wheel. + /// + /// No terminating event is guaranteed (though it may happen). Scrolling is in + /// discrete steps. It is up to the caller how to interpret such different step sizes. + Wheel, + /// Scrolling through tilting the scroll wheel. + /// + /// No terminating event is guaranteed (though it may happen). Scrolling is in + /// discrete steps. It is up to the caller how to interpret such different step sizes. + WheelTilt, +} + +/// Direction of physical motion that caused pointer axis event +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum AxisRelativeDirection { + /// Physical motion matches axis direction + Identical, + /// Physical motion is inverse of axis direction (e.g. natural scrolling) + Inverted, +} + +#[cfg(feature = "wayland_frontend")] +impl From for wl_pointer::AxisRelativeDirection { + #[inline] + fn from(direction: AxisRelativeDirection) -> Self { + match direction { + AxisRelativeDirection::Identical => wl_pointer::AxisRelativeDirection::Identical, + AxisRelativeDirection::Inverted => wl_pointer::AxisRelativeDirection::Inverted, + } + } +} + +/// Trait for pointer events generated by scrolling on an axis. +pub trait PointerAxisEvent: Event { + /// Amount of scrolling in pixels on the given [`Axis`]. + /// + /// Guaranteed to be `Some` when source returns either [`AxisSource::Finger`] or [`AxisSource::Continuous`]. + fn amount(&self, axis: Axis) -> Option; + + /// Amount of scrolling in discrete steps on the given [`Axis`]. + /// + /// Guaranteed to be `Some` when source returns either [`AxisSource::Wheel`] or [`AxisSource::WheelTilt`]. + fn amount_v120(&self, axis: Axis) -> Option; + + /// Source of the scroll event. + fn source(&self) -> AxisSource; + + /// Relative direction of physical motion. + fn relative_direction(&self, axis: Axis) -> AxisRelativeDirection; +} + +impl PointerAxisEvent for UnusedEvent { + fn amount(&self, _axis: Axis) -> Option { + match *self {} + } + + fn amount_v120(&self, _axis: Axis) -> Option { + match *self {} + } + + fn source(&self) -> AxisSource { + match *self {} + } + + fn relative_direction(&self, _axis: Axis) -> AxisRelativeDirection { + match *self {} + } +} + +/// Trait for pointer events generated by relative device movement. +pub trait PointerMotionEvent: Event { + /// Delta between the last and new pointer device position interpreted as pixel movement + fn delta(&self) -> Point { + (self.delta_x(), self.delta_y()).into() + } + + /// Unaccelerated delta between the last and new pointer device position + fn delta_unaccel(&self) -> Point { + (self.delta_x_unaccel(), self.delta_y_unaccel()).into() + } + + /// Delta on the x axis between the last and new pointer device position interpreted as pixel movement + fn delta_x(&self) -> f64; + + /// Delta on the y axis between the last and new pointer device position interpreted as pixel movement + fn delta_y(&self) -> f64; + + /// Unaccelerated delta on the x axis between the last and new pointer device position + fn delta_x_unaccel(&self) -> f64; + + /// Unaccelerated delta on the y axis between the last and new pointer device position + fn delta_y_unaccel(&self) -> f64; +} + +impl PointerMotionEvent for UnusedEvent { + fn delta_x(&self) -> f64 { + match *self {} + } + + fn delta_y(&self) -> f64 { + match *self {} + } + + fn delta_x_unaccel(&self) -> f64 { + match *self {} + } + + fn delta_y_unaccel(&self) -> f64 { + match *self {} + } +} + +/// Trait for pointer events generated by absolute device positioning. +pub trait PointerMotionAbsoluteEvent: AbsolutePositionEvent {} +impl PointerMotionAbsoluteEvent for UnusedEvent {} + +/// Input event with absolute location data. +pub trait AbsolutePositionEvent: Event { + /// Device position in it's original coordinate space. + /// + /// The format is defined by the backend implementation. + fn position(&self) -> Point { + (self.x(), self.y()).into() + } + + /// Device x position in it's original coordinate space. + /// + /// The format is defined by the backend implementation. + fn x(&self) -> f64; + + /// Device y position in it's original coordinate space. + /// + /// The format is defined by the backend implementation. + fn y(&self) -> f64; + + /// Device position converted to the targets coordinate space. + /// E.g. the focused output's resolution. + fn position_transformed(&self, coordinate_space: Size) -> Point { + ( + self.x_transformed(coordinate_space.w), + self.y_transformed(coordinate_space.h), + ) + .into() + } + + /// Device x position converted to the targets coordinate space's width. + /// E.g. the focused output's width. + fn x_transformed(&self, width: i32) -> f64; + + /// Device y position converted to the targets coordinate space's height. + /// E.g. the focused output's height. + fn y_transformed(&self, height: i32) -> f64; +} + +impl AbsolutePositionEvent for UnusedEvent { + fn x(&self) -> f64 { + match *self {} + } + + fn y(&self) -> f64 { + match *self {} + } + + fn x_transformed(&self, _width: i32) -> f64 { + match *self {} + } + + fn y_transformed(&self, _height: i32) -> f64 { + match *self {} + } +} + +/// Slot of a different touch event. +/// +/// Touch events are grouped by slots, usually to identify different +/// fingers on a multi-touch enabled input device. Events should only +/// be interpreted in the context of other events on the same slot. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TouchSlot { + id: Option, +} + +impl From> for TouchSlot { + #[inline] + fn from(id: Option) -> Self { + Self { id } + } +} + +impl From for i32 { + #[inline] + fn from(slot: TouchSlot) -> i32 { + slot.id.map(|id| id as i32).unwrap_or(-1) + } +} + +/// Trait with functions available for all touch events. +pub trait TouchEvent: Event { + /// Multi-touch slot identifier. + fn slot(&self) -> TouchSlot; +} + +impl TouchEvent for UnusedEvent { + #[inline] + fn slot(&self) -> TouchSlot { + match *self {} + } +} + +/// Trait for touch events starting at a given position. +pub trait TouchDownEvent: TouchEvent + AbsolutePositionEvent {} +impl TouchDownEvent for UnusedEvent {} + +/// Trait for touch events regarding movement on the screen +pub trait TouchMotionEvent: TouchEvent + AbsolutePositionEvent {} +impl TouchMotionEvent for UnusedEvent {} + +/// Trait for touch events finishing. +pub trait TouchUpEvent: TouchEvent {} +impl TouchUpEvent for UnusedEvent {} + +/// Trait for touch events canceling the chain +pub trait TouchCancelEvent: TouchEvent {} +impl TouchCancelEvent for UnusedEvent {} + +/// Trait for touch frame events +pub trait TouchFrameEvent: Event {} +impl TouchFrameEvent for UnusedEvent {} + +/// Types of Switches +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Switch { + /// The laptop lid was closed when the [`SwitchState`] is + /// [`On`](SwitchState::On), or was opened when it is [`Off`](SwitchState::Off) + Lid, + /// This switch indicates whether the device is in normal laptop mode + /// or behaves like a tablet-like device where the primary + /// interaction is usually a touch screen. When in tablet mode, the + /// keyboard and touchpad are usually inaccessible. + /// + /// If the switch is in state [`SwitchState::Off`], the + /// device is in laptop mode. If the switch is in state + /// [`SwitchState::On`], the device is in tablet mode and the + /// keyboard or touchpad may not be accessible. + /// + /// It is up to the caller to identify which devices are inaccessible + /// in tablet mode. + TabletMode, +} + +/// State of a Switch +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SwitchState { + /// Switch is off + Off, + /// Switch is on + On, +} + +/// Trait for switch toggle events +pub trait SwitchToggleEvent: Event { + /// [`Switch`] which triggered the event + fn switch(&self) -> Option; + /// [`State`](SwitchState) of the switch + fn state(&self) -> SwitchState; +} +impl SwitchToggleEvent for UnusedEvent { + fn switch(&self) -> Option { + match *self {} + } + fn state(&self) -> SwitchState { + match *self {} + } +} + +/// Trait that describes objects providing a source of input events. All input backends +/// need to implement this and provide the same base guarantees about the precision of +/// given events. +pub trait InputBackend: Sized { + /// Type representing input devices + type Device: Device; + /// Type representing keyboard events + type KeyboardKeyEvent: KeyboardKeyEvent; + /// Type representing axis events on pointer devices + type PointerAxisEvent: PointerAxisEvent; + /// Type representing button events on pointer devices + type PointerButtonEvent: PointerButtonEvent; + /// Type representing motion events of pointer devices + type PointerMotionEvent: PointerMotionEvent; + /// Type representing motion events of pointer devices + type PointerMotionAbsoluteEvent: PointerMotionAbsoluteEvent; + /// Type representing swipe begin events of pointer devices + type GestureSwipeBeginEvent: GestureSwipeBeginEvent; + /// Type representing swipe update events of pointer devices + type GestureSwipeUpdateEvent: GestureSwipeUpdateEvent; + /// Type representing swipe end events of pointer devices + type GestureSwipeEndEvent: GestureSwipeEndEvent; + /// Type representing pinch begin events of pointer devices + type GesturePinchBeginEvent: GesturePinchBeginEvent; + /// Type representing pinch update events of pointer devices + type GesturePinchUpdateEvent: GesturePinchUpdateEvent; + /// Type representing pinch end events of pointer devices + type GesturePinchEndEvent: GesturePinchEndEvent; + /// Type representing hold begin events of pointer devices + type GestureHoldBeginEvent: GestureHoldBeginEvent; + /// Type representing hold end events of pointer devices + type GestureHoldEndEvent: GestureHoldEndEvent; + /// Type representing touch events starting + type TouchDownEvent: TouchDownEvent; + /// Type representing touch events ending + type TouchUpEvent: TouchUpEvent; + /// Type representing touch events from moving + type TouchMotionEvent: TouchMotionEvent; + /// Type representing canceling of touch events + type TouchCancelEvent: TouchCancelEvent; + /// Type representing touch frame events + type TouchFrameEvent: TouchFrameEvent; + /// Type representing axis events on tablet devices + type TabletToolAxisEvent: TabletToolAxisEvent; + /// Type representing proximity events on tablet devices + type TabletToolProximityEvent: TabletToolProximityEvent; + /// Type representing tip events on tablet devices + type TabletToolTipEvent: TabletToolTipEvent; + /// Type representing button events on tablet tool devices + type TabletToolButtonEvent: TabletToolButtonEvent; + /// Type representing switch toggle events + type SwitchToggleEvent: SwitchToggleEvent; + + /// Special events that are custom to this backend + type SpecialEvent; +} + +/// Different events that can be generated by an input backend +#[derive(Debug)] +pub enum InputEvent { + /// An input device was connected + DeviceAdded { + /// The added device + device: B::Device, + }, + /// An input device was disconnected + DeviceRemoved { + /// The removed device + device: B::Device, + }, + /// A keyboard event occurred + Keyboard { + /// The keyboard event + event: B::KeyboardKeyEvent, + }, + /// A relative pointer motion occurred + PointerMotion { + /// The pointer motion event + event: B::PointerMotionEvent, + }, + /// An absolute pointer motion occurs + PointerMotionAbsolute { + /// The absolute pointer motion event + event: B::PointerMotionAbsoluteEvent, + }, + /// A pointer button was pressed or released + PointerButton { + /// The pointer button event + event: B::PointerButtonEvent, + }, + /// A pointer action occurred while scrolling on an axis + PointerAxis { + /// The pointer axis event + event: B::PointerAxisEvent, + }, + /// A pointer swipe gesture began + GestureSwipeBegin { + /// The gesture event + event: B::GestureSwipeBeginEvent, + }, + /// A pointer swipe gesture updated + GestureSwipeUpdate { + /// The gesture event + event: B::GestureSwipeUpdateEvent, + }, + /// A pointer swipe gesture ended + GestureSwipeEnd { + /// The gesture event + event: B::GestureSwipeEndEvent, + }, + /// A pointer pinch gesture began + GesturePinchBegin { + /// The gesture event + event: B::GesturePinchBeginEvent, + }, + /// A pointer pinch gesture updated + GesturePinchUpdate { + /// The gesture event + event: B::GesturePinchUpdateEvent, + }, + /// A pointer pinch gesture ended + GesturePinchEnd { + /// The gesture event + event: B::GesturePinchEndEvent, + }, + /// A pointer hold gesture began + GestureHoldBegin { + /// The gesture event + event: B::GestureHoldBeginEvent, + }, + /// A pointer hold gesture ended + GestureHoldEnd { + /// The gesture event + event: B::GestureHoldEndEvent, + }, + /// A new touchpoint appeared + TouchDown { + /// The touch down event + event: B::TouchDownEvent, + }, + /// A touchpoint moved + TouchMotion { + /// The touch motion event + event: B::TouchMotionEvent, + }, + /// A touchpoint was removed + TouchUp { + /// The touch up event + event: B::TouchUpEvent, + }, + /// A touch sequence was cancelled + TouchCancel { + /// The touch cancel event + event: B::TouchCancelEvent, + }, + /// A touch frame was emitted + /// + /// A set of two events received on the same seat between two frames should + /// be interpreted as an atomic event. + TouchFrame { + /// The touch frame event + event: B::TouchFrameEvent, + }, + + /// A tablet tool axis was emitted + TabletToolAxis { + /// The tablet tool axis event + event: B::TabletToolAxisEvent, + }, + + /// A tablet tool proximity was emitted + TabletToolProximity { + /// The tablet tool proximity event + event: B::TabletToolProximityEvent, + }, + + /// A tablet tool tip event was emitted + TabletToolTip { + /// The tablet tool axis event + event: B::TabletToolTipEvent, + }, + + /// A tablet tool button was pressed or released + TabletToolButton { + /// The pointer button event + event: B::TabletToolButtonEvent, + }, + + /// A switch was toggled + SwitchToggle { + /// The switch toggle event + event: B::SwitchToggleEvent, + }, + + /// Special event specific of this backend + Special(B::SpecialEvent), +} + +/// Converts an xorg mouse button to the format used by libinput. +/// +/// Taken from https://sources.debian.org/src/xserver-xorg-input-libinput/1.1.0-1/src/xf86libinput.c/?hl=1508#L236-L252 +#[cfg(any(feature = "backend_winit", feature = "backend_x11"))] +pub(crate) fn xorg_mouse_to_libinput(xorg: u32) -> u32 { + match xorg { + 0 => 0, + 1 => 0x110, // BTN_LEFT + 2 => 0x112, // BTN_MIDDLE + 3 => 0x111, // BTN_RIGHT + _ => xorg - 8 + 0x113, // BTN_SIZE + } +} diff --git a/vendor/smithay/src/backend/input/tablet.rs b/vendor/smithay/src/backend/input/tablet.rs new file mode 100644 index 0000000..6b8992d --- /dev/null +++ b/vendor/smithay/src/backend/input/tablet.rs @@ -0,0 +1,336 @@ +use super::{AbsolutePositionEvent, ButtonState, Event, InputBackend, UnusedEvent}; +use crate::utils::{Logical, Point}; +use bitflags::bitflags; + +/// Description of physical tablet tool +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct TabletToolDescriptor { + /// The tool type is the high-level type of the tool and usually decides the interaction expected from this tool. + pub tool_type: TabletToolType, + /// Unique hardware serial number of the tool + pub hardware_serial: u64, + /// Hardware id in Wacom’s format + pub hardware_id_wacom: u64, + /// Tool capabilities + /// Notifies the client of any capabilities of this tool, beyond the main set of x/y axes and tip up/down detection + pub capabilities: TabletToolCapabilities, +} + +/// Describes the physical type of tool. The physical type of tool generally defines its base usage. +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum TabletToolType { + /// A generic pen. + Pen, + /// Eraser + Eraser, + /// A paintbrush-like tool. + Brush, + /// Physical drawing tool, e.g. Wacom Inking Pen + Pencil, + /// An airbrush-like tool. + Airbrush, + /// A mouse bound to the tablet. + Mouse, + /// A mouse tool with a lens. + Lens, + /// A rotary device with positional and rotation data. + Totem, + /// Type of the device is not known or does not match any known ones + Unknown, +} + +bitflags! { + /// Describes extra capabilities on a tablet. + /// + /// Any tool must provide x and y values, extra axes are device-specific. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct TabletToolCapabilities: u32 { + /// Tilt axes + const TILT = 1; + /// Pressure axis + const PRESSURE = 2; + /// Distance axis + const DISTANCE = 4; + /// Z-rotation axis + const ROTATION = 16; + /// Slider axis + const SLIDER = 32; + /// Wheel axis + const WHEEL = 64; + } +} + +/// Tablet tool event +/// +/// The [AbsolutePositionEvent] implementation for tablet tool events produces (untransformed) +/// coordinates in mm from the top left corner of the tablet in its current logical orientation. +pub trait TabletToolEvent: AbsolutePositionEvent { + /// Get tablet tool that caused this event + fn tool(&self) -> TabletToolDescriptor; + + /// Delta between the last and new pointer device position interpreted as pixel movement + fn delta(&self) -> Point { + (self.delta_x(), self.delta_y()).into() + } + + /// Returns the current tilt along the (X,Y) axis of the tablet's current logical + /// orientation, in degrees off the tablet's z axis. + /// + /// That is, if the tool is perfectly orthogonal to the tablet, the tilt angle is 0. + /// When the top tilts towards the logical top/left of the tablet, the x/y tilt + /// angles are negative, if the top tilts towards the logical bottom/right of the + /// tablet, the x/y tilt angles are positive. + /// + /// If this axis does not exist on the current tool, this function returns (0,0). + fn tilt(&self) -> (f64, f64) { + (self.tilt_x(), self.tilt_y()) + } + + /// Check if the tilt was updated in this event. + fn tilt_has_changed(&self) -> bool { + self.tilt_x_has_changed() || self.tilt_y_has_changed() + } + + /// Delta on the x axis between the last and new pointer device position interpreted as pixel movement + fn delta_x(&self) -> f64; + /// Delta on the y axis between the last and new pointer device position interpreted as pixel movement + fn delta_y(&self) -> f64; + + /// If this axis does not exist on the current tool, this function returns 0. + fn distance(&self) -> f64; + + /// Check if the distance axis was updated in this event. + fn distance_has_changed(&self) -> bool; + + /// Returns the current pressure being applied on the tool in use, normalized to the range [0, 1]. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn pressure(&self) -> f64; + + /// Check if the pressure axis was updated in this event. + fn pressure_has_changed(&self) -> bool; + + /// Returns the current position of the slider on the tool, normalized to the range + /// [-1, 1]. + /// + /// The logical zero is the neutral position of the slider, or the logical center of + /// the axis. This axis is available on e.g. the Wacom Airbrush. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn slider_position(&self) -> f64; + + /// Check if the slider axis was updated in this event. + fn slider_has_changed(&self) -> bool; + + /// Returns the current tilt along the X axis of the tablet's current logical + /// orientation, in degrees off the tablet's z axis. + /// + /// That is, if the tool is perfectly orthogonal to the tablet, the tilt angle is 0. + /// When the top tilts towards the logical top/left of the tablet, the x/y tilt + /// angles are negative, if the top tilts towards the logical bottom/right of the + /// tablet, the x/y tilt angles are positive. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn tilt_x(&self) -> f64; + + /// Check if the tilt x axis was updated in this event. + fn tilt_x_has_changed(&self) -> bool; + + /// Returns the current tilt along the Y axis of the tablet's current logical + /// orientation, in degrees off the tablet's z axis. + /// + /// That is, if the tool is perfectly orthogonal to the tablet, the tilt angle is 0. + /// When the top tilts towards the logical top/left of the tablet, the x/y tilt + /// angles are negative, if the top tilts towards the logical bottom/right of the + /// tablet, the x/y tilt angles are positive. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn tilt_y(&self) -> f64; + + /// Check if the tilt y axis was updated in this event. + fn tilt_y_has_changed(&self) -> bool; + + /// Returns the current z rotation of the tool in degrees, clockwise from the tool's logical neutral position. + /// + /// For tools of type Mouse and Lens the logical neutral position is pointing to the current logical north of the tablet. + /// For tools of type Brush, the logical neutral position is with the buttons pointing up. + /// + /// If this axis does not exist on the current tool, this function returns 0. + fn rotation(&self) -> f64; + + /// Check if the z-rotation axis was updated in this event. + fn rotation_has_changed(&self) -> bool; + + /// Return the delta for the wheel in degrees. + fn wheel_delta(&self) -> f64; + /// Return the delta for the wheel in discrete steps (e.g. wheel clicks). + fn wheel_delta_discrete(&self) -> i32; + /// Check if the wheel axis was updated in this event. + fn wheel_has_changed(&self) -> bool; +} + +impl TabletToolEvent for UnusedEvent { + fn tool(&self) -> TabletToolDescriptor { + match *self {} + } + fn delta_x(&self) -> f64 { + match *self {} + } + fn delta_y(&self) -> f64 { + match *self {} + } + fn distance(&self) -> f64 { + match *self {} + } + fn distance_has_changed(&self) -> bool { + match *self {} + } + fn pressure(&self) -> f64 { + match *self {} + } + fn pressure_has_changed(&self) -> bool { + match *self {} + } + fn slider_position(&self) -> f64 { + match *self {} + } + fn slider_has_changed(&self) -> bool { + match *self {} + } + fn tilt_x(&self) -> f64 { + match *self {} + } + fn tilt_x_has_changed(&self) -> bool { + match *self {} + } + fn tilt_y(&self) -> f64 { + match *self {} + } + fn tilt_y_has_changed(&self) -> bool { + match *self {} + } + fn rotation(&self) -> f64 { + match *self {} + } + fn rotation_has_changed(&self) -> bool { + match *self {} + } + fn wheel_delta(&self) -> f64 { + match *self {} + } + fn wheel_delta_discrete(&self) -> i32 { + match *self {} + } + fn wheel_has_changed(&self) -> bool { + match *self {} + } +} + +/// Trait for axis tablet tool events. +pub trait TabletToolAxisEvent: TabletToolEvent + Event {} + +impl TabletToolAxisEvent for UnusedEvent {} + +/// The state of proximity for a tool on a device. +/// +/// The proximity of a tool is a binary state signalling whether the tool is within a +/// detectable distance of the tablet device. A tool that is out of proximity cannot +/// generate events. +/// +/// On some hardware a tool goes out of proximity when it ceases to touch the surface. On +/// other hardware, the tool is still detectable within a short distance (a few cm) off +/// the surface. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ProximityState { + /// Out of proximity + Out, + /// In proximity + In, +} + +/// Trait for tablet tool proximity events. +pub trait TabletToolProximityEvent: TabletToolEvent + Event { + /// Returns the new proximity state of a tool from a proximity event. + /// + /// Used to check whether or not a tool came in or out of proximity during an + /// `TabletToolProximityEvent`. + /// + /// See [Handling of proximity events](https://wayland.freedesktop.org/libinput/doc/latest/tablet-support.html#tablet-fake-proximity) + /// for recommendations on proximity handling. + fn state(&self) -> ProximityState; +} + +impl TabletToolProximityEvent for UnusedEvent { + fn state(&self) -> ProximityState { + match *self {} + } +} + +/// The tip contact state for a tool on a device. +/// +/// The tip contact state of a tool is a binary state signalling whether the tool is +/// touching the surface of the tablet device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TabletToolTipState { + /// Not touching the surface + Up, + /// Touching the surface + Down, +} + +/// Signals that a tool has come in contact with the surface of a device with the +/// `DeviceCapability::TabletTool` capability. +/// +/// On devices without distance proximity detection, the `TabletToolTipEvent` is sent +/// immediately after `TabletToolProximityEvent` for the tip down event, and +/// immediately before for the tip up event. +/// +/// The decision when a tip touches the surface is device-dependent and may be +/// derived from pressure data or other means. If the tip state is changed by axes +/// changing state, the `TabletToolTipEvent` includes the changed axes and no +/// additional axis event is sent for this state change. In other words, a caller +/// must look at both `TabletToolAxisEvent` and `TabletToolTipEvent` events to know +/// the current state of the axes. +/// +/// If a button state change occurs at the same time as a tip state change, the order +/// of events is device-dependent. +pub trait TabletToolTipEvent: TabletToolEvent + Event { + /// Returns the new tip state of a tool from a tip event. + /// + /// Used to check whether or not a tool came in contact with the tablet surface or + /// left contact with the tablet surface during an `TabletToolTipEvent`. + fn tip_state(&self) -> TabletToolTipState; +} + +impl TabletToolTipEvent for UnusedEvent { + fn tip_state(&self) -> TabletToolTipState { + match *self {} + } +} + +/// Signals that a tool has changed a logical button state on a device with the DeviceCapability::TabletTool capability. +pub trait TabletToolButtonEvent: TabletToolEvent + Event { + /// Return the button that triggered this event. + fn button(&self) -> u32; + + /// For the button of a TabletToolButtonEvent, + /// return the total number of buttons pressed on all devices on the associated seat after the event was triggered. + fn seat_button_count(&self) -> u32; + + /// Return the button state of the event. + fn button_state(&self) -> ButtonState; +} + +impl TabletToolButtonEvent for UnusedEvent { + fn button(&self) -> u32 { + match *self {} + } + + fn seat_button_count(&self) -> u32 { + match *self {} + } + + fn button_state(&self) -> ButtonState { + match *self {} + } +} diff --git a/vendor/smithay/src/backend/libei/input.rs b/vendor/smithay/src/backend/libei/input.rs new file mode 100644 index 0000000..2bbcad5 --- /dev/null +++ b/vendor/smithay/src/backend/libei/input.rs @@ -0,0 +1,298 @@ +// Implementation of `InputBackend` for reis requests + +use reis::{ + eis, + request::{self, DeviceCapability}, +}; +use std::path::PathBuf; + +use crate::backend::input::{self, InputBackend, InputTime}; + +use super::EiInput; + +/// Generic ei scroll event +#[derive(Debug)] +pub enum ScrollEvent { + /// Continuous scroll event + Delta(request::ScrollDelta), + /// Scroll cancel event + Cancel(request::ScrollCancel), + /// Discrete scroll event + Discrete(request::ScrollDiscrete), + /// Scroll stop event + Stop(request::ScrollStop), +} + +impl InputBackend for EiInput { + type Device = request::Device; + type KeyboardKeyEvent = request::KeyboardKey; + type PointerAxisEvent = ScrollEvent; + type PointerButtonEvent = request::Button; + type PointerMotionEvent = request::PointerMotion; + type PointerMotionAbsoluteEvent = request::PointerMotionAbsolute; + + type GestureSwipeBeginEvent = input::UnusedEvent; + type GestureSwipeUpdateEvent = input::UnusedEvent; + type GestureSwipeEndEvent = input::UnusedEvent; + type GesturePinchBeginEvent = input::UnusedEvent; + type GesturePinchUpdateEvent = input::UnusedEvent; + type GesturePinchEndEvent = input::UnusedEvent; + type GestureHoldBeginEvent = input::UnusedEvent; + type GestureHoldEndEvent = input::UnusedEvent; + + type TouchDownEvent = request::TouchDown; + type TouchUpEvent = request::TouchUp; + type TouchMotionEvent = request::TouchMotion; + type TouchCancelEvent = request::TouchCancel; + type TouchFrameEvent = request::Frame; + + type TabletToolAxisEvent = input::UnusedEvent; + type TabletToolProximityEvent = input::UnusedEvent; + type TabletToolTipEvent = input::UnusedEvent; + type TabletToolButtonEvent = input::UnusedEvent; + + type SwitchToggleEvent = input::UnusedEvent; + + type SpecialEvent = input::UnusedEvent; +} + +impl input::Device for request::Device { + fn id(&self) -> String { + use reis::Interface; + let object = self.device().as_object(); + let id = object.id(); + format!("ei-{id}") + } + + fn name(&self) -> String { + self.name().unwrap_or("").to_string() + } + + fn has_capability(&self, capability: input::DeviceCapability) -> bool { + match capability { + input::DeviceCapability::Gesture => false, + input::DeviceCapability::Keyboard => self.has_capability(DeviceCapability::Keyboard), + input::DeviceCapability::Pointer => { + self.has_capability(DeviceCapability::Pointer) + || self.has_capability(DeviceCapability::PointerAbsolute) + } + input::DeviceCapability::Switch => false, + input::DeviceCapability::TabletPad => false, + input::DeviceCapability::TabletTool => false, + input::DeviceCapability::Touch => self.has_capability(DeviceCapability::Touch), + } + } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option { + None + } +} + +impl input::Event for T { + fn time(&self) -> InputTime { + InputTime::from_micros(request::EventTime::time(self)) + } + + fn device(&self) -> request::Device { + request::DeviceEvent::device(self).clone() + } +} + +impl input::TouchFrameEvent for request::Frame {} + +impl input::KeyboardKeyEvent for request::KeyboardKey { + fn key_code(&self) -> input::Keycode { + input::Keycode::from(self.key + 8) + } + + fn state(&self) -> input::KeyState { + match self.state { + eis::keyboard::KeyState::Released => input::KeyState::Released, + eis::keyboard::KeyState::Press => input::KeyState::Pressed, + } + } + + fn count(&self) -> u32 { + 1 + } +} + +impl input::Event for ScrollEvent { + fn time(&self) -> InputTime { + match self { + Self::Delta(evt) => evt.time(), + Self::Cancel(evt) => evt.time(), + Self::Discrete(evt) => evt.time(), + Self::Stop(evt) => evt.time(), + } + } + + fn device(&self) -> request::Device { + match self { + Self::Delta(evt) => evt.device(), + Self::Cancel(evt) => evt.device(), + Self::Discrete(evt) => evt.device(), + Self::Stop(evt) => evt.device(), + } + } +} + +impl input::PointerAxisEvent for ScrollEvent { + fn amount(&self, axis: input::Axis) -> Option { + match self { + Self::Delta(evt) => match axis { + input::Axis::Horizontal if evt.dx != 0.0 => Some(evt.dx.into()), + input::Axis::Vertical if evt.dy != 0.0 => Some(evt.dy.into()), + _ => None, + }, + // Same as Mutter + Self::Cancel(evt) => match axis { + input::Axis::Horizontal if evt.x => Some(0.01), + input::Axis::Vertical if evt.y => Some(0.01), + _ => None, + }, + Self::Discrete(_evt) => None, + Self::Stop(evt) => match axis { + input::Axis::Horizontal if evt.x => Some(0.0), + input::Axis::Vertical if evt.y => Some(0.0), + _ => None, + }, + } + } + + fn amount_v120(&self, axis: input::Axis) -> Option { + match self { + Self::Discrete(evt) => match axis { + input::Axis::Horizontal if evt.discrete_dx != 0 => Some(evt.discrete_dx.into()), + input::Axis::Vertical if evt.discrete_dy != 0 => Some(evt.discrete_dy.into()), + _ => None, + }, + _ => None, + } + } + + fn source(&self) -> input::AxisSource { + // Mutter seems to also use wheel for all the scroll events + input::AxisSource::Wheel + } + + fn relative_direction(&self, _axis: input::Axis) -> input::AxisRelativeDirection { + input::AxisRelativeDirection::Identical + } +} + +impl input::PointerButtonEvent for request::Button { + fn button_code(&self) -> u32 { + self.button + } + + fn state(&self) -> input::ButtonState { + match self.state { + eis::button::ButtonState::Press => input::ButtonState::Pressed, + eis::button::ButtonState::Released => input::ButtonState::Released, + } + } +} + +impl input::PointerMotionEvent for request::PointerMotion { + fn delta_x(&self) -> f64 { + self.dx.into() + } + + fn delta_y(&self) -> f64 { + self.dy.into() + } + + fn delta_x_unaccel(&self) -> f64 { + self.dx.into() + } + + fn delta_y_unaccel(&self) -> f64 { + self.dy.into() + } +} + +impl input::PointerMotionAbsoluteEvent for request::PointerMotionAbsolute {} +impl input::AbsolutePositionEvent for request::PointerMotionAbsolute { + fn x(&self) -> f64 { + self.dx_absolute.into() + } + + fn y(&self) -> f64 { + self.dy_absolute.into() + } + + fn x_transformed(&self, _width: i32) -> f64 { + self.dx_absolute.into() + } + + fn y_transformed(&self, _height: i32) -> f64 { + self.dy_absolute.into() + } +} + +impl input::TouchDownEvent for request::TouchDown {} +impl input::TouchEvent for request::TouchDown { + fn slot(&self) -> input::TouchSlot { + Some(self.touch_id).into() + } +} +impl input::AbsolutePositionEvent for request::TouchDown { + fn x(&self) -> f64 { + self.x.into() + } + + fn y(&self) -> f64 { + self.y.into() + } + + fn x_transformed(&self, _width: i32) -> f64 { + self.x.into() + } + + fn y_transformed(&self, _height: i32) -> f64 { + self.y.into() + } +} + +impl input::TouchUpEvent for request::TouchUp {} +impl input::TouchEvent for request::TouchUp { + fn slot(&self) -> input::TouchSlot { + Some(self.touch_id).into() + } +} + +impl input::TouchMotionEvent for request::TouchMotion {} +impl input::TouchEvent for request::TouchMotion { + fn slot(&self) -> input::TouchSlot { + Some(self.touch_id).into() + } +} +impl input::AbsolutePositionEvent for request::TouchMotion { + fn x(&self) -> f64 { + self.x.into() + } + + fn y(&self) -> f64 { + self.y.into() + } + + fn x_transformed(&self, _width: i32) -> f64 { + self.x.into() + } + + fn y_transformed(&self, _height: i32) -> f64 { + self.y.into() + } +} + +impl input::TouchCancelEvent for request::TouchCancel {} +impl input::TouchEvent for request::TouchCancel { + fn slot(&self) -> input::TouchSlot { + Some(self.touch_id).into() + } +} diff --git a/vendor/smithay/src/backend/libei/mod.rs b/vendor/smithay/src/backend/libei/mod.rs new file mode 100644 index 0000000..355108c --- /dev/null +++ b/vendor/smithay/src/backend/libei/mod.rs @@ -0,0 +1,323 @@ +//! Input backend for `libei` sender contexts +//! +//! ``` no_run +//! # let event_loop: calloop::EventLoop<()> = todo!(); +//! +//! use reis::{calloop::EisListenerSource, eis}; +//! use smithay::{ +//! backend::libei::{EiInput, EiInputEvent}, +//! input::keyboard::XkbConfig, +//! }; +//! +//! let listener = eis::Listener::bind_auto().unwrap(); +//! let listener_source = EisListenerSource::new(listener); +//! let handle = event_loop.handle(); +//! event_loop.handle().insert_source(listener_source, |context, _, _| { +//! let source = EiInput::new(context); +//! handle.insert_source(source, |event, connection, _data| { +//! match event { +//! EiInputEvent::Connected => { +//! let seat = connection.add_seat("default"); +//! let _ = seat.add_keyboard("virtual keyboard", XkbConfig::default()); +//! seat.add_pointer("virtual pointer"); +//! seat.add_pointer_absolute("virtual absolute pointer", &[]); +//! seat.add_touch("virtual touch", &[]); +//! } +//! EiInputEvent::Disconnected => {} +//! EiInputEvent::Event(event) => { +//! // Pass input event to compositor's input event handling logic +//! // ... +//! } +//! EiInputEvent::TextKeysym { .. } | EiInputEvent::TextUtf8 { .. } => {} +//! } +//! }).unwrap(); +//! Ok(calloop::PostAction::Continue) +//! }).unwrap(); +//! ``` + +// TODO: Add helper for receiver contexts + +use calloop::{EventSource, PostAction, Readiness, Token, TokenFactory}; +use reis::{ + calloop::EisRequestSourceEvent, + eis, + request::{DeviceCapability, EisRequest}, +}; +use std::{ + io, + sync::{Arc, Mutex}, +}; + +use crate::backend::input::{InputEvent, KeyState}; + +mod input; +pub use input::ScrollEvent; +mod seat; +pub use seat::{EiInputSeat, EiRegion}; + +/// An [`EventSource`] for receiving input from an EI sender context and +/// converting to [`InputEvent`]s. +#[derive(Debug)] +pub struct EiInput { + source: reis::calloop::EisRequestSource, + connection: Option, + event_sender: calloop::channel::Sender>, + channel_source: calloop::channel::Channel>, +} + +impl EiInput { + /// Create an EI sender event source. + /// + /// `context` should be a new EI socket that has not been used yet. + pub fn new(context: eis::Context) -> Self { + let (event_sender, channel_source) = calloop::channel::channel(); + Self { + source: reis::calloop::EisRequestSource::new(context, 0), + event_sender, + channel_source, + connection: None, + } + } +} + +/// A connection for an EI sender context that can be used to add seats and +/// devices. +#[derive(Debug)] +pub struct EiInputConnection(Arc); + +#[derive(Debug)] +struct EiInputConnectionInner { + connection: reis::request::Connection, + event_sender: calloop::channel::Sender>, + seats: Mutex>, +} + +impl EiInputConnection { + fn new( + connection: reis::request::Connection, + event_sender: calloop::channel::Sender>, + ) -> Self { + Self(Arc::new(EiInputConnectionInner { + connection, + event_sender, + seats: Mutex::new(Vec::new()), + })) + } + + /// Add a seat to the EI connection + pub fn add_seat(&self, name: &str) -> EiInputSeat { + let seat = self.0.connection.add_seat( + Some(name), + // Capabilities can't be added to a seat; so advertise them + // all, but only create relevant devices on bind. + DeviceCapability::Pointer + | DeviceCapability::PointerAbsolute + | DeviceCapability::Keyboard + | DeviceCapability::Touch + | DeviceCapability::Scroll + | DeviceCapability::Button + | DeviceCapability::Text, + ); + let seat = EiInputSeat::new(self, seat, self.0.event_sender.clone()); + self.0.seats.lock().unwrap().push(seat.clone()); + seat + } + + /// Send buffered events on EI socket + pub fn flush(&self) -> rustix::io::Result<()> { + self.0.connection.flush() + } + + /// Returns the underlying `eis` connection. + pub fn eis_connection(&self) -> &eis::Connection { + self.0.connection.connection() + } +} + +/// An event produced by an [`EiInput`] event source. +#[derive(Debug)] +pub enum EiInputEvent { + /// The client has finished the EI handshake. Seats and devices can + /// then be added. + Connected, + /// The client has disconnected from the server. + Disconnected, + /// An input event has been received from the client. + Event(InputEvent), + /// The client injected a keysym via the `ei_text` interface. Unlike [`InputEvent::Keyboard`] + /// (which carries a keycode), this is keymap-independent; the compositor is responsible for + /// turning the keysym into input. + TextKeysym { + /// The XKB keysym. + keysym: u32, + /// Whether the keysym is pressed or released. + state: KeyState, + }, + /// The client injected UTF-8 text via the `ei_text` interface. + TextUtf8 { + /// The UTF-8 text. + text: String, + }, +} + +impl EventSource for EiInput { + type Event = EiInputEvent; + type Metadata = EiInputConnection; + type Ret = (); + type Error = io::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut cb: F, + ) -> Result::Error> + where + F: FnMut(EiInputEvent, &mut EiInputConnection), + { + let _ = self.channel_source.process_events(readiness, token, |event, ()| { + if let calloop::channel::Event::Msg(event) = event { + // Can't create device until there's a connection, so no channel messages + let connection = self.connection.as_mut().unwrap(); + cb(EiInputEvent::Event(event), connection); + } + }); + self.source.process_events(readiness, token, |event, connection| { + // Wrap connection in `EiInputConnection` if not created yet + if self.connection.is_none() { + self.connection = Some(EiInputConnection::new( + connection.clone(), + self.event_sender.clone(), + )); + } + let connection = self.connection.as_mut().unwrap(); + + match event { + Ok(EisRequestSourceEvent::Connected) => { + if connection.0.connection.context_type() == eis::handshake::ContextType::Receiver { + connection + .0 + .connection + .disconnected(eis::connection::DisconnectReason::Disconnected, None); + let _ = connection.flush(); + return Ok(PostAction::Remove); + } + cb(EiInputEvent::Connected, connection); + } + Ok(EisRequestSourceEvent::Request(EisRequest::Disconnect)) => { + cb(EiInputEvent::Disconnected, connection); + return Ok(PostAction::Remove); + } + Ok(EisRequestSourceEvent::Request(EisRequest::Bind(request))) => { + if let Some(seat) = connection + .0 + .seats + .lock() + .unwrap() + .iter() + .find(|seat| **seat == request.seat) + { + seat.bind(request.capabilities); + } + } + Ok(EisRequestSourceEvent::Request(EisRequest::TextKeysym(event))) => { + let state = match event.state { + eis::keyboard::KeyState::Press => KeyState::Pressed, + eis::keyboard::KeyState::Released => KeyState::Released, + }; + cb( + EiInputEvent::TextKeysym { + keysym: event.keysym, + state, + }, + connection, + ); + } + Ok(EisRequestSourceEvent::Request(EisRequest::TextUtf8(event))) => { + cb(EiInputEvent::TextUtf8 { text: event.text }, connection); + } + Ok(EisRequestSourceEvent::Request(request)) => { + if let Some(input_event) = convert_request(request) { + cb(EiInputEvent::Event(input_event), connection); + } + } + Err(err) => { + cb(EiInputEvent::Disconnected, connection); + tracing::error!("Libei client error: {}", err); + return Ok(PostAction::Remove); + } + } + let _ = connection.flush(); + Ok(PostAction::Continue) + }) + } + + fn register( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut TokenFactory, + ) -> Result<(), calloop::Error> { + self.channel_source.register(poll, token_factory)?; + self.source.register(poll, token_factory) + } + + fn reregister( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut TokenFactory, + ) -> Result<(), calloop::Error> { + self.channel_source.reregister(poll, token_factory)?; + self.source.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut calloop::Poll) -> Result<(), calloop::Error> { + self.channel_source.unregister(poll)?; + self.source.unregister(poll) + } +} + +fn convert_request(request: EisRequest) -> Option> { + match request { + EisRequest::KeyboardKey(event) => Some(InputEvent::Keyboard { event }), + EisRequest::PointerMotion(event) => Some(InputEvent::PointerMotion { event }), + EisRequest::PointerMotionAbsolute(event) => Some(InputEvent::PointerMotionAbsolute { event }), + EisRequest::Button(event) => Some(InputEvent::PointerButton { event }), + EisRequest::ScrollDelta(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Delta(event), + }), + EisRequest::ScrollStop(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Stop(event), + }), + EisRequest::ScrollCancel(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Cancel(event), + }), + EisRequest::ScrollDiscrete(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Discrete(event), + }), + EisRequest::TouchDown(event) => Some(InputEvent::TouchDown { event }), + EisRequest::TouchUp(event) => Some(InputEvent::TouchUp { event }), + EisRequest::TouchMotion(event) => Some(InputEvent::TouchMotion { event }), + EisRequest::TouchCancel(event) => Some(InputEvent::TouchCancel { event }), + EisRequest::DeviceClosed(event) => Some(InputEvent::DeviceRemoved { device: event.device }), + // `ei_device.frame` is not touch-specific: it commits whatever the client has sent + // for that device. The capability check is sufficient here because reis only emits + // a frame for a device that had pending events of its own, and every device we + // create has a single capability. So a frame on a touch-capable device + // necessarily contains touch events. This would need revisiting if a device ever + // advertised `Touch` alongside another capability. + EisRequest::Frame(event) => event + .device + .has_capability(reis::request::DeviceCapability::Touch) + .then_some(InputEvent::TouchFrame { event }), + // `TextKeysym`/`TextUtf8` are surfaced as `EiInputEvent::TextKeysym`/`TextUtf8` directly, + // so they never reach here. + EisRequest::TextKeysym(_) + | EisRequest::TextUtf8(_) + | EisRequest::Disconnect + | EisRequest::Bind(_) + | EisRequest::RequestDevice(_) + | EisRequest::Ready(_) + | EisRequest::DeviceStartEmulating(_) + | EisRequest::DeviceStopEmulating(_) => None, + } +} diff --git a/vendor/smithay/src/backend/libei/seat.rs b/vendor/smithay/src/backend/libei/seat.rs new file mode 100644 index 0000000..c435a74 --- /dev/null +++ b/vendor/smithay/src/backend/libei/seat.rs @@ -0,0 +1,433 @@ +use reis::{eis::device::DeviceType, enumflags2::BitFlags, request::DeviceCapability}; +use std::sync::{Arc, Mutex, Weak}; +use xkbcommon::xkb; + +use crate::{ + backend::input::InputEvent, + input::keyboard::{KeymapFile, XkbConfig}, + utils::{Logical, Rectangle}, +}; + +use super::{EiInput, EiInputConnection, EiInputConnectionInner}; + +/// A region advertised on an EI absolute pointer device. +/// +/// Describes a rectangular coordinate space the client may point within, in the +/// compositor's logical coordinate system. Clients that capture at a different +/// (e.g. physical) resolution use `scale` to convert their coordinates into the +/// logical space the compositor expects. +/// +/// `rect` is the logical offset and size of the region; `scale` the +/// physical-to-logical scale of the underlying output (e.g. `2.0` for a 200% +/// display). `mapping_id`, when set, identifies which screen/stream the region +/// maps to (it must match the `mapping_id` the client sees for the corresponding +/// screencast stream), letting a client correlate a region with the video it shows. +#[derive(Clone, Debug, PartialEq)] +pub struct EiRegion { + /// Logical offset and size of the region. + pub rect: Rectangle, + /// Physical-to-logical scale of the region (e.g. `2.0` at 200%). + pub scale: f32, + /// Identifier tying this region to a screencast stream (e.g. the output name). + pub mapping_id: Option, +} + +/// A seat advertised on an EI sender context +#[derive(Clone, Debug)] +pub struct EiInputSeat(Arc>); + +impl PartialEq for EiInputSeat { + fn eq(&self, other: &reis::request::Seat) -> bool { + self.0.lock().unwrap().seat == *other + } +} + +impl EiInputSeat { + pub(super) fn new( + connection: &EiInputConnection, + seat: reis::request::Seat, + event_sender: calloop::channel::Sender>, + ) -> Self { + Self(Arc::new(Mutex::new(EiInputSeatInner { + connection: Arc::downgrade(&connection.0), + seat, + event_sender, + keyboard: None, + pointer: None, + pointer_absolute: None, + regions: Vec::new(), + touch: None, + text: None, + device_keyboard: None, + device_pointer: None, + device_pointer_absolute: None, + device_touch: None, + device_text: None, + bound_capabilities: BitFlags::empty(), + }))) + } + + pub(super) fn bind(&self, capabilities: BitFlags) { + let mut inner = self.0.lock().unwrap(); + inner.bound_capabilities = capabilities; + inner.refresh_devices(); + } + + /// Add a keyboard device to the EI seat + /// + /// Calling on a seat that already has a keyboard device will remove + /// that device and add a new one. + pub fn add_keyboard( + &self, + name: &str, + xkb_config: XkbConfig<'_>, + ) -> Result<(), crate::input::keyboard::Error> { + let mut inner = self.0.lock().unwrap(); + inner.device_keyboard = None; + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let keymap = xkb_config.compile_keymap(&context).map_err(|_| { + tracing::debug!("Loading keymap from XkbConfig failed"); + crate::input::keyboard::Error::BadKeymap + })?; + let keymap_file = KeymapFile::new(&keymap); + inner.keyboard = Some((name.to_string(), keymap_file)); + inner.refresh_devices(); + Ok(()) + } + + /// Remove keyboard device from the EI seat + pub fn remove_keyboard(&self) { + let mut inner = self.0.lock().unwrap(); + inner.device_keyboard = None; + inner.keyboard = None; + inner.flush(); + } + + /// Send the compositor's current modifier state to this seat's keyboard device via + /// `ei_keyboard.modifiers`, so the client keeps its keyboard state in sync with the + /// compositor (mirroring `wl_keyboard.modifiers`). No-op if there is no bound keyboard. + pub fn keyboard_modifiers(&self, depressed: u32, locked: u32, latched: u32, group: u32) { + let inner = self.0.lock().unwrap(); + let Some(device) = inner.device_keyboard.as_ref() else { + return; + }; + let Some(keyboard) = device.device.interface::() else { + return; + }; + let Some(connection) = inner.connection.upgrade() else { + return; + }; + connection.connection.with_next_serial(|serial| { + keyboard.modifiers(serial, depressed, locked, latched, group); + }); + let _ = connection.connection.flush(); + } + + /// Add a pointer device to the EI seat + /// + /// The EI device will have button and scroll capabilities in addition + /// to the pointer capability. + /// + /// Calling on a seat that already has a pointer device will remove + /// that device and add a new one. + pub fn add_pointer(&self, name: &str) { + let mut inner = self.0.lock().unwrap(); + inner.device_pointer = None; + inner.pointer = Some(name.to_string()); + inner.refresh_devices(); + } + + /// Remove pointer device from the EI seat + pub fn remove_pointer(&self) { + let mut inner = self.0.lock().unwrap(); + inner.device_pointer = None; + inner.pointer = None; + inner.flush(); + } + + /// Add an absolute pointer device to the EI seat + /// + /// The EI device will have button and scroll capabilities in addition + /// to the pointer absolute capability. + /// + /// `regions` describes the logical coordinate space(s) the client may point + /// within (see [`EiRegion`]). Pass an empty slice to advertise no region, in + /// which case the client must guess the coordinate space. Advertising a + /// region lets clients that capture at a different resolution (e.g. physical + /// pixels on a HiDPI output) convert their coordinates correctly. + /// + /// Calling on a seat that already has an absolute pointer device will remove + /// that device and add a new one. + pub fn add_pointer_absolute(&self, name: &str, regions: &[EiRegion]) { + let mut inner = self.0.lock().unwrap(); + inner.device_pointer_absolute = None; + inner.pointer_absolute = Some(name.to_string()); + inner.regions = regions.to_vec(); + inner.refresh_devices(); + } + + /// Remove absolute pointer device from the EI seat + pub fn remove_pointer_absolute(&self) { + let mut inner = self.0.lock().unwrap(); + inner.device_pointer_absolute = None; + inner.pointer_absolute = None; + inner.flush(); + } + + /// Add a touch device to the EI seat + /// + /// `regions` describes the logical coordinate space(s) the client may send absolute touch + /// coordinates in (see [`EiRegion`]). + /// + /// Calling on a seat that already has a touch device will remove + /// that device and add a new one. + pub fn add_touch(&self, name: &str, regions: &[EiRegion]) { + let mut inner = self.0.lock().unwrap(); + inner.device_touch = None; + inner.touch = Some(name.to_string()); + inner.regions = regions.to_vec(); + inner.refresh_devices(); + } + + /// Update the coordinate regions advertised to the client, recreating whichever absolute + /// devices (absolute pointer, touch) this seat already has. + /// + /// Regions are immutable once a device is created, so the devices have to be replaced for a + /// client to see a new output layout/scale. + pub fn update_regions(&self, regions: &[EiRegion]) { + let mut inner = self.0.lock().unwrap(); + inner.regions = regions.to_vec(); + if inner.pointer_absolute.is_some() { + inner.device_pointer_absolute = None; + } + if inner.touch.is_some() { + inner.device_touch = None; + } + inner.refresh_devices(); + } + + /// Remove touch device from the EI seat + pub fn remove_touch(&self) { + let mut inner = self.0.lock().unwrap(); + inner.device_touch = None; + inner.touch = None; + inner.flush(); + } + + /// Add a text device to the EI seat. + /// + /// A text device lets clients inject input by keysym or UTF-8 string (`ei_text`), independent + /// of any keymap. Calling on a seat that already has a text device will remove that device and + /// add a new one. + pub fn add_text(&self, name: &str) { + let mut inner = self.0.lock().unwrap(); + inner.device_text = None; + inner.text = Some(name.to_string()); + inner.refresh_devices(); + } + + /// Remove text device from the EI seat + pub fn remove_text(&self) { + let mut inner = self.0.lock().unwrap(); + inner.device_text = None; + inner.text = None; + inner.flush(); + } + + /// Remove seat from EI connection + pub fn remove(&self) { + let inner = self.0.lock().unwrap(); + inner.seat.remove(); + if let Some(connection) = inner.connection.upgrade() { + let mut seats = connection.seats.lock().unwrap(); + if let Some(idx) = seats.iter().position(|s| Arc::ptr_eq(&s.0, &self.0)) { + seats.remove(idx); + } + } + } +} + +#[derive(Debug)] +struct EiInputSeatInner { + connection: Weak, + seat: reis::request::Seat, + bound_capabilities: BitFlags, + event_sender: calloop::channel::Sender>, + // Interfaces advertised by the server + keyboard: Option<(String, KeymapFile)>, + pointer: Option, + pointer_absolute: Option, + // Regions advertised on the absolute pointer device, describing the logical + // coordinate space(s) the client may address (see `EiRegion`). + regions: Vec, + touch: Option, + text: Option, + // Devices created in response to client bind + device_keyboard: Option, + device_pointer: Option, + device_pointer_absolute: Option, + device_touch: Option, + device_text: Option, +} + +impl EiInputSeatInner { + /// Flush this seat's connection. + /// + /// Device add/remove (and the `ei_device.destroyed` a removal emits) are usually triggered + /// from *compositor* state changes (an output layout/scale change, a keymap change). + /// Without an explicit flush here those messages would sit in the output buffer until the client + /// happened to send an unrelated event, so a client that is only listening (not injecting) + /// would never learn its devices changed. + fn flush(&self) { + if let Some(connection) = self.connection.upgrade() { + let _ = connection.connection.flush(); + } + } + + // Add any devices the server provides, for capabilities the client has bound, if no device yet + fn refresh_devices(&mut self) { + if self.device_keyboard.is_none() && self.bound_capabilities.contains(DeviceCapability::Keyboard) { + if let Some((name, keymap_file)) = self.keyboard.as_ref() { + let device = self.seat.add_device( + Some(name), + DeviceType::Virtual, + DeviceCapability::Keyboard.into(), + |device| { + let keyboard = device.interface::().unwrap(); + let _ = keymap_file.with_fd(true, |fd, len| { + keyboard.keymap(reis::eis::keyboard::KeymapType::Xkb, len as u32, fd); + }); + }, + ); + device.resumed(); + let _ = self.event_sender.send(InputEvent::DeviceAdded { + device: device.clone(), + }); + self.device_keyboard = Some(DeviceDropWrapper::new(device, &self.event_sender)); + } + } + + if self.device_pointer.is_none() && self.bound_capabilities.contains(DeviceCapability::Pointer) { + if let Some(name) = self.pointer.as_ref() { + let device = self.seat.add_device( + Some(name), + DeviceType::Virtual, + DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll, + |_| {}, + ); + device.resumed(); + let _ = self.event_sender.send(InputEvent::DeviceAdded { + device: device.clone(), + }); + self.device_pointer = Some(DeviceDropWrapper::new(device, &self.event_sender)); + } + } + + if self.device_pointer_absolute.is_none() + && self + .bound_capabilities + .contains(DeviceCapability::PointerAbsolute) + { + if let Some(name) = self.pointer_absolute.as_ref() { + let regions = self.regions.clone(); + let device = self.seat.add_device( + Some(name), + DeviceType::Virtual, + DeviceCapability::PointerAbsolute | DeviceCapability::Button | DeviceCapability::Scroll, + |device| advertise_regions(device, ®ions), + ); + device.resumed(); + let _ = self.event_sender.send(InputEvent::DeviceAdded { + device: device.clone(), + }); + self.device_pointer_absolute = Some(DeviceDropWrapper::new(device, &self.event_sender)); + } + } + + if self.device_touch.is_none() && self.bound_capabilities.contains(DeviceCapability::Touch) { + if let Some(name) = self.touch.as_ref() { + let regions = self.regions.clone(); + let device = self.seat.add_device( + Some(name), + DeviceType::Virtual, + DeviceCapability::Touch.into(), + |device| advertise_regions(device, ®ions), + ); + device.resumed(); + let _ = self.event_sender.send(InputEvent::DeviceAdded { + device: device.clone(), + }); + self.device_touch = Some(DeviceDropWrapper::new(device, &self.event_sender)); + } + } + + if self.device_text.is_none() && self.bound_capabilities.contains(DeviceCapability::Text) { + if let Some(name) = self.text.as_ref() { + let device = self.seat.add_device( + Some(name), + DeviceType::Virtual, + DeviceCapability::Text.into(), + |_| {}, + ); + device.resumed(); + let _ = self.event_sender.send(InputEvent::DeviceAdded { + device: device.clone(), + }); + self.device_text = Some(DeviceDropWrapper::new(device, &self.event_sender)); + } + } + + // Deliver any device add/remove produced above to the client (see `flush`). + self.flush(); + } +} + +/// Advertise the coordinate space(s) a client may address on an absolute device. +/// +/// These `ei_device.region` events must be sent after the device is created but before `done`, +/// i.e. from within `add_device`'s callback. +fn advertise_regions(device: &reis::request::Device, regions: &[EiRegion]) { + for region in regions { + // `region_mapping_id` applies to the region created by the next `region` request, + // so send it first when present. + if let Some(mapping_id) = ®ion.mapping_id { + device.device().region_mapping_id(mapping_id); + } + // `ei_device.region` coordinates are unsigned + device.device().region( + region.rect.loc.x.max(0) as u32, + region.rect.loc.y.max(0) as u32, + region.rect.size.w.max(0) as u32, + region.rect.size.h.max(0) as u32, + region.scale, + ); + } +} + +// Helper that remove the device on drop, and send `DeviceRemoved` +#[derive(Debug)] +struct DeviceDropWrapper { + device: reis::request::Device, + event_sender: calloop::channel::Sender>, +} + +impl DeviceDropWrapper { + fn new( + device: reis::request::Device, + event_sender: &calloop::channel::Sender>, + ) -> Self { + Self { + device, + event_sender: event_sender.clone(), + } + } +} + +impl Drop for DeviceDropWrapper { + fn drop(&mut self) { + let _ = self.event_sender.send(InputEvent::DeviceRemoved { + device: self.device.clone(), + }); + self.device.remove(); + } +} diff --git a/vendor/smithay/src/backend/libinput/mod.rs b/vendor/smithay/src/backend/libinput/mod.rs new file mode 100644 index 0000000..4ab39dd --- /dev/null +++ b/vendor/smithay/src/backend/libinput/mod.rs @@ -0,0 +1,891 @@ +//! Implementation of input backend trait for types provided by `libinput` + +use crate::backend::input::{ + self as backend, Axis, AxisRelativeDirection, AxisSource, InputBackend, InputEvent, InputTime, +}; +#[cfg(feature = "backend_session")] +use crate::backend::session::{AsErrno, Session}; +use input as libinput; +use input::event; + +use std::{ + io, + os::unix::io::{AsFd, BorrowedFd}, + path::PathBuf, +}; +#[cfg(feature = "backend_session")] +use std::{os::unix::io::OwnedFd, path::Path}; + +use calloop::{EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory}; + +use tracing::{debug_span, info, trace}; + +mod tablet; + +/// Libinput based [`InputBackend`]. +/// +/// Tracks input of all devices given manually or via a udev seat to a provided libinput +/// context. +#[derive(Debug)] +pub struct LibinputInputBackend { + context: libinput::Libinput, + token: Option, + span: tracing::Span, +} + +impl LibinputInputBackend { + /// Initialize a new [`LibinputInputBackend`] from a given already initialized + /// [libinput context](libinput::Libinput). + pub fn new(context: libinput::Libinput) -> Self { + let span = debug_span!("backend_libinput"); + let _guard = span.enter(); + + info!("Initializing a libinput backend"); + + drop(_guard); + LibinputInputBackend { + context, + token: None, + span, + } + } + + /// Returns a reference to the underlying libinput context + pub fn context(&self) -> &libinput::Libinput { + &self.context + } +} + +impl backend::Device for libinput::Device { + fn id(&self) -> String { + self.sysname().into() + } + + fn name(&self) -> String { + self.name().into() + } + + fn has_capability(&self, capability: backend::DeviceCapability) -> bool { + libinput::Device::has_capability(self, capability.into()) + } + + fn usb_id(&self) -> Option<(u32, u32)> { + Some(( + libinput::Device::id_product(self), + libinput::Device::id_vendor(self), + )) + } + + fn syspath(&self) -> Option { + #[cfg(feature = "udev")] + return unsafe { libinput::Device::udev_device(self) }.map(|d| d.syspath().to_owned()); + + #[cfg(not(feature = "udev"))] + None + } +} + +impl From for libinput::DeviceCapability { + #[inline] + fn from(other: backend::DeviceCapability) -> libinput::DeviceCapability { + match other { + backend::DeviceCapability::Gesture => libinput::DeviceCapability::Gesture, + backend::DeviceCapability::Keyboard => libinput::DeviceCapability::Keyboard, + backend::DeviceCapability::Pointer => libinput::DeviceCapability::Pointer, + backend::DeviceCapability::Switch => libinput::DeviceCapability::Switch, + backend::DeviceCapability::TabletPad => libinput::DeviceCapability::TabletPad, + backend::DeviceCapability::TabletTool => libinput::DeviceCapability::TabletTool, + backend::DeviceCapability::Touch => libinput::DeviceCapability::Touch, + } + } +} + +impl backend::Event for event::keyboard::KeyboardKeyEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::keyboard::KeyboardEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::KeyboardKeyEvent for event::keyboard::KeyboardKeyEvent { + fn key_code(&self) -> backend::Keycode { + use input::event::keyboard::KeyboardEventTrait; + (self.key() + 8).into() + } + + fn state(&self) -> backend::KeyState { + use input::event::keyboard::KeyboardEventTrait; + self.key_state().into() + } + + fn count(&self) -> u32 { + self.seat_key_count() + } +} + +impl backend::Event for event::switch::SwitchToggleEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::switch::SwitchEventTrait::time_usec(self)) + } + + fn device(&self) -> ::Device { + event::EventTrait::device(self) + } +} + +impl backend::SwitchToggleEvent for event::switch::SwitchToggleEvent { + fn switch(&self) -> Option { + event::switch::SwitchToggleEvent::switch(self).and_then(|switch| { + Some(match switch { + event::switch::Switch::Lid => backend::Switch::Lid, + event::switch::Switch::TabletMode => backend::Switch::TabletMode, + _ => return None, + }) + }) + } + + fn state(&self) -> backend::SwitchState { + match event::switch::SwitchToggleEvent::switch_state(self) { + event::switch::SwitchState::Off => backend::SwitchState::Off, + event::switch::SwitchState::On => backend::SwitchState::On, + } + } +} + +/// Generic pointer scroll event from libinput +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum PointerScrollAxis { + /// Scroll event from pointer wheel + Wheel(event::pointer::PointerScrollWheelEvent), + /// Scroll event from pointer finger + Finger(event::pointer::PointerScrollFingerEvent), + /// Continuous scroll event + Continuous(event::pointer::PointerScrollContinuousEvent), +} + +impl PointerScrollAxis { + fn has_axis(&self, axis: event::pointer::Axis) -> bool { + use input::event::pointer::PointerScrollEvent; + match self { + Self::Wheel(evt) => evt.has_axis(axis), + Self::Finger(evt) => evt.has_axis(axis), + Self::Continuous(evt) => evt.has_axis(axis), + } + } +} + +impl backend::Event for PointerScrollAxis { + fn time(&self) -> InputTime { + InputTime::from_micros(match self { + Self::Wheel(evt) => event::pointer::PointerEventTrait::time_usec(evt), + Self::Finger(evt) => event::pointer::PointerEventTrait::time_usec(evt), + Self::Continuous(evt) => event::pointer::PointerEventTrait::time_usec(evt), + }) + } + + fn device(&self) -> libinput::Device { + match self { + Self::Wheel(evt) => event::EventTrait::device(evt), + Self::Finger(evt) => event::EventTrait::device(evt), + Self::Continuous(evt) => event::EventTrait::device(evt), + } + } +} + +impl backend::PointerAxisEvent for PointerScrollAxis { + fn amount(&self, axis: Axis) -> Option { + use input::event::pointer::PointerScrollEvent; + let axis = axis.into(); + if self.has_axis(axis) { + Some(match self { + Self::Wheel(evt) => evt.scroll_value(axis), + Self::Finger(evt) => evt.scroll_value(axis), + Self::Continuous(evt) => evt.scroll_value(axis), + }) + } else { + None + } + } + + fn amount_v120(&self, axis: Axis) -> Option { + let axis = axis.into(); + if self.has_axis(axis) { + match self { + Self::Wheel(evt) => Some(evt.scroll_value_v120(axis)), + Self::Finger(_evt) => None, + Self::Continuous(_evt) => None, + } + } else { + None + } + } + + fn source(&self) -> AxisSource { + match self { + Self::Wheel(_) => AxisSource::Wheel, + Self::Finger(_) => AxisSource::Finger, + Self::Continuous(_) => AxisSource::Continuous, + } + } + + fn relative_direction(&self, _axis: Axis) -> backend::AxisRelativeDirection { + let device = backend::Event::::device(self); + if device.config_scroll_natural_scroll_enabled() { + AxisRelativeDirection::Inverted + } else { + AxisRelativeDirection::Identical + } + } +} + +impl backend::Event for event::pointer::PointerButtonEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::pointer::PointerEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::PointerButtonEvent for event::pointer::PointerButtonEvent { + fn button_code(&self) -> u32 { + self.button() + } + + fn state(&self) -> backend::ButtonState { + self.button_state().into() + } +} + +impl backend::Event for event::pointer::PointerMotionEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::pointer::PointerEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::PointerMotionEvent for event::pointer::PointerMotionEvent { + fn delta_x(&self) -> f64 { + self.dx() + } + + fn delta_y(&self) -> f64 { + self.dy() + } + + fn delta_x_unaccel(&self) -> f64 { + self.dx_unaccelerated() + } + + fn delta_y_unaccel(&self) -> f64 { + self.dy_unaccelerated() + } +} + +impl backend::Event for event::pointer::PointerMotionAbsoluteEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::pointer::PointerEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::PointerMotionAbsoluteEvent + for event::pointer::PointerMotionAbsoluteEvent +{ +} + +impl backend::AbsolutePositionEvent for event::pointer::PointerMotionAbsoluteEvent { + fn x(&self) -> f64 { + self.absolute_x() + } + + fn y(&self) -> f64 { + self.absolute_y() + } + + fn x_transformed(&self, width: i32) -> f64 { + self.absolute_x_transformed(width as u32) + } + + fn y_transformed(&self, height: i32) -> f64 { + self.absolute_y_transformed(height as u32) + } +} + +impl backend::GestureBeginEvent for T +where + T: event::gesture::GestureEventTrait + backend::Event, +{ + fn fingers(&self) -> u32 { + self.finger_count() as u32 + } +} + +impl backend::GestureEndEvent for T +where + T: event::gesture::GestureEndEvent + backend::Event, +{ + fn cancelled(&self) -> bool { + self.cancelled() + } +} + +impl backend::Event for event::gesture::GestureSwipeBeginEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GestureSwipeBeginEvent for event::gesture::GestureSwipeBeginEvent {} + +impl backend::Event for event::gesture::GestureSwipeUpdateEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GestureSwipeUpdateEvent for event::gesture::GestureSwipeUpdateEvent { + fn delta_x(&self) -> f64 { + event::gesture::GestureEventCoordinates::dx(self) + } + + fn delta_y(&self) -> f64 { + event::gesture::GestureEventCoordinates::dy(self) + } +} + +impl backend::Event for event::gesture::GestureSwipeEndEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GestureSwipeEndEvent for event::gesture::GestureSwipeEndEvent {} + +impl backend::Event for event::gesture::GesturePinchBeginEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GesturePinchBeginEvent for event::gesture::GesturePinchBeginEvent {} + +impl backend::Event for event::gesture::GesturePinchUpdateEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GesturePinchUpdateEvent for event::gesture::GesturePinchUpdateEvent { + fn delta_x(&self) -> f64 { + event::gesture::GestureEventCoordinates::dx(self) + } + + fn delta_y(&self) -> f64 { + event::gesture::GestureEventCoordinates::dy(self) + } + + fn scale(&self) -> f64 { + event::gesture::GesturePinchEventTrait::scale(self) + } + + fn rotation(&self) -> f64 { + self.angle_delta() + } +} + +impl backend::Event for event::gesture::GesturePinchEndEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GesturePinchEndEvent for event::gesture::GesturePinchEndEvent {} + +impl backend::Event for event::gesture::GestureHoldBeginEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GestureHoldBeginEvent for event::gesture::GestureHoldBeginEvent {} + +impl backend::Event for event::gesture::GestureHoldEndEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::gesture::GestureEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::GestureHoldEndEvent for event::gesture::GestureHoldEndEvent {} + +impl backend::Event for event::touch::TouchDownEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::touch::TouchEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::TouchDownEvent for event::touch::TouchDownEvent {} + +impl backend::TouchEvent for event::touch::TouchDownEvent { + fn slot(&self) -> backend::TouchSlot { + event::touch::TouchEventSlot::slot(self).into() + } +} + +impl backend::AbsolutePositionEvent for event::touch::TouchDownEvent { + fn x(&self) -> f64 { + event::touch::TouchEventPosition::x(self) + } + + fn y(&self) -> f64 { + event::touch::TouchEventPosition::y(self) + } + + fn x_transformed(&self, width: i32) -> f64 { + event::touch::TouchEventPosition::x_transformed(self, width as u32) + } + + fn y_transformed(&self, height: i32) -> f64 { + event::touch::TouchEventPosition::y_transformed(self, height as u32) + } +} + +impl backend::Event for event::touch::TouchMotionEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::touch::TouchEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::TouchMotionEvent for event::touch::TouchMotionEvent {} + +impl backend::TouchEvent for event::touch::TouchMotionEvent { + fn slot(&self) -> backend::TouchSlot { + event::touch::TouchEventSlot::slot(self).into() + } +} + +impl backend::AbsolutePositionEvent for event::touch::TouchMotionEvent { + fn x(&self) -> f64 { + event::touch::TouchEventPosition::x(self) + } + + fn y(&self) -> f64 { + event::touch::TouchEventPosition::y(self) + } + + fn x_transformed(&self, width: i32) -> f64 { + event::touch::TouchEventPosition::x_transformed(self, width as u32) + } + + fn y_transformed(&self, height: i32) -> f64 { + event::touch::TouchEventPosition::y_transformed(self, height as u32) + } +} + +impl backend::Event for event::touch::TouchUpEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::touch::TouchEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::TouchUpEvent for event::touch::TouchUpEvent {} + +impl backend::TouchEvent for event::touch::TouchUpEvent { + fn slot(&self) -> backend::TouchSlot { + event::touch::TouchEventSlot::slot(self).into() + } +} + +impl backend::Event for event::touch::TouchCancelEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::touch::TouchEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::TouchCancelEvent for event::touch::TouchCancelEvent {} + +impl backend::TouchEvent for event::touch::TouchCancelEvent { + fn slot(&self) -> backend::TouchSlot { + event::touch::TouchEventSlot::slot(self).into() + } +} + +impl backend::Event for event::touch::TouchFrameEvent { + fn time(&self) -> InputTime { + InputTime::from_micros(event::touch::TouchEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::TouchFrameEvent for event::touch::TouchFrameEvent {} + +impl InputBackend for LibinputInputBackend { + type Device = libinput::Device; + type KeyboardKeyEvent = event::keyboard::KeyboardKeyEvent; + type PointerAxisEvent = PointerScrollAxis; + type PointerButtonEvent = event::pointer::PointerButtonEvent; + type PointerMotionEvent = event::pointer::PointerMotionEvent; + type PointerMotionAbsoluteEvent = event::pointer::PointerMotionAbsoluteEvent; + + type GestureSwipeBeginEvent = event::gesture::GestureSwipeBeginEvent; + type GestureSwipeUpdateEvent = event::gesture::GestureSwipeUpdateEvent; + type GestureSwipeEndEvent = event::gesture::GestureSwipeEndEvent; + type GesturePinchBeginEvent = event::gesture::GesturePinchBeginEvent; + type GesturePinchUpdateEvent = event::gesture::GesturePinchUpdateEvent; + type GesturePinchEndEvent = event::gesture::GesturePinchEndEvent; + type GestureHoldBeginEvent = event::gesture::GestureHoldBeginEvent; + type GestureHoldEndEvent = event::gesture::GestureHoldEndEvent; + + type TouchDownEvent = event::touch::TouchDownEvent; + type TouchUpEvent = event::touch::TouchUpEvent; + type TouchMotionEvent = event::touch::TouchMotionEvent; + type TouchCancelEvent = event::touch::TouchCancelEvent; + type TouchFrameEvent = event::touch::TouchFrameEvent; + type TabletToolAxisEvent = event::tablet_tool::TabletToolAxisEvent; + type TabletToolProximityEvent = event::tablet_tool::TabletToolProximityEvent; + type TabletToolTipEvent = event::tablet_tool::TabletToolTipEvent; + type TabletToolButtonEvent = event::tablet_tool::TabletToolButtonEvent; + + type SwitchToggleEvent = event::switch::SwitchToggleEvent; + + type SpecialEvent = backend::UnusedEvent; +} + +impl From for backend::KeyState { + #[inline] + fn from(libinput: event::keyboard::KeyState) -> Self { + match libinput { + event::keyboard::KeyState::Pressed => backend::KeyState::Pressed, + event::keyboard::KeyState::Released => backend::KeyState::Released, + } + } +} + +impl From for backend::Axis { + #[inline] + fn from(libinput: event::pointer::Axis) -> Self { + match libinput { + event::pointer::Axis::Vertical => backend::Axis::Vertical, + event::pointer::Axis::Horizontal => backend::Axis::Horizontal, + } + } +} + +impl From for event::pointer::Axis { + #[inline] + fn from(axis: backend::Axis) -> Self { + match axis { + backend::Axis::Vertical => event::pointer::Axis::Vertical, + backend::Axis::Horizontal => event::pointer::Axis::Horizontal, + } + } +} + +impl From for backend::ButtonState { + #[inline] + fn from(libinput: event::pointer::ButtonState) -> Self { + match libinput { + event::pointer::ButtonState::Pressed => backend::ButtonState::Pressed, + event::pointer::ButtonState::Released => backend::ButtonState::Released, + } + } +} + +impl From for libinput::Led { + #[inline] + fn from(value: crate::input::keyboard::LedState) -> Self { + let mut leds = libinput::Led::empty(); + if value.num.unwrap_or_default() { + leds |= libinput::Led::NUMLOCK; + } + if value.caps.unwrap_or_default() { + leds |= libinput::Led::CAPSLOCK; + } + if value.scroll.unwrap_or_default() { + leds |= libinput::Led::SCROLLLOCK; + } + leds + } +} + +/// Wrapper for types implementing the [`Session`] trait to provide +/// a [`libinput::LibinputInterface`] implementation. +#[cfg(feature = "backend_session")] +#[derive(Debug)] +pub struct LibinputSessionInterface(S); + +#[cfg(feature = "backend_session")] +impl From for LibinputSessionInterface { + #[inline] + fn from(session: S) -> LibinputSessionInterface { + LibinputSessionInterface(session) + } +} + +#[cfg(feature = "backend_session")] +impl libinput::LibinputInterface for LibinputSessionInterface { + fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { + use rustix::fs::OFlags; + self.0 + .open(path, OFlags::from_bits_truncate(flags as u32)) + .map_err(|err| err.as_errno().unwrap_or(1 /*Use EPERM by default*/)) + } + + fn close_restricted(&mut self, fd: OwnedFd) { + let _ = self.0.close(fd); + } +} + +impl AsFd for LibinputInputBackend { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.context.as_fd() + } +} + +impl EventSource for LibinputInputBackend { + type Event = InputEvent; + type Metadata = (); + type Ret = (); + type Error = io::Error; + + #[profiling::function] + fn process_events(&mut self, _: Readiness, token: Token, mut callback: F) -> io::Result + where + F: FnMut(Self::Event, &mut ()) -> Self::Ret, + { + if Some(token) == self.token { + let _guard = self.span.enter(); + self.context.dispatch()?; + + for event in &mut self.context { + match event { + libinput::Event::Device(device_event) => match device_event { + event::DeviceEvent::Added(device_added_event) => { + let added = event::EventTrait::device(&device_added_event); + + info!("New device {:?}", added.sysname(),); + + callback(InputEvent::DeviceAdded { device: added }, &mut ()); + } + event::DeviceEvent::Removed(device_removed_event) => { + let removed = event::EventTrait::device(&device_removed_event); + + info!("Removed device {:?}", removed.sysname(),); + + callback(InputEvent::DeviceRemoved { device: removed }, &mut ()); + } + _ => { + trace!("Unknown libinput device event"); + } + }, + libinput::Event::Touch(touch_event) => match touch_event { + event::TouchEvent::Down(down_event) => { + callback(InputEvent::TouchDown { event: down_event }, &mut ()); + } + event::TouchEvent::Motion(motion_event) => { + callback(InputEvent::TouchMotion { event: motion_event }, &mut ()); + } + event::TouchEvent::Up(up_event) => { + callback(InputEvent::TouchUp { event: up_event }, &mut ()); + } + event::TouchEvent::Cancel(cancel_event) => { + callback(InputEvent::TouchCancel { event: cancel_event }, &mut ()); + } + event::TouchEvent::Frame(frame_event) => { + callback(InputEvent::TouchFrame { event: frame_event }, &mut ()); + } + _ => { + trace!("Unknown libinput touch event"); + } + }, + libinput::Event::Keyboard(keyboard_event) => match keyboard_event { + event::KeyboardEvent::Key(key_event) => { + callback(InputEvent::Keyboard { event: key_event }, &mut ()); + } + _ => { + trace!("Unknown libinput keyboard event"); + } + }, + libinput::Event::Pointer(pointer_event) => match pointer_event { + event::PointerEvent::Motion(motion_event) => { + callback(InputEvent::PointerMotion { event: motion_event }, &mut ()); + } + event::PointerEvent::MotionAbsolute(motion_abs_event) => { + callback( + InputEvent::PointerMotionAbsolute { + event: motion_abs_event, + }, + &mut (), + ); + } + event::PointerEvent::ScrollWheel(scroll_event) => { + callback( + InputEvent::PointerAxis { + event: PointerScrollAxis::Wheel(scroll_event), + }, + &mut (), + ); + } + event::PointerEvent::ScrollFinger(scroll_event) => { + callback( + InputEvent::PointerAxis { + event: PointerScrollAxis::Finger(scroll_event), + }, + &mut (), + ); + } + event::PointerEvent::ScrollContinuous(scroll_event) => { + callback( + InputEvent::PointerAxis { + event: PointerScrollAxis::Continuous(scroll_event), + }, + &mut (), + ); + } + event::PointerEvent::Button(button_event) => { + callback(InputEvent::PointerButton { event: button_event }, &mut ()); + } + _ => { + trace!("Unknown libinput pointer event"); + } + }, + libinput::Event::Gesture(gesture_event) => match gesture_event { + event::GestureEvent::Swipe(event::gesture::GestureSwipeEvent::Begin(event)) => { + callback(InputEvent::GestureSwipeBegin { event }, &mut ()); + } + event::GestureEvent::Swipe(event::gesture::GestureSwipeEvent::Update(event)) => { + callback(InputEvent::GestureSwipeUpdate { event }, &mut ()); + } + event::GestureEvent::Swipe(event::gesture::GestureSwipeEvent::End(event)) => { + callback(InputEvent::GestureSwipeEnd { event }, &mut ()); + } + event::GestureEvent::Pinch(event::gesture::GesturePinchEvent::Begin(event)) => { + callback(InputEvent::GesturePinchBegin { event }, &mut ()); + } + event::GestureEvent::Pinch(event::gesture::GesturePinchEvent::Update(event)) => { + callback(InputEvent::GesturePinchUpdate { event }, &mut ()); + } + event::GestureEvent::Pinch(event::gesture::GesturePinchEvent::End(event)) => { + callback(InputEvent::GesturePinchEnd { event }, &mut ()); + } + event::GestureEvent::Hold(event::gesture::GestureHoldEvent::Begin(event)) => { + callback(InputEvent::GestureHoldBegin { event }, &mut ()); + } + event::GestureEvent::Hold(event::gesture::GestureHoldEvent::End(event)) => { + callback(InputEvent::GestureHoldEnd { event }, &mut ()); + } + _ => { + trace!("Unknown libinput gesture event"); + } + }, + libinput::Event::Tablet(tablet_event) => match tablet_event { + event::TabletToolEvent::Axis(event) => { + callback(InputEvent::TabletToolAxis { event }, &mut ()); + } + event::TabletToolEvent::Proximity(event) => { + callback(InputEvent::TabletToolProximity { event }, &mut ()); + } + event::TabletToolEvent::Tip(event) => { + callback(InputEvent::TabletToolTip { event }, &mut ()); + } + event::TabletToolEvent::Button(event) => { + callback(InputEvent::TabletToolButton { event }, &mut ()); + } + _ => { + trace!("Unknown libinput tablet event"); + } + }, + libinput::Event::Switch(switch_event) => match switch_event { + event::SwitchEvent::Toggle(event) => { + callback(InputEvent::SwitchToggle { event }, &mut ()); + } + _ => { + trace!("Unknown libinput switch event"); + } + }, + _ => {} //FIXME: What to do with the rest. + } + } + } + + Ok(PostAction::Continue) + } + + fn register(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.token = Some(factory.token()); + // Safety: the FD cannot be closed without removing the LibinputInputBackend from the event loop + unsafe { poll.register(self.as_fd(), Interest::READ, Mode::Level, self.token.unwrap()) } + } + + fn reregister(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.token = Some(factory.token()); + poll.reregister(self.as_fd(), Interest::READ, Mode::Level, self.token.unwrap()) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.token = None; + poll.unregister(self.as_fd()) + } +} diff --git a/vendor/smithay/src/backend/libinput/tablet.rs b/vendor/smithay/src/backend/libinput/tablet.rs new file mode 100644 index 0000000..b9cf5ba --- /dev/null +++ b/vendor/smithay/src/backend/libinput/tablet.rs @@ -0,0 +1,195 @@ +use crate::backend::input::{ + self as backend, InputTime, TabletToolCapabilities, TabletToolDescriptor, TabletToolTipState, + TabletToolType, +}; + +use input as libinput; +use input::event; +use input::event::{EventTrait, tablet_tool}; + +use super::LibinputInputBackend; + +/// Marker for tablet tool events +pub trait IsTabletEvent: tablet_tool::TabletToolEventTrait + EventTrait {} + +impl IsTabletEvent for tablet_tool::TabletToolAxisEvent {} +impl IsTabletEvent for tablet_tool::TabletToolProximityEvent {} +impl IsTabletEvent for tablet_tool::TabletToolTipEvent {} +impl IsTabletEvent for tablet_tool::TabletToolButtonEvent {} + +impl backend::Event for E +where + E: IsTabletEvent, +{ + fn time(&self) -> InputTime { + InputTime::from_micros(tablet_tool::TabletToolEventTrait::time_usec(self)) + } + + fn device(&self) -> libinput::Device { + event::EventTrait::device(self) + } +} + +impl backend::TabletToolAxisEvent for tablet_tool::TabletToolAxisEvent {} + +impl backend::TabletToolProximityEvent for tablet_tool::TabletToolProximityEvent { + fn state(&self) -> backend::ProximityState { + match tablet_tool::TabletToolProximityEvent::proximity_state(self) { + tablet_tool::ProximityState::In => backend::ProximityState::In, + tablet_tool::ProximityState::Out => backend::ProximityState::Out, + } + } +} + +impl backend::TabletToolTipEvent for tablet_tool::TabletToolTipEvent { + fn tip_state(&self) -> TabletToolTipState { + match tablet_tool::TabletToolTipEvent::tip_state(self) { + tablet_tool::TipState::Up => backend::TabletToolTipState::Up, + tablet_tool::TipState::Down => backend::TabletToolTipState::Down, + } + } +} + +impl backend::AbsolutePositionEvent for E +where + E: IsTabletEvent + event::EventTrait, +{ + fn x(&self) -> f64 { + tablet_tool::TabletToolEventTrait::x(self) + } + + fn y(&self) -> f64 { + tablet_tool::TabletToolEventTrait::y(self) + } + + fn x_transformed(&self, width: i32) -> f64 { + tablet_tool::TabletToolEventTrait::x_transformed(self, width as u32) + } + + fn y_transformed(&self, height: i32) -> f64 { + tablet_tool::TabletToolEventTrait::y_transformed(self, height as u32) + } +} + +impl backend::TabletToolEvent for E +where + E: IsTabletEvent + event::EventTrait, +{ + fn tool(&self) -> TabletToolDescriptor { + let tool = self.tool(); + + let tool_type = match tool.tool_type() { + Some(tablet_tool::TabletToolType::Pen) => TabletToolType::Pen, + Some(tablet_tool::TabletToolType::Eraser) => TabletToolType::Eraser, + Some(tablet_tool::TabletToolType::Brush) => TabletToolType::Brush, + Some(tablet_tool::TabletToolType::Pencil) => TabletToolType::Pencil, + Some(tablet_tool::TabletToolType::Airbrush) => TabletToolType::Airbrush, + Some(tablet_tool::TabletToolType::Mouse) => TabletToolType::Mouse, + Some(tablet_tool::TabletToolType::Lens) => TabletToolType::Lens, + Some(tablet_tool::TabletToolType::Totem) => TabletToolType::Totem, + _ => TabletToolType::Unknown, + }; + + let hardware_serial = tool.serial(); + let hardware_id_wacom = tool.tool_id(); + + let mut capabilities = TabletToolCapabilities::empty(); + + capabilities.set(TabletToolCapabilities::TILT, tool.has_tilt()); + capabilities.set(TabletToolCapabilities::PRESSURE, tool.has_pressure()); + capabilities.set(TabletToolCapabilities::DISTANCE, tool.has_distance()); + capabilities.set(TabletToolCapabilities::ROTATION, tool.has_rotation()); + capabilities.set(TabletToolCapabilities::SLIDER, tool.has_slider()); + capabilities.set(TabletToolCapabilities::WHEEL, tool.has_wheel()); + + TabletToolDescriptor { + tool_type, + hardware_serial, + hardware_id_wacom, + capabilities, + } + } + + fn delta_x(&self) -> f64 { + tablet_tool::TabletToolEventTrait::dx(self) + } + + fn delta_y(&self) -> f64 { + tablet_tool::TabletToolEventTrait::dy(self) + } + + fn distance(&self) -> f64 { + tablet_tool::TabletToolEventTrait::distance(self) + } + + fn distance_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::distance_has_changed(self) + } + + fn pressure(&self) -> f64 { + tablet_tool::TabletToolEventTrait::pressure(self) + } + + fn pressure_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::pressure_has_changed(self) + } + + fn slider_position(&self) -> f64 { + tablet_tool::TabletToolEventTrait::slider_position(self) + } + + fn slider_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::slider_has_changed(self) + } + + fn tilt_x(&self) -> f64 { + tablet_tool::TabletToolEventTrait::tilt_x(self) + } + + fn tilt_x_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::tilt_x_has_changed(self) + } + + fn tilt_y(&self) -> f64 { + tablet_tool::TabletToolEventTrait::tilt_y(self) + } + + fn tilt_y_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::tilt_y_has_changed(self) + } + + fn rotation(&self) -> f64 { + tablet_tool::TabletToolEventTrait::rotation(self) + } + + fn rotation_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::rotation_has_changed(self) + } + + fn wheel_delta(&self) -> f64 { + tablet_tool::TabletToolEventTrait::wheel_delta(self) + } + + fn wheel_delta_discrete(&self) -> i32 { + // I have no idea why f64 is returned by this fn, in libinput's api wheel clicks are always i32 + tablet_tool::TabletToolEventTrait::wheel_delta_discrete(self) as i32 + } + + fn wheel_has_changed(&self) -> bool { + tablet_tool::TabletToolEventTrait::wheel_has_changed(self) + } +} + +impl backend::TabletToolButtonEvent for tablet_tool::TabletToolButtonEvent { + fn button(&self) -> u32 { + tablet_tool::TabletToolButtonEvent::button(self) + } + + fn seat_button_count(&self) -> u32 { + tablet_tool::TabletToolButtonEvent::seat_button_count(self) + } + + fn button_state(&self) -> backend::ButtonState { + tablet_tool::TabletToolButtonEvent::button_state(self).into() + } +} diff --git a/vendor/smithay/src/backend/mod.rs b/vendor/smithay/src/backend/mod.rs new file mode 100644 index 0000000..b320a27 --- /dev/null +++ b/vendor/smithay/src/backend/mod.rs @@ -0,0 +1,141 @@ +// Allow in this module because of existing usage +#![allow(clippy::uninlined_format_args)] +//! Backend (rendering/input) helpers +//! +//! This module provides helpers for interaction with the operating system. +//! +//! ## Module structure +//! +//! The module is largely structured around three main aspects of interaction with the OS: +//! session management, input handling, and graphics. +//! +//! ### Session management +//! +//! Session management relates to mechanisms allowing the compositor to access the resources +//! it needs to function. It contains interaction with the login manager if any ((e)logind or +//! seatd), as well as releasing those resources when TTY-switching. It is handled by the +//! [`session`] module, gated by the `backend_session` cargo feature. You will generally need +//! it to run your compositor directly on a TTY. +//! +//! This module is tightly coupled with the [`udev`] module (gated by the `backend_udev` cargo +//! feature), which allows the discovery of usable graphics and input devices on the system, using +//! the udev system daemon. +//! +//! ### Input handling +//! +//! Input handling consists in discovering the various available input devices, and receiving +//! all inputs events from it. Smithay is build to support different possible sources for +//! that input data, with a generic API provided by the traits and types defined in the +//! [`input`] module. An input provider following this API based on `libinput` is given in the +//! [`libinput`] module, gated by the `backend_libinput` cargo feature. The winit backend +//! (see below) also provides an input provider. +//! +//! ### Graphics +//! +//! Combining content from the clients and displaying it on the screen is the central role of +//! a wayland compositor, and also one of its most complex tasks; several backend modules are +//! dedicated to this task. +//! +//! Smithay provides a rendering infrastructure built around graphics buffers: you retrieve buffers +//! for your client, you composite them into a new buffer holding the contents of your desktop, +//! that you will then submit to the hardware for display. The backbone of this infrastructure is +//! structured around two modules: +//! +//! - [`allocator`] contains generic traits representing the capability to +//! allocate and convert graphical buffers, as well as an implementation of this +//! capability using GBM (see its module-level docs for details). +//! - [`renderer`] provides traits representing the capability of graphics +//! rendering using those buffers, as well as an implementation of this +//! capability using GLes2 (see its module-level docs for details). +//! +//! Alongside this backbone capability, Smithay also provides the [`drm`] module, which handles +//! direct interaction with the graphical physical devices to setup the display pipeline and +//! submit rendered buffers to the monitors for display. This module is gated by the +//! `backend_drm` cargo feature. +//! +//! The [`egl`] module provides the logic to setup an OpenGL context. It is used by the Gles2 +//! renderer (which is based on OpenGL), and also provides the capability for clients to use +//! the `wl_drm`-based hardware-acceleration provided by Mesa, a precursor to the +//! [`linux_dmabuf`](crate::wayland::dmabuf) Wayland protocol extension. Note that, at the +//! moment, even clients using dma-buf still require that the `wl_drm` infrastructure is +//! initialized to have hardware-acceleration. +//! +//! ## X11 backend +//! +//! Alongside this infrastructure, Smithay also provides an alternative backend based on +//! [x11rb](https://crates.io/crates/x11rb), which makes it possible to run your compositor as +//! an X11 client. This is generally quite helpful for development and debugging. +//! +//! The X11 backend does not concern itself with what renderer is in use, allowing presentation to +//! the window assuming you can provide it with a [`Dmabuf`](crate::backend::allocator::dmabuf::Dmabuf). +//! The X11 backend is also an input provider, and is accessible in the [`x11`] module, gated by +//! the `backend_x11` cargo feature. +//! +//! ## Winit backend +//! +//! Alongside this infrastructure, Smithay also provides an alternative backend based on +//! [winit](https://crates.io/crates/winit), which makes it possible to run your compositor as +//! a Wayland or X11 client. You are encouraged to use the X11 backend where possible since winit +//! does not integrate into calloop too well. This backend is generally quite helpful for +//! development and debugging. That backend is both a renderer and an input provider, and is +//! accessible in the [`winit`] module, gated by the `backend_winit` cargo feature. +//! + +pub mod allocator; +pub mod input; +pub mod renderer; + +#[cfg(feature = "backend_drm")] +pub mod drm; +#[cfg(feature = "backend_egl")] +pub mod egl; +#[cfg(feature = "backend_libinput")] +pub mod libinput; +#[cfg(feature = "backend_session")] +pub mod session; +#[cfg(feature = "backend_udev")] +pub mod udev; + +#[cfg(feature = "backend_vulkan")] +pub mod vulkan; + +#[cfg(feature = "backend_winit")] +pub mod winit; + +#[cfg(feature = "backend_x11")] +pub mod x11; + +#[cfg(feature = "backend_libei")] +pub mod libei; + +/// Error that can happen when swapping buffers. +#[derive(Debug, thiserror::Error)] +pub enum SwapBuffersError { + /// The buffers have already been swapped. + /// + /// This error can be returned when `swap_buffers` has been called multiple times + /// without any modification in between. + #[error("Buffers are already swapped, swap_buffers was called too many times")] + AlreadySwapped, + /// The corresponding context has been lost and needs to be recreated. + /// + /// All the objects associated to it (textures, buffers, programs, etc.) + /// need to be recreated from scratch. Underlying resources like native surfaces + /// might also need to be recreated. + /// + /// Operations will have no effect. Functions that read textures, buffers, etc. + /// will return uninitialized data instead. + #[error("The context has been lost, it needs to be recreated: {0}")] + ContextLost(Box), + /// A temporary condition caused to rendering to fail. + /// + /// Depending on the underlying error this *might* require fixing internal state of the rendering backend, + /// but failures mapped to `TemporaryFailure` are always recoverable without re-creating the entire stack, + /// as is represented by `ContextLost`. + /// + /// Proceed after investigating the source to reschedule another full rendering step or just this page_flip at a later time. + /// If the root cause cannot be discovered and subsequent renderings also fail, it is advised to fallback to + /// recreation. + #[error("A temporary condition caused the page flip to fail: {0}")] + TemporaryFailure(Box), +} diff --git a/vendor/smithay/src/backend/renderer/color.rs b/vendor/smithay/src/backend/renderer/color.rs new file mode 100644 index 0000000..60970b8 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/color.rs @@ -0,0 +1,77 @@ +use std::ops::Mul; + +/// A four-component color representing pre-multiplied RGBA color values +#[derive(Debug, Copy, Clone, Default, PartialEq)] +pub struct Color32F([f32; 4]); + +impl Color32F { + /// Initialize a new [`Color32F`] + #[inline] + pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { + Self([r, g, b, a]) + } +} + +impl Color32F { + /// Transparent color + pub const TRANSPARENT: Color32F = Color32F::new(0.0, 0.0, 0.0, 0.0); + + /// Solid black color + pub const BLACK: Color32F = Color32F::new(0f32, 0f32, 0f32, 1f32); +} + +impl Color32F { + /// Red color component + #[inline] + pub fn r(&self) -> f32 { + self.0[0] + } + + /// Green color component + #[inline] + pub fn g(&self) -> f32 { + self.0[1] + } + + /// Blue color component + #[inline] + pub fn b(&self) -> f32 { + self.0[2] + } + + /// Alpha color component + #[inline] + pub fn a(&self) -> f32 { + self.0[3] + } + + /// Color components + #[inline] + pub fn components(self) -> [f32; 4] { + self.0 + } +} + +impl Color32F { + /// Test if the color represents a opaque color + #[inline] + pub fn is_opaque(&self) -> bool { + self.a() == 1f32 + } +} + +impl From<[f32; 4]> for Color32F { + #[inline] + fn from(value: [f32; 4]) -> Self { + Self(value) + } +} + +impl Mul for Color32F { + type Output = Color32F; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Self::new(self.r() * rhs, self.g() * rhs, self.b() * rhs, self.a() * rhs) + } +} diff --git a/vendor/smithay/src/backend/renderer/damage/mod.rs b/vendor/smithay/src/backend/renderer/damage/mod.rs new file mode 100644 index 0000000..efd70ec --- /dev/null +++ b/vendor/smithay/src/backend/renderer/damage/mod.rs @@ -0,0 +1,974 @@ +//! Helper for effective output damage tracking +//! +//! # Why use this implementation +//! +//! The [`OutputDamageTracker`] in combination with the [`RenderElement`] trait +//! can help you to reduce resource consumption by tracking what elements have +//! been damaged and only redraw the damaged parts on an output. +//! +//! It does so by keeping track of the last used [`CommitCounter`] for all provided +//! [`RenderElement`]s and queries the element for new damage on each call to [`render_output`](OutputDamageTracker::render_output) or [`damage_output`](OutputDamageTracker::damage_output). +//! +//! Additionally the damage tracker will automatically generate damage in the following situations: +//! - Current geometry for elements entering the output +//! - Current and last known geometry for moved elements (includes z-index changes) +//! - Last known geometry for elements no longer present +//! +//! Elements fully occluded by opaque regions as defined by elements higher in the stack are skipped. +//! The actual action taken by the damage tracker can be inspected from the returned [`RenderElementStates`]. +//! +//! You can initialize it with a static output by using [`OutputDamageTracker::new`] or +//! allow it to track a specific [`Output`] with [`OutputDamageTracker::from_output`]. +//! +//! See the [`renderer::element`](crate::backend::renderer::element) module for more information +//! about how to use [`RenderElement`]. +//! +//! # How to use it +//! +//! ```no_run +//! # use smithay::{ +//! # backend::renderer::{Color32F, DebugFlags, Frame, ImportMem, Renderer, Texture, TextureFilter, sync::SyncPoint, test::{DummyRenderer, DummyFramebuffer}}, +//! # utils::{Buffer, Physical, Rectangle, Size}, +//! # }; +//! use smithay::{ +//! backend::{ +//! allocator::Fourcc, +//! renderer::{ +//! damage::OutputDamageTracker, +//! element::{ +//! Kind, +//! memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, +//! } +//! }, +//! }, +//! utils::{Point, Transform}, +//! }; +//! use std::time::{Duration, Instant}; +//! +//! const WIDTH: i32 = 10; +//! const HEIGHT: i32 = 10; +//! # let mut renderer = DummyRenderer::default(); +//! # let mut framebuffer = DummyFramebuffer; +//! # let buffer_age = 0; +//! +//! // Initialize a new damage tracker for a static output +//! let mut damage_tracker = OutputDamageTracker::new((800, 600), 1.0, Transform::Normal); +//! +//! // Initialize a buffer to render +//! let mut memory_buffer = MemoryRenderBuffer::new(Fourcc::Argb8888, (WIDTH, HEIGHT), 1, Transform::Normal, None); +//! +//! let mut last_update = Instant::now(); +//! +//! loop { +//! let now = Instant::now(); +//! if now.duration_since(last_update) >= Duration::from_secs(3) { +//! let mut render_context = memory_buffer.render(); +//! +//! render_context.draw(|_buffer| { +//! // Update the changed parts of the buffer +//! +//! // Return the updated parts +//! Result::<_, ()>::Ok(vec![Rectangle::from_size((WIDTH, HEIGHT).into())]) +//! }); +//! +//! last_update = now; +//! } +//! +//! // Create a render element from the buffer +//! let location = Point::from((100.0, 100.0)); +//! let render_element = +//! MemoryRenderBufferRenderElement::from_buffer(&mut renderer, location, &memory_buffer, None, None, None, Kind::Unspecified) +//! .expect("Failed to upload memory to gpu"); +//! +//! // Render the output +//! damage_tracker +//! .render_output( +//! &mut renderer, +//! &mut framebuffer, +//! buffer_age, +//! &[render_element], +//! [0.8, 0.8, 0.9, 1.0], +//! ) +//! .expect("failed to render the output"); +//! } +//! ``` + +use std::{ + collections::{HashMap, VecDeque}, + ops::Range, + sync::Once, +}; + +use indexmap::IndexMap; +use smallvec::{SmallVec, smallvec}; +use tracing::{info_span, instrument, trace, warn}; + +use crate::{ + backend::renderer::{Frame, element::RenderElementPresentationState}, + output::{Output, OutputModeSource, OutputNoMode}, + utils::{Buffer as BufferCoords, Physical, Rectangle, Scale, Size, Transform, user_data::UserDataMap}, +}; + +use super::{ + Color32F, + element::{Element, Id, RenderElement, RenderElementState, RenderElementStates}, + sync::SyncPoint, + utils::CommitCounter, +}; + +use super::{Renderer, Texture}; + +mod shaper; + +use shaper::DamageShaper; + +const MAX_AGE: usize = 4; + +#[derive(Debug, Clone, Copy)] +struct ElementInstanceState { + last_src: Rectangle, + last_geometry: Rectangle, + last_transform: Transform, + last_alpha: f32, + last_z_index: usize, + last_is_framebuffer_effect: bool, +} + +impl ElementInstanceState { + #[inline] + fn matches( + &self, + src: Rectangle, + geometry: Rectangle, + transform: Transform, + alpha: f32, + z_index: usize, + is_framebuffer_effect: bool, + ) -> bool { + self.last_src == src + && self.last_geometry == geometry + && self.last_transform == transform + && self.last_alpha == alpha + && self.last_z_index == z_index + && self.last_is_framebuffer_effect == is_framebuffer_effect + } +} + +#[derive(Debug, Clone)] +struct ElementState { + last_commit: CommitCounter, + last_instances: SmallVec<[ElementInstanceState; 1]>, +} + +impl ElementState { + #[inline] + fn instance_matches( + &self, + src: Rectangle, + geometry: Rectangle, + transform: Transform, + alpha: f32, + z_index: usize, + is_framebuffer_effect: bool, + ) -> bool { + self.last_instances + .iter() + .any(|instance| instance.matches(src, geometry, transform, alpha, z_index, is_framebuffer_effect)) + } +} + +#[derive(Debug, Default)] +struct RendererState { + transform: Option, + size: Option>, + elements: IndexMap, + effects_cache: HashMap, + old_damage: VecDeque>>, + opaque_regions: Vec>, + clear_color: Option, +} + +/// Damage tracker for a single output +#[derive(Debug)] +pub struct OutputDamageTracker { + mode: OutputModeSource, + last_state: RendererState, + damage_shaper: DamageShaper, + damage: Vec>, + element_damage: Vec>, + opaque_regions: Vec>, + opaque_regions_index: Vec>, + element_damage_index: Vec, + element_opaque_regions: Vec>, + element_visible_area_workhouse: Vec>, + span: tracing::Span, +} + +/// Errors thrown by [`OutputDamageTracker::render_output`] +#[derive(thiserror::Error)] +pub enum Error { + /// The provided [`Renderer`] returned an error + #[error(transparent)] + Rendering(E), + /// The given [`Output`] has no mode set + #[error(transparent)] + OutputNoMode(#[from] OutputNoMode), +} + +/// Represents the result from rendering the output +#[derive(Debug)] +pub struct RenderOutputResult<'a> { + /// Holds the sync point of the rendering operation + pub sync: SyncPoint, + /// Holds the damage from the rendering operation + pub damage: Option<&'a Vec>>, + /// Holds the render element states + pub states: RenderElementStates, +} + +impl RenderOutputResult<'_> { + fn skipped(states: RenderElementStates) -> Self { + Self { + sync: SyncPoint::signaled(), + damage: None, + states, + } + } +} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Rendering(err) => std::fmt::Debug::fmt(err, f), + Error::OutputNoMode(err) => std::fmt::Debug::fmt(err, f), + } + } +} + +impl OutputDamageTracker { + /// Initialize a static [`OutputDamageTracker`] + pub fn new( + size: impl Into>, + scale: impl Into>, + transform: Transform, + ) -> Self { + Self { + mode: OutputModeSource::Static { + size: size.into(), + scale: scale.into(), + transform, + }, + last_state: Default::default(), + damage_shaper: Default::default(), + damage: Default::default(), + element_damage: Default::default(), + opaque_regions: Default::default(), + opaque_regions_index: Default::default(), + element_damage_index: Default::default(), + element_opaque_regions: Default::default(), + element_visible_area_workhouse: Default::default(), + span: info_span!("renderer_damage"), + } + } + + /// Initialize a new [`OutputDamageTracker`] from an [`Output`] + /// + /// The renderer will keep track of changes to the [`Output`] + /// and handle size and scaling changes automatically on the + /// next call to [`render_output`](OutputDamageTracker::render_output) + pub fn from_output(output: &Output) -> Self { + Self { + mode: OutputModeSource::Auto(output.downgrade()), + damage_shaper: Default::default(), + damage: Default::default(), + element_damage: Default::default(), + opaque_regions: Default::default(), + opaque_regions_index: Default::default(), + element_damage_index: Default::default(), + element_opaque_regions: Default::default(), + element_visible_area_workhouse: Default::default(), + last_state: Default::default(), + span: info_span!("renderer_damage", output = output.name()), + } + } + + /// Initialize a new [`OutputDamageTracker`] from an [`OutputModeSource`]. + /// + /// This should only be used when trying to support both static and automatic output mode + /// sources. For known modes use [`OutputDamageTracker::new`] or + /// [`OutputDamageTracker::from_output`] instead. + pub fn from_mode_source(output_mode_source: impl Into) -> Self { + Self { + mode: output_mode_source.into(), + span: info_span!("render_damage"), + damage_shaper: Default::default(), + damage: Default::default(), + element_damage: Default::default(), + element_opaque_regions: Default::default(), + opaque_regions: Default::default(), + opaque_regions_index: Default::default(), + element_damage_index: Default::default(), + element_visible_area_workhouse: Default::default(), + last_state: Default::default(), + } + } + + /// Get the [`OutputModeSource`] of the [`OutputDamageTracker`] + pub fn mode(&self) -> &OutputModeSource { + &self.mode + } + + /// Render this output with the provided [`Renderer`] + /// + /// - `elements` for this output in front-to-back order + #[instrument(level = "trace", parent = &self.span, skip(renderer, framebuffer, elements, clear_color))] + #[profiling::function] + pub fn render_output( + &mut self, + renderer: &mut R, + framebuffer: &mut R::Framebuffer<'_>, + age: usize, + elements: &[E], + clear_color: impl Into, + ) -> Result, Error> + where + E: RenderElement, + R: Renderer, + R::TextureId: Texture, + { + let clear_color = clear_color.into(); + let (output_size, output_scale, output_transform) = + std::convert::TryInto::<(Size, Scale, Transform)>::try_into(&self.mode)?; + + // Output transform is specified in surface-rotation, so inversion gives us the + // render transform for the output itself. + let output_transform = output_transform.invert(); + + // We have to apply to output transform to the output size so that the intersection + // tests in damage_output_internal produces the correct results and do not crop + // damage with the wrong size + let output_geo = Rectangle::from_size(output_transform.transform_size(output_size)); + + // This will hold all the damage we need for this rendering step + let mut render_elements: Vec<&E> = Vec::with_capacity(elements.len()); + let states = self.damage_output_internal( + age, + elements, + output_scale, + output_transform, + output_geo, + Some(clear_color), + &mut render_elements, + None, + ); + + if self.damage.is_empty() { + trace!("no damage, skipping rendering"); + return Ok(RenderOutputResult::skipped(states)); + } + + self.render_output_internal(renderer, framebuffer, &render_elements, clear_color, states) + } + + /// Render this output with the provided [`Renderer`] and provided states. + /// + /// Note: This is meant for cases, where rendering should happen with a different + /// age value, but `damage_output` was used previously to reflect some internal + /// damage tracking. + /// + /// - `elements` for this output in front-to-back order + #[instrument(level = "trace", parent = &self.span, skip(renderer, framebuffer, elements, clear_color, states))] + #[profiling::function] + pub fn render_output_with_states( + &mut self, + renderer: &mut R, + framebuffer: &mut R::Framebuffer<'_>, + age: usize, + elements: &[E], + clear_color: impl Into, + states: RenderElementStates, + ) -> Result, Error> + where + E: RenderElement, + R: Renderer, + R::TextureId: Texture, + { + let clear_color = clear_color.into(); + let (output_size, output_scale, output_transform) = + std::convert::TryInto::<(Size, Scale, Transform)>::try_into(&self.mode)?; + + // Output transform is specified in surface-rotation, so inversion gives us the + // render transform for the output itself. + let output_transform = output_transform.invert(); + + // We have to apply to output transform to the output size so that the intersection + // tests in damage_output_internal produces the correct results and do not crop + // damage with the wrong size + let output_geo = Rectangle::from_size(output_transform.transform_size(output_size)); + + // This will hold all the damage we need for this rendering step + let mut render_elements: Vec<&E> = Vec::with_capacity(elements.len()); + let _ = self.damage_output_internal( + age, + elements, + output_scale, + output_transform, + output_geo, + Some(clear_color), + &mut render_elements, + Some(&states), + ); + + if self.damage.is_empty() { + trace!("no damage, skipping rendering"); + return Ok(RenderOutputResult::skipped(states)); + } + + self.render_output_internal(renderer, framebuffer, &render_elements, clear_color, states) + } + + /// Damage this output and return the damage without actually rendering the difference + /// + /// - `elements` for this output in front-to-back order + #[instrument(level = "trace", parent = &self.span, skip(elements))] + #[profiling::function] + pub fn damage_output<'a, 'e, E>( + &'a mut self, + age: usize, + elements: &'e [E], + ) -> Result<(Option<&'a Vec>>, RenderElementStates), OutputNoMode> + where + E: Element, + { + let (output_size, output_scale, output_transform) = self.mode.clone().try_into()?; + + // Output transform is specified in surface-rotation, so inversion gives us the + // render transform for the output itself. + let output_transform = output_transform.invert(); + + // We have to apply to output transform to the output size so that the intersection + // tests in damage_output_internal produces the correct results and do not crop + // damage with the wrong size + let output_geo = Rectangle::from_size(output_transform.transform_size(output_size)); + + let mut render_elements: Vec<&E> = Vec::with_capacity(elements.len()); + let states = self.damage_output_internal( + age, + elements, + output_scale, + output_transform, + output_geo, + self.last_state.clear_color, + &mut render_elements, + None, + ); + + if self.damage.is_empty() { + Ok((None, states)) + } else { + Ok((Some(&self.damage), states)) + } + } + + #[allow(clippy::too_many_arguments)] + #[profiling::function] + fn damage_output_internal<'a, E>( + &mut self, + age: usize, + elements: &'a [E], + output_scale: Scale, + output_transform: Transform, + output_geo: Rectangle, + clear_color: Option, + render_elements: &mut Vec<&'a E>, + with_states: Option<&RenderElementStates>, + ) -> RenderElementStates + where + E: Element, + { + self.damage.clear(); + self.opaque_regions.clear(); + self.opaque_regions_index.clear(); + self.element_damage_index.clear(); + + let mut element_render_states = RenderElementStates { + states: HashMap::with_capacity(elements.len()), + }; + + // we have to take the element damage to be able to move it around + let mut element_damage = std::mem::take(&mut self.element_damage); + + let mut element_visible_area_workhouse = std::mem::take(&mut self.element_visible_area_workhouse); + let mut render_element_z_index = 0; + for element in elements.iter() { + let element_id = element.id(); + let element_loc = element.geometry(output_scale).loc; + + // First test if the element overlaps with the output + // if not we can skip it + let element_output_geometry = match element.geometry(output_scale).intersection(output_geo) { + Some(geo) => geo, + None => continue, + }; + + // Then test if the element is completely hidden behind opaque regions + element_visible_area_workhouse.clear(); + element_visible_area_workhouse.push(element_output_geometry); + element_visible_area_workhouse = Rectangle::subtract_rects_many_in_place( + element_visible_area_workhouse, + self.opaque_regions.iter().copied(), + ); + let element_visible_area = element_visible_area_workhouse + .iter() + .fold(0usize, |acc, item| acc + (item.size.w * item.size.h) as usize); + + // No need to draw a completely hidden element + if element_visible_area == 0 { + // We allow multiple instance of a single element, so do not + // override the state if we already have one + if !element_render_states.states.contains_key(element_id) { + element_render_states + .states + .insert(element_id.clone(), RenderElementState::skipped()); + } + continue; + } + + let element_src = element.src(); + let element_geometry = element.geometry(output_scale); + let element_transform = element.transform(); + let element_alpha = element.alpha(); + let element_last_state = self.last_state.elements.get(element.id()); + let element_is_framebuffer_effect = element.is_framebuffer_effect(); + + self.element_damage_index.push(self.damage.len()); + if element_last_state + .map(|s| { + !s.instance_matches( + element_src, + element_geometry, + element_transform, + element_alpha, + render_element_z_index, + element_is_framebuffer_effect, + ) + }) + .unwrap_or(true) + { + if let Some(intersection) = element_geometry.intersection(output_geo) { + self.damage.push(intersection); + } + if let Some(state) = element_last_state { + self.damage.extend( + state + .last_instances + .iter() + .filter_map(|i| i.last_geometry.intersection(output_geo)), + ); + } + } else { + let element_output_damage = element + .damage_since( + output_scale, + self.last_state.elements.get(element_id).map(|s| s.last_commit), + ) + .into_iter() + .map(|mut d| { + d.loc += element_loc; + d + }) + .filter_map(|geo| geo.intersection(output_geo)); + self.damage.extend(element_output_damage); + } + + let element_opaque_regions_start_index = self.opaque_regions.len(); + let element_opaque_regions = element + .opaque_regions(output_scale) + .into_iter() + .map(|mut region| { + region.loc += element_loc; + region + }) + .filter_map(|geo| geo.intersection(output_geo)); + self.opaque_regions.extend(element_opaque_regions); + let element_opaque_regions_end_index = self.opaque_regions.len(); + self.opaque_regions_index + .push(element_opaque_regions_start_index..element_opaque_regions_end_index); + + render_element_z_index += 1; + render_elements.push(element); + + if let Some(state) = element_render_states.states.get_mut(element_id) { + if matches!(state.presentation_state, RenderElementPresentationState::Skipped) { + *state = RenderElementState::rendered(element_visible_area); + } else { + state.visible_area += element_visible_area; + } + if element_is_framebuffer_effect { + // if we have multiple instance of the same Id, we need to force a call + // to `capture_framebuffer` before both `draw`-calls, as we can't rely on the cache. + static WARN_ONCE: Once = Once::new(); + WARN_ONCE.call_once(|| { + warn!("Duplicated FramebufferEffect element. Re-capturing every frame"); + }); + state.needs_capture = true; + } + } else { + element_render_states.states.insert( + element_id.clone(), + RenderElementState::rendered(element_visible_area), + ); + } + } + std::mem::swap( + &mut self.element_visible_area_workhouse, + &mut element_visible_area_workhouse, + ); + + let mut force_effect_redraw = false; + + // add the damage for elements gone that are not covered an opaque region + let mut elements_gone = self + .last_state + .elements + .iter() + .filter(|(id, _)| { + element_render_states + .states + .get(id) + .map(|state| state.presentation_state == RenderElementPresentationState::Skipped) + .unwrap_or(true) + }) + .peekable(); + + if elements_gone.peek().is_some() { + force_effect_redraw = true; + } + + for (_, state) in elements_gone { + self.damage.extend( + state + .last_instances + .iter() + .filter_map(|i| i.last_geometry.intersection(output_geo)), + ); + } + + // damage regions no longer covered by opaque regions + element_damage.clear(); + element_damage.extend_from_slice(&self.last_state.opaque_regions); + element_damage = + Rectangle::subtract_rects_many_in_place(element_damage, self.opaque_regions.iter().copied()); + if element_damage.iter().any(|rect| !rect.is_empty()) { + force_effect_redraw = true; + } + self.damage.extend_from_slice(&element_damage); + + // we no longer need the element damage, return it so that we can + // re-use its allocation next time + std::mem::swap(&mut self.element_damage, &mut element_damage); + + if self.last_state.size != Some(output_geo.size) + || self.last_state.transform != Some(output_transform) + || self.last_state.clear_color != clear_color + { + // The output geometry or transform changed, so just damage everything + trace!( + previous_geometry = ?self.last_state.size, + current_geometry = ?output_geo.size, + previous_transform = ?self.last_state.transform, + current_transform = ?output_transform, + previous_clear_color = ?self.last_state.clear_color, + current_clear_color = ?clear_color, + "Output geometry, transform or clear color changed, damaging whole output geometry"); + self.damage.clear(); + self.damage.push(output_geo); + force_effect_redraw = true; + } + + // for backdrop elements check if anything below them changed and add full-damage, if it did. + for (z_index, element) in render_elements + .iter() + .enumerate() + .rev() + .filter(|(_, e)| e.is_framebuffer_effect()) + { + let damage_index = if force_effect_redraw { + 0 + } else { + self.element_damage_index[z_index] + }; + let opaque_regions_index = if force_effect_redraw { + self.opaque_regions.len() + } else { + self.opaque_regions_index[z_index].start + }; + let element_geometry = element.geometry(output_scale); + // SAFETY: render_elements only contains elements overlapping with the output geometry + let intersection = element_geometry.intersection(output_geo).unwrap(); + let element_state = element_render_states.states.get_mut(element.id()).unwrap(); + let with_element_state = with_states + .as_ref() + .and_then(|states| states.states.get(element.id())); + + if element_state.needs_capture + || with_element_state.is_some_and(|state| state.needs_capture) + || self + .damage + .iter() + .skip(damage_index) + .any(|d| d.overlaps(intersection)) + { + element_state.needs_capture = true; + self.damage.push(intersection); + // also drop all opaque regions on top, so they don't block re-drawing below the blur element + for region in self.opaque_regions.iter_mut().take(opaque_regions_index) { + // we want to leave `self.opaque_regions_index` intact, + // fixing it up would be very involved, so lets do the next best thing + // and keep at least part of the opaque region, if possible. + *region = Rectangle::subtract_rect(*region, intersection) + .into_iter() + .next() + .unwrap_or_default(); + } + } + } + + // That is all completely new damage, which we need to store for subsequent renders + let mut new_damage = self.damage.clone(); + new_damage.shrink_to_fit(); + + // We now add old damage states, if we have an age value + if age > 0 && self.last_state.old_damage.len() >= age { + trace!("age of {} recent enough, using old damage", age); + // We do not need even older states anymore + self.last_state.old_damage.truncate(age); + self.damage + .extend(self.last_state.old_damage.iter().take(age - 1).flatten().copied()); + } else { + trace!( + "no old damage available, re-render everything. age: {} old_damage len: {}", + age, + self.last_state.old_damage.len(), + ); + // we still truncate the old damage to prevent growing + // indefinitely in case we are continuously called with + // an age of 0 + self.last_state.old_damage.truncate(MAX_AGE); + // just damage everything, if we have no damage + self.damage.clear(); + self.damage.push(output_geo); + }; + + // Optimize the damage for rendering + + // Clamp all rectangles to the bounds removing the ones without intersection. + self.damage.retain_mut(|rect| { + if let Some(intersected) = rect.intersection(output_geo) { + *rect = intersected; + true + } else { + false + } + }); + + self.damage_shaper.shape_damage(&mut self.damage); + + if self.damage.is_empty() { + trace!("nothing damaged, exiting early"); + return element_render_states; + } + + let mut new_elements_state = std::mem::take(&mut self.last_state.elements); + new_elements_state.clear(); + new_elements_state.reserve(render_elements.len()); + let new_elements_state = + render_elements + .iter() + .enumerate() + .fold(new_elements_state, |mut map, (z_index, elem)| { + let id = elem.id(); + let elem_src = elem.src(); + let elem_alpha = elem.alpha(); + let elem_geometry = elem.geometry(output_scale); + let elem_transform = elem.transform(); + let element_is_framebuffer_effect = elem.is_framebuffer_effect(); + + if let Some(state) = map.get_mut(id) { + state.last_instances.push(ElementInstanceState { + last_src: elem_src, + last_geometry: elem_geometry, + last_transform: elem_transform, + last_alpha: elem_alpha, + last_z_index: z_index, + last_is_framebuffer_effect: element_is_framebuffer_effect, + }); + } else { + let current_commit = elem.current_commit(); + map.insert( + id.clone(), + ElementState { + last_commit: current_commit, + last_instances: smallvec![ElementInstanceState { + last_src: elem_src, + last_geometry: elem_geometry, + last_transform: elem_transform, + last_alpha: elem_alpha, + last_z_index: z_index, + last_is_framebuffer_effect: element_is_framebuffer_effect, + }], + }, + ); + } + + map + }); + + self.last_state + .effects_cache + .retain(|id, _| new_elements_state.contains_key(id)); + self.last_state.size = Some(output_geo.size); + self.last_state.transform = Some(output_transform); + self.last_state.elements = new_elements_state; + self.last_state.old_damage.push_front(new_damage); + self.last_state.opaque_regions.clear(); + self.last_state + .opaque_regions + .extend(self.opaque_regions.iter().copied()); + self.last_state.opaque_regions.shrink_to_fit(); + self.last_state.clear_color = clear_color; + + element_render_states + } + + #[instrument(level = "trace", parent = &self.span, skip(renderer, framebuffer, render_elements, clear_color, states))] + #[profiling::function] + fn render_output_internal( + &mut self, + renderer: &mut R, + framebuffer: &mut R::Framebuffer<'_>, + render_elements: &[E], + clear_color: Color32F, + states: RenderElementStates, + ) -> Result, Error> + where + E: RenderElement, + R: Renderer, + R::TextureId: Texture, + { + let (output_size, output_scale, output_transform) = + std::convert::TryInto::<(Size, Scale, Transform)>::try_into(&self.mode)?; + // Output transform is specified in surface-rotation, so inversion gives us the + // render transform for the output itself. + let output_transform = output_transform.invert(); + + trace!( + "rendering with damage {:?} and opaque regions {:?}", + self.damage, self.opaque_regions + ); + + let render_res = (|| { + // we have to take the element damage to be able to move it around + let mut element_damage = std::mem::take(&mut self.element_damage); + let mut element_opaque_regions = std::mem::take(&mut self.element_opaque_regions); + let mut frame = renderer.render(framebuffer, output_size, output_transform)?; + + element_damage.clear(); + element_damage.extend_from_slice(&self.damage); + element_damage = + Rectangle::subtract_rects_many_in_place(element_damage, self.opaque_regions.iter().copied()); + + trace!("clearing damage {:?}", element_damage); + frame.clear(clear_color, &element_damage)?; + + for (z_index, element) in render_elements.iter().rev().enumerate() { + let element_id = element.id(); + let element_geometry = element.geometry(output_scale); + + element_damage.clear(); + element_damage.extend( + self.damage + .iter() + .filter_map(|d| d.intersection(element_geometry)), + ); + + let element_opaque_regions_range = + self.opaque_regions_index.iter().rev().nth(z_index).unwrap(); + element_damage = Rectangle::subtract_rects_many_in_place( + element_damage, + self.opaque_regions[..element_opaque_regions_range.start] + .iter() + .copied(), + ); + element_damage.iter_mut().for_each(|d| { + d.loc -= element_geometry.loc; + }); + + if element_damage.is_empty() { + trace!( + "skipping rendering element {:?} with geometry {:?}, no damage", + element_id, element_geometry + ); + continue; + } + + element_opaque_regions.clear(); + element_opaque_regions.extend( + self.opaque_regions[element_opaque_regions_range.start..element_opaque_regions_range.end] + .iter() + .copied() + .map(|mut rect| { + rect.loc -= element_geometry.loc; + rect + }), + ); + + trace!( + "rendering element {:?} with geometry {:?} and damage {:?}", + element_id, element_geometry, element_damage, + ); + + let element_src = element.src(); + if states + .element_render_state(element_id.clone()) + .is_some_and(|state| state.needs_capture) + { + let cache = self + .last_state + .effects_cache + .entry(element_id.clone()) + .or_default(); + element.capture_framebuffer(&mut frame, element_src, element_geometry, cache)?; + } + + element.draw( + &mut frame, + element_src, + element_geometry, + &element_damage, + &element_opaque_regions, + self.last_state.effects_cache.get(element_id), + )?; + } + + // return the element damage so that we can re-use the allocation + std::mem::swap(&mut self.element_damage, &mut element_damage); + std::mem::swap(&mut self.element_opaque_regions, &mut element_opaque_regions); + frame.finish() + })(); + + match render_res { + Ok(sync) => Ok(RenderOutputResult { + sync, + damage: Some(&self.damage), + states, + }), + Err(err) => { + // if the rendering errors on us, we need to be prepared, that this whole buffer was partially updated and thus now unusable. + // thus clean our old states before returning + self.last_state = Default::default(); + Err(Error::Rendering(err)) + } + } + } +} diff --git a/vendor/smithay/src/backend/renderer/damage/shaper.rs b/vendor/smithay/src/backend/renderer/damage/shaper.rs new file mode 100644 index 0000000..caff076 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/damage/shaper.rs @@ -0,0 +1,411 @@ +use std::mem; + +use crate::utils::{Physical, Rectangle, Size}; + +/// The recommended minimum tile side. +const DEFAULT_MIN_TILE_SIDE: i32 = 16; + +/// The maximum ratio of the largest damage rectangle to the current damage bbox. +const MAX_DAMAGE_TO_DAMAGE_BBOX_RATIO: f32 = 0.9; + +/// The gap between tiles damage to merge tiles into one. +const TILE_DAMAGE_GAP_FRACTION: i32 = 4; + +/// State of the damage shaping. +#[derive(Debug, Default)] +pub struct DamageShaper { + /// Cache for tiles to avoid re-allocation. + tiles_cache: Vec, + /// The damage accumulated during shaping. + out_damage: Vec>, +} + +impl DamageShaper { + /// Shape damage rectangles. + #[profiling::function] + pub fn shape_damage(&mut self, in_damage: &mut Vec>) { + self.out_damage.clear(); + self.tiles_cache.clear(); + + // Call the implementation without direction. + self.shape_damage_impl(in_damage, None, false); + + // The shaped damage is inside of `out_damage`, so swap it with `in_damage` since + // it's irrelevant. + mem::swap(&mut self.out_damage, in_damage); + } + + // A divide and conquer hybrid damage shaping algorithm. + // + // The key idea is to split rectangles by non-overlapping segments on dominating axis (largest + // side of the damage bounding box). + // + // When damage is overlapping through the entire `in_damage` span we shape damage using the + // _tile_ based shaper, where the damage bounding box is split into tiles and damage is being + // computed for each tile individually by walking all the current damage rectangles. + #[inline(never)] + #[profiling::function] + fn shape_damage_impl( + &mut self, + in_damage: &mut [Rectangle], + last_direction: Option, + invert_direction: bool, + ) { + // Optimize small damage input. + if in_damage.is_empty() { + return; + } else if in_damage.len() == 1 { + self.out_damage.push(in_damage[0]); + return; + } + + // Compute the effective damage bounding box and the maximum damaged area rectangle. + + let (x_min, y_min, x_max, y_max, max_damage_area) = + in_damage + .iter() + .fold((i32::MAX, i32::MAX, i32::MIN, i32::MIN, i32::MIN), |acc, rect| { + let area = rect.size.w.saturating_mul(rect.size.h); + ( + acc.0.min(rect.loc.x), + acc.1.min(rect.loc.y), + acc.2.max(rect.loc.x + rect.size.w), + acc.3.max(rect.loc.y + rect.size.h), + acc.4.max(area), + ) + }); + + let bbox_w = x_max - x_min; + let bbox_h = y_max - y_min; + + let damage_bbox = Rectangle::::new((x_min, y_min).into(), (bbox_w, bbox_h).into()); + + // Damage the current bounding box when there's a damage rect covering near all the area. + if max_damage_area as f32 / (damage_bbox.size.w.saturating_mul(damage_bbox.size.h)) as f32 + > MAX_DAMAGE_TO_DAMAGE_BBOX_RATIO + { + self.out_damage.push(damage_bbox); + return; + } + + // Now we try to split bounding box to process non-overlapping damage rects separately. + // + // The whole approach is recursive and splits viewport if and only if we have a gap + // in the current segment or rectangles touch each other, since the renderer excludes + // borders. + // + // Examples: + // [0, 3], [1, 2] [2, 3] [3, 4] + // will have a split at ^ + // [0, 3], [1, 2] [2, 3] [6, 10] + // will have a split at ^ + // + // Resulting in recursively trying to shape damage before and after + // split point. + + let mut direction = if bbox_w >= bbox_h { + DamageSplitAxis::X + } else { + DamageSplitAxis::Y + }; + + if invert_direction { + direction = direction.invert(); + } + + // The coordinate where the first rectangle ends and where the potential overlap + // will end. + let mut overlap_end = match direction { + DamageSplitAxis::X => { + if Some(direction) != last_direction { + in_damage.sort_unstable_by(|lhs, rhs| { + // Sort ascending by X and then descending by width, so when multiple + // rectangles with the same X are present the first will overlap the most. + lhs.loc.x.cmp(&rhs.loc.x).then(rhs.size.w.cmp(&lhs.size.w)) + }); + } + in_damage[0].loc.x + in_damage[0].size.w + } + DamageSplitAxis::Y => { + if Some(direction) != last_direction { + in_damage.sort_unstable_by(|lhs, rhs| { + // Sort ascending by Y and then descending by height, so when multiple + // rectangles with the same Y are present the first will overlap the most. + lhs.loc.y.cmp(&rhs.loc.y).then(rhs.size.h.cmp(&lhs.size.h)) + }); + } + in_damage[0].loc.y + in_damage[0].size.h + } + }; + + // The start of overlap. + let mut overlap_start_idx = 0; + for idx in overlap_start_idx + 1..in_damage.len() { + let rect = in_damage[idx]; + let (rect_start, rect_end) = match direction { + DamageSplitAxis::X => (rect.loc.x, rect.loc.x + rect.size.w), + DamageSplitAxis::Y => (rect.loc.y, rect.loc.y + rect.size.h), + }; + + // NOTE the renderer excludes the boundary, otherwise we need `>`. + if rect_start >= overlap_end { + self.shape_damage_impl(&mut in_damage[overlap_start_idx..idx], Some(direction), false); + + // Advance the overlap. + overlap_start_idx = idx; + overlap_end = rect_end; + } else { + overlap_end = overlap_end.max(rect_end); + } + } + + // When rectangle covers the entire bounding box and we've tried different direction of + // splitting perform the tiled based shaping. + if overlap_start_idx == 0 && invert_direction { + // We pick more steps for edges which don't have full overlap. + const NUM_TILES: i32 = 4; + // NOTE we need to revert direction back to use larger side preferences. + let (tile_w, tile_h) = match direction.invert() { + DamageSplitAxis::X => (bbox_w / NUM_TILES, bbox_h / (NUM_TILES * 2)), + DamageSplitAxis::Y => (bbox_w / (NUM_TILES * 2), bbox_h / NUM_TILES), + }; + let tile_size = (tile_w.max(MIN_TILE_SIDE), tile_h.max(MIN_TILE_SIDE)).into(); + + self.shape_damage_tiled(in_damage, damage_bbox, tile_size); + } else { + self.shape_damage_impl( + &mut in_damage[overlap_start_idx..], + Some(direction), + overlap_start_idx == 0, + ); + } + } + + #[inline] + #[profiling::function] + fn shape_damage_tiled( + &mut self, + in_damage: &[Rectangle], + bbox: Rectangle, + tile_size: Size, + ) { + self.tiles_cache.clear(); + let tile_gap: Size = From::from(( + tile_size.w / TILE_DAMAGE_GAP_FRACTION, + tile_size.h / TILE_DAMAGE_GAP_FRACTION, + )); + + for x in (bbox.loc.x..bbox.loc.x + bbox.size.w).step_by(tile_size.w as usize) { + let mut tiles_in_column = 0; + for y in (bbox.loc.y..bbox.loc.y + bbox.size.h).step_by(tile_size.h as usize) { + // NOTE the in_damage is constrained to the `bbox`, so it can't go outside + // the tile, even though some tiles could go outside the `bbox`. + let bbox = Rectangle::::new((x, y).into(), tile_size); + let mut tile = Tile { + bbox, + damage: None, + #[cfg(test)] + merged_tiles: 0, + }; + + // Intersect damage regions with the given tile bounding box. + for damage_rect in in_damage.iter() { + if let Some(intersection) = tile.bbox.intersection(*damage_rect) { + tile.damage = if let Some(tile_damage) = tile.damage { + Some(intersection.merge(tile_damage)) + } else { + Some(intersection) + }; + } + } + + tiles_in_column += 1; + self.tiles_cache.push(tile); + } + + // Try to reduce amount of damage by merging adjacent tiles. + let num_tiles = self.tiles_cache.len(); + for idx in num_tiles - tiles_in_column..num_tiles - 1 { + let (damage, adjacent_damage) = + match (self.tiles_cache[idx].damage, self.tiles_cache[idx + 1].damage) { + (Some(damage), Some(adjacent_damage)) => (damage, adjacent_damage), + _ => continue, + }; + + if damage.loc.y + damage.size.h + tile_gap.h >= adjacent_damage.loc.y + && (damage.size.w - adjacent_damage.size.w).abs() < tile_gap.w + { + self.tiles_cache[idx].damage = None; + self.tiles_cache[idx + 1].damage = Some(damage.merge(adjacent_damage)); + #[cfg(test)] + { + self.tiles_cache[idx + 1].merged_tiles = self.tiles_cache[idx].merged_tiles + 1; + } + } + } + } + + self.out_damage + .extend(self.tiles_cache.iter().filter_map(|tile| tile.damage)); + } +} + +/// Tile with the damage tracking information. +#[derive(Debug)] +struct Tile { + /// Bounding box for the given tile. + bbox: Rectangle, + /// The accumulated damage in the tile. + damage: Option>, + // To ensure that damage is constrained in tests. + #[cfg(test)] + merged_tiles: i32, +} + +/// Direction to split damage bounding box. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DamageSplitAxis { + /// Split damage bounding box on x axis. + X, + /// Split damage bounding box on y axis. + Y, +} + +impl DamageSplitAxis { + /// Invert the split direction. + fn invert(self) -> Self { + match self { + DamageSplitAxis::X => DamageSplitAxis::Y, + DamageSplitAxis::Y => DamageSplitAxis::X, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn shaper() -> DamageShaper { + DamageShaper::default() + } + + // Check that tiles align and not overlap. + fn check_tiles(tiles: &[Tile]) { + let mut xs: Vec = tiles.iter().map(|tile| tile.bbox.loc.x).collect(); + let rows = xs.iter().filter(|&&x| tiles[0].bbox.loc.x == x).count(); + xs.dedup(); + let cols = xs.len(); + + let mut bbox = tiles[0].bbox; + for col in 0..cols { + for row in 0..rows { + let tile = &tiles[col * rows + row]; + assert_eq!(bbox, tile.bbox); + if let Some(damage) = tile.damage { + // Ensure that all tile damage is within them. + assert!(tile.bbox.loc.x <= damage.loc.x && damage.size.w <= tile.bbox.size.w); + assert!( + tile.bbox.loc.y - tile.bbox.size.h * tile.merged_tiles <= damage.loc.y + && damage.size.h <= tile.bbox.loc.y + tile.bbox.size.h + ); + } + bbox.loc.y += bbox.size.h; + } + bbox.loc.y = tiles[0].bbox.loc.y; + bbox.loc.x += bbox.size.w; + } + } + + fn damage_area(damage: &[Rectangle]) -> i32 { + let mut area = 0; + for rect in damage { + area += rect.size.w * rect.size.h; + } + area + } + + #[test] + fn tile_shaping() { + let mut damage = vec![ + Rectangle::::new((98, 406).into(), (36, 48).into()), + Rectangle::::new((158, 502).into(), (828, 168).into()), + Rectangle::::new((122, 694).into(), (744, 528).into()), + Rectangle::::new((194, 1318).into(), (420, 72).into()), + Rectangle::::new((146, 1414).into(), (312, 48).into()), + Rectangle::::new((32, 406).into(), (108, 1152).into()), + ]; + + let mut shaper = shaper(); + let mut to_shape = damage.clone(); + shaper.shape_damage(&mut to_shape); + check_tiles(&shaper.tiles_cache); + + // Re-shaping shouldn't trigger tile based algorithm and it should result in the same + // damage, given that end damage is not overlapping. + let mut to_shape_again = to_shape.clone(); + shaper.shape_damage(&mut to_shape_again); + to_shape_again.sort_by_key(|rect| rect.loc.x); + to_shape.sort_by_key(|rect| rect.loc.x); + assert_eq!(to_shape, to_shape_again); + assert!(shaper.tiles_cache.is_empty()); + + // Having big chunk of damage shouldn't trigger tiling. + let bbox = Rectangle::::from_size((3840, 2160).into()); + damage.push(bbox); + shaper.shape_damage(&mut damage); + assert_eq!(damage[0], bbox); + assert!(shaper.tiles_cache.is_empty()); + } + + #[test] + fn small_damage() { + let mut shaper = shaper(); + let mut damage = vec![]; + shaper.shape_damage(&mut damage); + assert!(damage.is_empty()); + + let rect = Rectangle::::from_size((5, 5).into()); + damage.push(rect); + shaper.shape_damage(&mut damage); + assert!(damage.len() == 1); + assert_eq!(damage[0], rect); + } + + #[test] + fn shape_pixels() { + let mut shaper = shaper(); + let mut damage = vec![]; + shaper.shape_damage(&mut damage); + assert!(damage.is_empty()); + + let w = 384; + let h = 216; + + for x in 0..w { + for y in 0..h { + let rect = Rectangle::::new((x, y).into(), (1, 1).into()); + damage.push(rect); + } + } + + let mut to_shape = damage.clone(); + shaper.shape_damage(&mut to_shape); + assert!(shaper.tiles_cache.is_empty()); + assert_eq!(damage_area(&to_shape), w * h); + to_shape.sort_by_key(|rect| rect.loc.x); + assert_eq!(damage, to_shape); + + let w1 = 216; + let h1 = 144; + let overlap1 = Rectangle::::from_size((w1, h1).into()); + let overlap2 = Rectangle::::new((w1, h1).into(), (w - w1, h - h1).into()); + damage.push(overlap1); + damage.push(overlap2); + + shaper.shape_damage(&mut damage); + assert!(shaper.tiles_cache.is_empty()); + assert_eq!(damage_area(&damage), w * h); + assert!(damage.contains(&overlap1)); + assert!(damage.contains(&overlap2)); + } +} diff --git a/vendor/smithay/src/backend/renderer/element/memory.rs b/vendor/smithay/src/backend/renderer/element/memory.rs new file mode 100644 index 0000000..1378d1f --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/memory.rs @@ -0,0 +1,662 @@ +//! Element to render from RGBA memory +//! +//! # Why use this implementation +//! +//! The [`MemoryRenderBuffer`] allows for easy integration of software-rendered elements +//! in the smithay rendering pipeline. As software-rendered elements eventually have to +//! upload to the GPU for rendering damage tracking is a crucial part. The [`MemoryRenderBuffer`] +//! allows for efficient damage tracking by providing a [`RenderContext`] which accumulates the +//! software-rendering damage. It automatically uploads the damaged parts to a [`RendererSuper::TextureId`](crate::backend::renderer::RendererSuper::TextureId) +//! during rendering. +//! +//! # Why **not** to use this implementation +//! +//! As described earlier the [`MemoryRenderBuffer`] is targeted at software rendering, if you have +//! some static content you may want to take a look at [`TextureBuffer`](super::texture::TextureBuffer) +//! or [`TextureRenderBuffer`](super::texture::TextureRenderBuffer) for hardware accelerated rendering. +//! +//! # How to use it +//! +//! The [`MemoryRenderBuffer`] represents a buffer of your data and holds the damage as reported by the [`RenderContext`]. +//! To render the buffer you have to create a [`MemoryRenderBufferRenderElement`] in your render loop as +//! shown in the example. +//! +//! ```no_run +//! # use smithay::{ +//! # backend::renderer::{Color32F, DebugFlags, Frame, ImportMem, Renderer, Texture, TextureFilter, sync::SyncPoint, test::{DummyRenderer, DummyFramebuffer}}, +//! # utils::{Buffer, Physical}, +//! # }; +//! use std::time::{Duration, Instant}; +//! +//! use smithay::{ +//! backend::{ +//! allocator::Fourcc, +//! renderer::{ +//! damage::OutputDamageTracker, +//! element::{ +//! Kind, +//! memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, +//! }, +//! }, +//! }, +//! utils::{Point, Rectangle, Size, Transform}, +//! }; +//! +//! const WIDTH: i32 = 10; +//! const HEIGHT: i32 = 10; +//! +//! // Initialize a empty render buffer +//! let mut buffer = MemoryRenderBuffer::new(Fourcc::Argb8888, (WIDTH, HEIGHT), 1, Transform::Normal, None); +//! +//! // Create a rendering context +//! let mut render_context = buffer.render(); +//! +//! // Draw to the buffer +//! render_context.draw(|buffer| { +//! buffer.chunks_exact_mut(4).for_each(|chunk| { +//! chunk.copy_from_slice(&[255, 231, 199, 255]); +//! }); +//! +//! // Return the whole buffer as damage +//! Result::<_, ()>::Ok(vec![Rectangle::from_size((WIDTH, HEIGHT).into())]) +//! }); +//! +//! // Optionally update the opaque regions +//! render_context.update_opaque_regions(Some(vec![Rectangle::from_size( +//! Size::from((WIDTH, HEIGHT)), +//! )])); +//! +//! // We explicitly drop the context here to make the borrow checker happy +//! std::mem::drop(render_context); +//! +//! // Initialize a static damage tracker +//! let mut damage_tracker = OutputDamageTracker::new((800, 600), 1.0, Transform::Normal); +//! # let mut renderer = DummyRenderer::default(); +//! # let mut framebuffer = DummyFramebuffer; +//! +//! let mut last_update = Instant::now(); +//! +//! loop { +//! let now = Instant::now(); +//! if now.duration_since(last_update) >= Duration::from_secs(3) { +//! let mut render_context = buffer.render(); +//! +//! render_context.draw(|_buffer| { +//! // Update the changed parts of the buffer +//! +//! // Return the updated parts +//! Result::<_, ()>::Ok(vec![Rectangle::from_size((WIDTH, HEIGHT).into())]) +//! }); +//! +//! last_update = now; +//! } +//! +//! // Create a render element from the buffer +//! let location = Point::from((100.0, 100.0)); +//! let render_element = MemoryRenderBufferRenderElement::from_buffer(&mut renderer, location, &buffer, None, None, None, Kind::Unspecified) +//! .expect("Failed to upload from memory to gpu"); +//! +//! // Render the element(s) +//! damage_tracker +//! .render_output(&mut renderer, &mut framebuffer, 0, &[&render_element], [0.8, 0.8, 0.9, 1.0]) +//! .expect("failed to render output"); +//! } +//! ``` + +use std::{ + any::Any, + collections::{HashMap, hash_map::Entry}, + sync::{Arc, Mutex, MutexGuard}, +}; + +use tracing::{instrument, trace, warn}; + +use crate::{ + backend::{ + allocator::{Fourcc, format::get_bpp}, + renderer::{ + ErasedContextId, Frame, ImportMem, Renderer, + utils::{CommitCounter, DamageBag, DamageSet, DamageSnapshot, OpaqueRegions}, + }, + }, + utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform, user_data::UserDataMap}, +}; + +use super::{Element, Id, Kind, RenderElement, UnderlyingStorage}; + +/// A buffer storing pixel data backed by system memory +#[derive(Debug, Clone)] +pub struct MemoryBuffer { + mem: Arc>, + format: Fourcc, + size: Size, + stride: i32, +} + +impl Default for MemoryBuffer { + fn default() -> Self { + Self { + mem: Default::default(), + format: Fourcc::Abgr8888, + size: Default::default(), + stride: Default::default(), + } + } +} + +impl MemoryBuffer { + /// Create a new zeroed memory buffer with the specified format and size + pub fn new(format: Fourcc, size: impl Into>) -> Self { + let size = size.into(); + let stride = size.w * (get_bpp(format).expect("Format with unknown bits per pixel") / 8) as i32; + let mem = vec![0; (stride * size.h) as usize]; + Self { + mem: Arc::new(mem), + format, + size, + stride, + } + } + + /// Create a new memory buffer from a slice with the specified format and size + pub fn from_slice(mem: &[u8], format: Fourcc, size: impl Into>) -> Self { + let size = size.into(); + let stride = size.w * (get_bpp(format).expect("Format with unknown bits per pixel") / 8) as i32; + assert!(mem.len() >= (stride * size.h) as usize); + Self { + mem: Arc::new(mem.to_vec()), + format, + size, + stride, + } + } + + /// Get the size of this buffer + pub fn size(&self) -> Size { + self.size + } + + /// Get the format of this buffer + pub fn format(&self) -> Fourcc { + self.format + } + + /// Get the stride of this buffer + pub fn stride(&self) -> i32 { + self.stride + } + + /// Resize this buffer to the size specified + pub fn resize(&mut self, size: impl Into>) -> bool { + self.size = size.into(); + self.stride = + self.size.w * (get_bpp(self.format).expect("Format with unknown bits per pixel") / 8) as i32; + let mem_size = (self.stride * self.size.h) as usize; + if self.mem.len() != mem_size { + let mem = Arc::make_mut(&mut self.mem); + mem.resize(mem_size, 0); + true + } else { + false + } + } +} + +impl std::ops::Deref for MemoryBuffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.mem + } +} + +impl std::ops::DerefMut for MemoryBuffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *Arc::make_mut(&mut self.mem) + } +} + +#[derive(Debug)] +struct MemoryRenderBufferInner { + mem: MemoryBuffer, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + damage_bag: DamageBag, + textures: HashMap>, + renderer_seen: HashMap, +} + +impl Default for MemoryRenderBufferInner { + fn default() -> Self { + MemoryRenderBufferInner { + mem: Default::default(), + scale: 1, + transform: Transform::Normal, + opaque_regions: None, + damage_bag: DamageBag::default(), + textures: HashMap::default(), + renderer_seen: HashMap::default(), + } + } +} + +impl MemoryRenderBufferInner { + fn new( + format: Fourcc, + size: impl Into>, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + MemoryRenderBufferInner { + mem: MemoryBuffer::new(format, size), + scale, + transform, + opaque_regions, + damage_bag: DamageBag::default(), + textures: HashMap::default(), + renderer_seen: HashMap::default(), + } + } + + fn from_memory( + mem: MemoryBuffer, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + MemoryRenderBufferInner { + mem, + scale, + transform, + opaque_regions, + damage_bag: DamageBag::default(), + textures: HashMap::default(), + renderer_seen: HashMap::default(), + } + } + + fn from_slice( + mem: &[u8], + format: Fourcc, + size: impl Into>, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + Self::from_memory( + MemoryBuffer::from_slice(mem, format, size), + scale, + transform, + opaque_regions, + ) + } + + fn resize(&mut self, size: impl Into>) { + if self.mem.resize(size) { + self.renderer_seen.clear(); + self.textures.clear(); + self.damage_bag.reset(); + self.opaque_regions = None; + } + } + + #[instrument(level = "trace", skip(renderer))] + #[profiling::function] + fn import_texture(&mut self, renderer: &mut R) -> Result + where + R: Renderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + let context_id = renderer.context_id().erased(); + let current_commit = self.damage_bag.current_commit(); + let last_commit = self.renderer_seen.get(&context_id).copied(); + let buffer_damage = self + .damage_bag + .damage_since(last_commit) + .map(|d| d.into_iter().reduce(|a, b| a.merge(b)).unwrap_or_default()) + .unwrap_or_else(|| Rectangle::from_size(self.mem.size())); + + let tex = match self.textures.entry(context_id.clone()) { + Entry::Occupied(entry) => { + let tex = entry.get().downcast_ref().unwrap(); + if !buffer_damage.is_empty() { + trace!("updating memory with damage {:#?}", &buffer_damage); + renderer.update_memory(tex, &self.mem, buffer_damage)? + } + tex.clone() + } + Entry::Vacant(entry) => { + trace!("importing memory"); + let tex = renderer.import_memory(&self.mem, self.mem.format(), self.mem.size(), false)?; + entry.insert(Box::new(tex.clone())); + tex + } + }; + + self.renderer_seen.insert(context_id, current_commit); + Ok(tex) + } +} + +/// A memory backed render buffer +#[derive(Debug, Clone)] +pub struct MemoryRenderBuffer { + id: Id, + inner: Arc>, +} + +impl Default for MemoryRenderBuffer { + fn default() -> Self { + Self { + id: Id::new(), + inner: Default::default(), + } + } +} + +impl MemoryRenderBuffer { + /// Initialize a empty [`MemoryRenderBuffer`] + pub fn new( + format: Fourcc, + size: impl Into>, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + let inner = MemoryRenderBufferInner::new(format, size, scale, transform, opaque_regions); + MemoryRenderBuffer { + id: Id::new(), + inner: Arc::new(Mutex::new(inner)), + } + } + + /// Initialize a [`MemoryRenderBuffer`] from an existing [`MemoryBuffer`] + pub fn from_memory( + mem: MemoryBuffer, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + let inner = MemoryRenderBufferInner::from_memory(mem, scale, transform, opaque_regions); + MemoryRenderBuffer { + id: Id::new(), + inner: Arc::new(Mutex::new(inner)), + } + } + + /// Initialize a [`MemoryRenderBuffer`] from a slice + pub fn from_slice( + mem: &[u8], + format: Fourcc, + size: impl Into>, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + let inner = MemoryRenderBufferInner::from_slice(mem, format, size, scale, transform, opaque_regions); + MemoryRenderBuffer { + id: Id::new(), + inner: Arc::new(Mutex::new(inner)), + } + } + + /// Render to the memory buffer + pub fn render(&mut self) -> RenderContext<'_> { + let guard = self.inner.lock().unwrap(); + RenderContext { + buffer: guard, + damage: Vec::new(), + opaque_regions: None, + } + } +} + +/// A render context for [`MemoryRenderBuffer`] +#[derive(Debug)] +pub struct RenderContext<'a> { + buffer: MutexGuard<'a, MemoryRenderBufferInner>, + damage: Vec>, + opaque_regions: Option>>>, +} + +impl RenderContext<'_> { + /// Resize the buffer + /// + /// Note that this will also reset the opaque regions. + /// If you previously set opaque regions you should update + /// them with [`RenderContext::update_opaque_regions`] + pub fn resize(&mut self, size: impl Into>) { + self.buffer.resize(size); + } + + /// Draw to the buffer + /// + /// Provided closure has to return updated regions. + pub fn draw(&mut self, f: F) -> Result<(), E> + where + F: FnOnce(&mut [u8]) -> Result>, E>, + { + let draw_damage = f(&mut self.buffer.mem)?; + self.damage.extend(draw_damage); + Ok(()) + } + + /// Update the opaque regions + pub fn update_opaque_regions(&mut self, opaque_regions: Option>>) { + self.opaque_regions = Some(opaque_regions); + } +} + +impl Drop for RenderContext<'_> { + fn drop(&mut self) { + self.buffer.damage_bag.add(std::mem::take(&mut self.damage)); + if let Some(opaque_regions) = self.opaque_regions.take() { + self.buffer.opaque_regions = opaque_regions; + } + } +} + +/// A render element for [`MemoryRenderBuffer`] +#[derive(Debug)] +pub struct MemoryRenderBufferRenderElement { + id: Id, + location: Point, + buffer: MemoryBuffer, + alpha: f32, + src: Rectangle, + buffer_scale: i32, + buffer_transform: Transform, + size: Size, + damage: DamageSnapshot, + opaque_regions: OpaqueRegions, + texture: R::TextureId, + kind: Kind, +} + +impl MemoryRenderBufferRenderElement { + /// Create a new [`MemoryRenderBufferRenderElement`] for + /// a [`MemoryRenderBuffer`] + pub fn from_buffer( + renderer: &mut R, + location: impl Into>, + buffer: &MemoryRenderBuffer, + alpha: Option, + src: Option>, + size: Option>, + kind: Kind, + ) -> Result + where + R: ImportMem, + R::TextureId: Send + Clone + 'static, + { + let mut inner = buffer.inner.lock().unwrap(); + let texture = inner.import_texture(renderer)?; + + let size = size + .or_else(|| src.map(|src| Size::from((src.size.w as i32, src.size.h as i32)))) + .unwrap_or_else(|| inner.mem.size().to_logical(inner.scale, inner.transform)); + + let src = src.unwrap_or_else(|| Rectangle::from_size(size.to_f64())); + + Ok(MemoryRenderBufferRenderElement { + id: buffer.id.clone(), + buffer: inner.mem.clone(), + location: location.into(), + alpha: alpha.unwrap_or(1.0), + src, + buffer_scale: inner.scale, + buffer_transform: inner.transform, + size, + opaque_regions: inner + .opaque_regions + .as_deref() + .map(OpaqueRegions::from_slice) + .unwrap_or_default(), + damage: inner.damage_bag.snapshot(), + texture, + kind, + }) + } + + fn physical_size(&self, scale: Scale) -> Size { + ((self.size.to_f64().to_physical(scale).to_point() + self.location).to_i32_round() + - self.location.to_i32_round()) + .to_size() + } + + fn scale(&self) -> Scale { + let src = self.src(); + Scale::from((self.size.w as f64 / src.size.w, self.size.h as f64 / src.size.h)) + } +} + +impl Element for MemoryRenderBufferRenderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.damage.current_commit() + } + + fn transform(&self) -> Transform { + self.buffer_transform + } + + fn src(&self) -> Rectangle { + self.src.to_buffer( + self.buffer_scale as f64, + self.buffer_transform, + &self.size.to_f64(), + ) + } + + fn geometry(&self, scale: Scale) -> Rectangle { + Rectangle::new(self.location.to_i32_round(), self.physical_size(scale)) + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + let physical_size = self.physical_size(scale); + let logical_scale = self.scale(); + + self.damage + .damage_since(commit) + .map(|damage| { + damage + .into_iter() + .filter_map(|rect| { + rect.to_f64() + .to_logical( + self.buffer_scale as f64, + self.buffer_transform, + &self.buffer.size.to_f64(), + ) + .intersection(self.src) + .map(|mut rect| { + rect.loc -= self.src.loc; + rect.upscale(logical_scale) + }) + .map(|rect| { + let surface_scale = + physical_size.to_f64() / self.size.to_f64().to_physical(scale); + rect.to_physical_precise_up(surface_scale * scale) + }) + }) + .collect::>() + }) + .unwrap_or_else(|| DamageSet::from_slice(&[Rectangle::from_size(physical_size)])) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + if self.alpha < 1.0 { + return OpaqueRegions::default(); + } + + let physical_size = self.physical_size(scale); + let logical_scale = self.scale(); + + self.opaque_regions + .iter() + .filter_map(|rect| { + rect.to_f64() + .to_logical( + self.buffer_scale as f64, + self.buffer_transform, + &self.buffer.size.to_f64(), + ) + .intersection(self.src) + .map(|mut rect| { + rect.loc -= self.src.loc; + rect.upscale(logical_scale) + }) + .map(|rect| { + let surface_scale = physical_size.to_f64() / self.size.to_f64().to_physical(scale); + rect.to_physical_precise_up(surface_scale * scale) + }) + }) + .collect::>() + } + + fn alpha(&self) -> f32 { + self.alpha + } + + fn kind(&self) -> Kind { + self.kind + } +} + +impl RenderElement for MemoryRenderBufferRenderElement +where + R: Renderer + ImportMem, + R::TextureId: 'static, +{ + #[instrument(level = "trace", skip(self, frame))] + #[profiling::function] + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + frame.render_texture_from_to( + &self.texture, + src, + dst, + damage, + opaque_regions, + self.buffer_transform, + self.alpha, + ) + } + + #[inline] + fn underlying_storage(&self, _renderer: &mut R) -> Option> { + Some(UnderlyingStorage::Memory(&self.buffer)) + } +} diff --git a/vendor/smithay/src/backend/renderer/element/mod.rs b/vendor/smithay/src/backend/renderer/element/mod.rs new file mode 100644 index 0000000..b687793 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/mod.rs @@ -0,0 +1,1840 @@ +//! Common base for elements that can be drawn by a [`Renderer`] +//! +//! A [`RenderElement`] defines what should be [`draw`](RenderElement::draw)n where. +//! Additionally it provides the foundation for effective damage tracked rendering +//! by allowing to query for damage between two [`CommitCounter`]s. +//! +//! For specialized renderers it can optionally provide access to the [`UnderlyingStorage`] +//! of the element. +//! +//! Out of the box smithay provides the following elements +//! - [`memory`] - Memory based render element +//! - [`texture`] - Texture based render element +//! - [`surface`] - Wayland surface render element +//! - [`solid`] - Solid color render element +//! +//! The [`render_elements!`] macro provides an easy way to aggregate multiple different [RenderElement]s +//! into a single enum. +//! +//! See the [`damage`](crate::backend::renderer::damage) module for more information on +//! damage tracking. + +use std::{ + collections::HashMap, + sync::{Arc, Weak}, +}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::{Resource, backend::ObjectId}; + +use crate::{ + output::{Output, WeakOutput}, + utils::{Buffer as BufferCoords, Physical, Point, Rectangle, Scale, Transform, user_data::UserDataMap}, +}; + +#[cfg(feature = "wayland_frontend")] +use super::utils::Buffer; +use super::{ + Renderer, + utils::{CommitCounter, DamageSet, OpaqueRegions}, +}; + +pub mod memory; +pub mod solid; +#[cfg(feature = "wayland_frontend")] +pub mod surface; +pub mod texture; +pub mod utils; + +crate::utils::ids::id_gen!(external_id); + +#[derive(Debug, Clone)] +/// A unique id for a [`RenderElement`] +pub struct Id { + inner: InnerId, + namespace: Option, +} + +#[derive(Debug, Clone)] +enum InnerId { + #[cfg(feature = "wayland_frontend")] + WaylandResource(ObjectId), + External(Arc), +} + +#[derive(Debug, Clone)] +/// A weak reference to a unique id for a [`RenderElement`] +pub struct WeakId { + inner: InnerWeakId, + namespace: Option, +} + +#[derive(Debug, Clone)] +enum InnerWeakId { + #[cfg(feature = "wayland_frontend")] + WaylandResource(ObjectId), + External(Weak), +} + +#[derive(Debug, Clone)] +struct ExternalId(usize); + +impl ExternalId { + fn new() -> Self { + ExternalId(external_id::next()) + } +} + +impl Drop for ExternalId { + fn drop(&mut self) { + external_id::remove(self.0); + } +} + +impl PartialEq for Id { + fn eq(&self, other: &Self) -> bool { + self.namespace == other.namespace + && match (&self.inner, &other.inner) { + #[cfg(feature = "wayland_frontend")] + (InnerId::WaylandResource(this_obj), InnerId::WaylandResource(other_obj)) => { + this_obj == other_obj + } + (InnerId::External(this_id), InnerId::External(other_id)) => Arc::ptr_eq(this_id, other_id), + #[allow(unreachable_patterns)] + _ => false, + } + } +} +impl Eq for Id {} + +impl PartialEq for WeakId { + fn eq(&self, other: &Self) -> bool { + self.namespace == other.namespace + && match (&self.inner, &other.inner) { + #[cfg(feature = "wayland_frontend")] + (InnerWeakId::WaylandResource(this_obj), InnerWeakId::WaylandResource(other_obj)) => { + this_obj == other_obj + } + (InnerWeakId::External(this_id), InnerWeakId::External(other_id)) => { + Weak::ptr_eq(this_id, other_id) + } + #[allow(unreachable_patterns)] + _ => false, + } + } +} +impl Eq for WeakId {} + +impl PartialEq for Id { + fn eq(&self, other: &WeakId) -> bool { + let this = self.downgrade(); + &this == other + } +} + +impl std::hash::Hash for Id { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + match &self.inner { + #[cfg(feature = "wayland_frontend")] + InnerId::WaylandResource(obj) => obj.hash(state), + InnerId::External(arc) => { + (Arc::as_ptr(arc) as usize).hash(state); + } + } + self.namespace.hash(state); + } +} + +impl std::hash::Hash for WeakId { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + match &self.inner { + #[cfg(feature = "wayland_frontend")] + InnerWeakId::WaylandResource(obj) => obj.hash(state), + InnerWeakId::External(arc) => { + (Weak::as_ptr(arc) as usize).hash(state); + } + } + self.namespace.hash(state); + } +} + +impl Id { + /// Create an id from a [`Resource`] + /// + /// Note: Calling this function for the same [`Resource`] + /// multiple times will return the same id. + #[cfg(feature = "wayland_frontend")] + pub fn from_wayland_resource(resource: &R) -> Self { + Id { + inner: InnerId::WaylandResource(resource.id()), + namespace: None, + } + } + + /// Create a new unique id + /// + /// Note: The id will be re-used once all instances of this [`Id`] + /// are dropped. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Id { + inner: InnerId::External(Arc::new(ExternalId::new())), + namespace: None, + } + } + + /// Add a namespace to this id. + /// + /// This allows re-use of an `Id`, while still differentiating + /// between multiple instances with a stable Hash. + /// + /// This is especially useful for elements implementing + /// [`Element::is_framebuffer_effect`], where multiple instances + /// of the same id in a single `DamageTracker`-call will + /// degrade performance. + pub fn namespaced(&self, namespace: usize) -> Self { + Id { + inner: self.inner.clone(), + namespace: Some(namespace), + } + } + + /// Remove a previously applied namespace from the `Id`. + pub fn unnamespaced(&self) -> Self { + Id { + inner: self.inner.clone(), + namespace: None, + } + } + + /// Create a weak reference to this Id, which won't keep it from being re-used internally. + pub fn downgrade(&self) -> WeakId { + WeakId { + inner: match &self.inner { + #[cfg(feature = "wayland_frontend")] + InnerId::WaylandResource(id) => InnerWeakId::WaylandResource(id.clone()), + InnerId::External(arc) => InnerWeakId::External(Arc::downgrade(arc)), + }, + namespace: self.namespace, + } + } +} + +impl WeakId { + /// Create `Id` from this weak handle, if it still exist + pub fn upgrade(&self) -> Option { + Some(Id { + inner: match &self.inner { + #[cfg(feature = "wayland_frontend")] + InnerWeakId::WaylandResource(obj) => InnerId::WaylandResource(obj.clone()), + InnerWeakId::External(weak) => InnerId::External(weak.upgrade()?), + }, + namespace: self.namespace, + }) + } +} + +#[cfg(feature = "wayland_frontend")] +impl From<&R> for Id { + #[inline] + fn from(resource: &R) -> Self { + Id::from_wayland_resource(resource) + } +} + +/// The underlying storage for a element +#[derive(Debug, Clone)] +pub enum UnderlyingStorage<'a> { + /// A wayland buffer + #[cfg(feature = "wayland_frontend")] + Wayland(&'a Buffer), + /// A memory backed buffer + Memory(&'a memory::MemoryBuffer), +} + +/// Defines the (optional) reason why a [`Element`] was selected for +/// rendering instead of direct scan-out +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderingReason { + /// The element buffer format is unsuited for direct scan-out + FormatUnsupported, + /// Element was selected for direct scan-out but failed + ScanoutFailed, +} + +/// Defines the presentation state of an element after rendering +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderElementPresentationState { + /// The element was selected for rendering + Rendering { + /// Optional reason why the element was selected for rendering + /// + /// Can be used to make a decision on sending an dmabuf feedback + /// scan-out tranche. + reason: Option, + }, + /// The element was selected for zero-copy scan-out + ZeroCopy, + /// The element was skipped as it is current not visible + Skipped, +} + +/// Defines the element render state after rendering +#[derive(Debug, Clone, Copy)] +pub struct RenderElementState { + /// Holds the physical visible area of the element on the output in pixels. + /// + /// Note: If the presentation_state is [`RenderElementPresentationState::Skipped`] this will be zero. + pub visible_area: usize, + /// Holds the presentation state of the element on the output + pub presentation_state: RenderElementPresentationState, + /// The next `RenderElements::draw` call to this element needs an accompanying `capture_framebuffer`-call. + pub needs_capture: bool, +} + +impl RenderElementState { + pub(crate) fn skipped() -> Self { + RenderElementState { + visible_area: Default::default(), + presentation_state: RenderElementPresentationState::Skipped, + needs_capture: false, + } + } + + pub(crate) fn rendered(visible_area: usize) -> Self { + RenderElementState { + visible_area, + presentation_state: RenderElementPresentationState::Rendering { reason: None }, + needs_capture: false, + } + } +} + +/// Holds the primary scanout output for a surface +#[derive(Debug, Default)] +pub struct PrimaryScanoutOutput(Option<(WeakOutput, RenderElementState)>); + +impl PrimaryScanoutOutput { + /// Update the primary scan-out output from [`RenderElementStates`] + /// + /// If the current primary scan-out output is different than the + /// provided output and the element is present according to the states + /// the provided compare function will be run to select the preferred + /// output. Smithay provides a [`default`](`default_primary_scanout_output_compare`) + /// compare function for convenience. + /// + /// Returns the updated primary scan-out output if any + pub fn update_from_render_element_states( + &mut self, + element_id: impl Into, + output: &Output, + namespace: Option, + states: &RenderElementStates, + compare: F, + ) -> Option + where + F: for<'a> Fn(&'a Output, &'a RenderElementState, &'a Output, &'a RenderElementState) -> &'a Output, + { + let element_id = element_id.into(); + let element_id = namespace + .map(|val| element_id.namespaced(val)) + .unwrap_or(element_id); + let element_was_presented = states.element_was_presented(element_id.clone()); + let element_state = states.element_render_state(element_id); + let primary_scanout_output = &mut self.0; + + let has_valid_output = primary_scanout_output + .as_ref() + .map(|(current, _)| current.is_alive()) + .unwrap_or(false); + let same_output = primary_scanout_output + .as_ref() + .map(|(current, _)| current == output) + .unwrap_or(false); + + // If the element was not presented and we have no valid output + // there is nothing we can do + if !element_was_presented && !has_valid_output { + return None; + } + + // If our current output is the one we received for the update + // and the element was not presented remove it, no need to check further + if !element_was_presented && same_output { + *primary_scanout_output = None; + return None; + } + + // If the element was presented but we have no current valid output + // just directly update and exit early + if element_was_presented && !has_valid_output { + *primary_scanout_output = Some((output.downgrade(), element_state.unwrap())); + return Some(output.clone()); + } + + // If the element was presented on the same output there + // is no need to check + if element_was_presented && has_valid_output && same_output { + primary_scanout_output.as_mut().unwrap().1 = element_state.unwrap(); + return Some(output.clone()); + } + + // If the element was presented on a different output and we have a + // valid current output, run the provided check + if element_was_presented && has_valid_output && !same_output { + let (current_output, current_state) = primary_scanout_output + .as_ref() + .map(|(output, state)| (output.upgrade().unwrap(), state)) + .unwrap(); + let updated = compare( + ¤t_output, + current_state, + output, + element_state.as_ref().unwrap(), + ) + .clone(); + *primary_scanout_output = Some((updated.downgrade(), element_state.unwrap())); + return Some(updated); + } + + // The element was not presented on that output and the current scan-out output + // is a different than we currently have, so we can just leave it as is + primary_scanout_output + .as_ref() + .and_then(|(output, _)| output.upgrade()) + } + + /// Gets the current primary scanout output if any + /// + /// Return `None` if there is no primary scanout output or + /// the stored output is longer alive + pub fn current_output(&self) -> Option { + self.0.as_ref().and_then(|(o, _)| o.upgrade()) + } +} + +/// Default function for primary scan-out selection +/// +/// This will prefer the next output when the visible portion of +/// the element on screen is at least twice the size of the +/// current visible portion. Otherwise it will prefer the output +/// with the higher refresh rate. +pub fn default_primary_scanout_output_compare<'a>( + current_output: &'a Output, + current_state: &RenderElementState, + next_output: &'a Output, + next_state: &RenderElementState, +) -> &'a Output { + const VISIBLE_AREA_THRESHOLD: usize = 2; + + let current_mode = current_output.current_mode(); + let next_mode = next_output.current_mode(); + + // We don't expect to be called with an output that has no mode, + // but to be safe we do not unwrap here + let next_mode_has_higher_refresh = next_mode + .map(|next_mode| { + current_mode + .map(|current_mode| next_mode.refresh > current_mode.refresh) + .unwrap_or(false) + }) + .unwrap_or(false); + + let current_visible_area_threshold = current_state.visible_area * VISIBLE_AREA_THRESHOLD; + let next_mode_visible_area_greater = next_state.visible_area >= current_visible_area_threshold; + + if next_mode_visible_area_greater || next_mode_has_higher_refresh { + next_output + } else { + current_output + } +} + +/// Holds the states for a set of [`RenderElement`]s +#[derive(Default, Debug, Clone)] +pub struct RenderElementStates { + /// Holds the render states of the elements + pub states: HashMap, +} + +impl RenderElementStates { + /// Return the [`RenderElementState`] for the specified [`Id`] + /// + /// Return `None` if the element is not included in the states + pub fn element_render_state(&self, id: impl Into) -> Option { + self.states.get(&id.into()).copied() + } + + /// Returns whether the element with the specified id was presented + /// + /// Returns `false` if the element with the id was not found or skipped + pub fn element_was_presented(&self, id: impl Into) -> bool { + self.element_render_state(id) + .map(|state| state.presentation_state != RenderElementPresentationState::Skipped) + .unwrap_or(false) + } +} + +/// Defines the kind of an [`Element`] +/// +/// This can give the backend a hint about how to handle the element +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub enum Kind { + /// The element represents a cursor + /// + /// An element representing a cursor is expected to change infrequently. + /// Not marking a cursor element as `Cursor` may result in lower performance and increased power usage. + /// In contrast, marking elements that change frequently as `Cursor` can degrade performance significantly. + Cursor, + /// The element is a good candidate for scanout + /// + /// An element marked as a scanout candidate is expected to refresh frequently or with a constant rate. + /// As such it makes a good candidate to save resources by placing it on an overlay plane, when using the drm backend + /// (see [`DrmCompositor`](crate::backend::drm::compositor::DrmCompositor)). + /// + /// Examples of applications like this include video players or games. To detect good candidates compositor should + /// employ heuristics. For example, it might keep track of the surface commit rate and mark surfaces hitting thresholds + /// as scanout candidates. + /// + /// Note: You can select a different `Kind` every time you create the render element from e.g. a wayland surface. + /// This means it is expected that compositors dynamically choose the appropriate `Kind` at runtime instead + /// of relying on static configuration. E.g. it can make sense to switch back to `Unspecified` during very dynamic + /// scenes like animations, as moving planes can be slow on certain hardware. + /// + /// Elements not marked as scanout candidates will never be considered, when selecting potential buffers for overlay planes. + ScanoutCandidate, + /// The element kind is unspecified + #[default] + Unspecified, +} + +/// A single element +pub trait Element { + /// Get the unique id of this element + fn id(&self) -> &Id; + /// Get the current commit position of this element + fn current_commit(&self) -> CommitCounter; + /// Get the location relative to the output + fn location(&self, scale: Scale) -> Point { + self.geometry(scale).loc + } + /// Get the src of the underlying buffer + fn src(&self) -> Rectangle; + /// Get the transform of the underlying buffer + fn transform(&self) -> Transform { + Transform::Normal + } + /// Get the geometry relative to the output + fn geometry(&self, scale: Scale) -> Rectangle; + /// Get the damage since the provided commit relative to the element + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + if commit != Some(self.current_commit()) { + DamageSet::from_slice(&[Rectangle::from_size(self.geometry(scale).size)]) + } else { + DamageSet::default() + } + } + /// Get the opaque regions of the element relative to the element + fn opaque_regions(&self, _scale: Scale) -> OpaqueRegions { + OpaqueRegions::default() + } + /// Returns an alpha value the element should be drawn with regardless of any + /// already encoded alpha in it's underlying representation. + fn alpha(&self) -> f32 { + 1.0 + } + /// Returns the [`Kind`] for this element + fn kind(&self) -> Kind { + Kind::default() + } + /// Returns whether this elements is a "framebuffer effect". + /// + /// Returning `true` will cause implementations of `RenderElement::capture_framebuffer` + /// to be called *before* the accompanying `RenderElement::draw` call, *if* the contents behind + /// the element have changed. + /// + /// Additionally damage calculation will be altered to always include the whole area behind the + /// element, if a capture is queued up, to make sure the framebuffer contents are available for capture. + /// + /// Any damage reported by this element will also cause `capture_framebuffer` to be called. + fn is_framebuffer_effect(&self) -> bool { + false + } +} + +/// A single render element +pub trait RenderElement: Element { + /// Draw this element + /// + /// `cache` will only be `Some` if [`Element::is_framebuffer_effect`] returns `true`. + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error>; + + /// Get the underlying storage of this element, may be used to optimize rendering (eg. drm planes) + #[inline] + fn underlying_storage(&self, renderer: &mut R) -> Option> { + let _ = renderer; + None + } + + /// Notification, that the underlying framebuffer has changed allowing the element + /// to blit the contents of the `frame`. + /// + /// Will only be called if `Element::is_framebuffer_effect` returns `true`. + fn capture_framebuffer( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), R::Error> { + let _ = (frame, src, dst, cache); + unimplemented!("error: is_framebuffer_effect without capture_framebuffer implementation!"); + } +} + +/// Types that can be converted into [`RenderElement`]s +pub trait AsRenderElements +where + R: Renderer, +{ + /// Type of the render element + type RenderElement: RenderElement; + /// Returns render elements for a given position and scale + fn render_elements>( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec; +} + +impl Element for &E +where + E: Element, +{ + fn id(&self) -> &Id { + (*self).id() + } + + fn current_commit(&self) -> CommitCounter { + (*self).current_commit() + } + + fn location(&self, scale: Scale) -> Point { + (*self).location(scale) + } + + fn src(&self) -> Rectangle { + (*self).src() + } + + fn transform(&self) -> Transform { + (*self).transform() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + (*self).geometry(scale) + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + (*self).damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + (*self).opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + (*self).alpha() + } + + fn kind(&self) -> Kind { + (*self).kind() + } + + fn is_framebuffer_effect(&self) -> bool { + (*self).is_framebuffer_effect() + } +} + +impl RenderElement for &E +where + R: Renderer, + E: RenderElement + Element, +{ + #[inline] + fn underlying_storage(&self, renderer: &mut R) -> Option> { + (*self).underlying_storage(renderer) + } + + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + (*self).draw(frame, src, dst, damage, opaque_regions, cache) + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + (*self).capture_framebuffer(frame, src, dst, cache) + } +} + +/// [`Element`] wrapper namespacing the [`Id`] of the internal element. +/// +/// See [`Id::namespaced`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct NamespacedElement { + inner: E, + namespace: Id, +} + +impl NamespacedElement { + /// Create a new `NamespacedElement` wrapping `elem` and using + /// `namespace` to namespace the elements id. + pub fn new(elem: E, namespace: usize) -> Self { + let namespace = elem.id().namespaced(namespace); + NamespacedElement { + inner: elem, + namespace, + } + } + + /// Extract the inner element + pub fn into_inner(self) -> E { + self.inner + } +} + +impl AsRef for NamespacedElement { + fn as_ref(&self) -> &E { + &self.inner + } +} + +impl AsMut for NamespacedElement { + fn as_mut(&mut self) -> &mut E { + &mut self.inner + } +} + +impl Element for NamespacedElement { + fn id(&self) -> &Id { + &self.namespace + } + + fn current_commit(&self) -> CommitCounter { + self.inner.current_commit() + } + + fn src(&self) -> Rectangle { + self.inner.src() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.inner.geometry(scale) + } + + fn location(&self, scale: Scale) -> Point { + self.inner.location(scale) + } + + fn transform(&self) -> Transform { + self.inner.transform() + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + self.inner.damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + self.inner.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.inner.alpha() + } + + fn kind(&self) -> Kind { + self.inner.kind() + } + + fn is_framebuffer_effect(&self) -> bool { + self.inner.is_framebuffer_effect() + } +} + +impl> RenderElement for NamespacedElement { + fn draw( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), ::Error> { + self.inner.draw(frame, src, dst, damage, opaque_regions, cache) + } + + fn underlying_storage(&self, renderer: &mut R) -> Option> { + self.inner.underlying_storage(renderer) + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + self.inner.capture_framebuffer(frame, src, dst, cache) + } +} + +// Clippy: `large_enum_variant` is easily triggered here, as the `_GenericCatcher` is always zero-size. +// Thus we disable the lint. + +#[macro_export] +#[doc(hidden)] +macro_rules! render_elements_internal { + (@enum $(#[$attr:meta])* $vis:vis $name:ident; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident $($custom:ident)+; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$($custom),+> { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident $lt:lifetime; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$lt> { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident $lt:lifetime $($custom:ident)+; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$lt, $($custom),+> { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + + (@enum $(#[$attr:meta])* $vis:vis $name:ident<$renderer:ident>; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$renderer> + where + $renderer: $crate::backend::renderer::Renderer, + { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher((std::marker::PhantomData<$renderer>, std::convert::Infallible)), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident<$renderer:ident, $($custom:ident),+>; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$renderer, $($custom),+> + where + $renderer: $crate::backend::renderer::Renderer, + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer>, + )+ + { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher((std::marker::PhantomData<$renderer>, std::convert::Infallible)), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime, $renderer:ident>; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$lt, $renderer> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher((std::marker::PhantomData<$renderer>, std::convert::Infallible)), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime, $renderer:ident, $($custom:ident),+>; $($(#[$meta:meta])* $body:ident=$field:ty$( as <$other_renderer:ty>)?),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$lt, $renderer, $($custom),+> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer>, + )+ + { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher((std::marker::PhantomData<$renderer>, std::convert::Infallible)), + } + }; + (@call $name:ident; $($x:ident),*) => { + $crate::backend::renderer::element::Element::$name($($x),*) + }; + (@call $renderer:ty; $name:ident; $($x:ident),*) => { + $crate::backend::renderer::element::RenderElement::<$renderer>::$name($($x),*) + }; + (@call $renderer:ty as $other:ty; draw; $x:ident, $renderer_ref:ident, $frame:ident, $($tail:ident),*) => { + $crate::backend::renderer::element::RenderElement::<$other>::draw($x, $renderer_ref.as_mut(), $frame.as_mut(), $($tail),*).map_err(Into::into) + }; + (@call $renderer:ty as $other:ty; $name:ident; $($x:ident),*) => { + $crate::backend::renderer::element::RenderElement::<$other>::$name($($x),*) + }; + (@body $($(#[$meta:meta])* $body:ident=$field:ty),* $(,)?) => { + fn id(&self) -> &$crate::backend::renderer::element::Id { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call id; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn location(&self, scale: $crate::utils::Scale) -> $crate::utils::Point { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call location; x, scale) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn src(&self) -> $crate::utils::Rectangle { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call src; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn transform(&self) -> $crate::utils::Transform { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call transform; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn geometry(&self, scale: $crate::utils::Scale) -> $crate::utils::Rectangle { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call geometry; x, scale) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn current_commit(&self) -> $crate::backend::renderer::utils::CommitCounter { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call current_commit; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn damage_since(&self, scale: $crate::utils::Scale, commit: Option<$crate::backend::renderer::utils::CommitCounter>) -> $crate::backend::renderer::utils::DamageSet { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call damage_since; x, scale, commit) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn opaque_regions(&self, scale: $crate::utils::Scale) -> $crate::backend::renderer::utils::OpaqueRegions { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call opaque_regions; x, scale) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn alpha(&self) -> f32 { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call alpha; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn kind(&self) -> $crate::backend::renderer::element::Kind { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call kind; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn is_framebuffer_effect(&self) -> bool { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call is_framebuffer_effect; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + }; + (@draw <$renderer:ty>; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + fn draw( + &self, + frame: &mut <$renderer as $crate::backend::renderer::RendererSuper>::Frame<'_, '_>, + src: $crate::utils::Rectangle, + dst: $crate::utils::Rectangle, + damage: &[$crate::utils::Rectangle], + opaque_regions: &[$crate::utils::Rectangle], + cache: Option<&$crate::utils::user_data::UserDataMap>, + ) -> Result<(), <$renderer as $crate::backend::renderer::RendererSuper>::Error> + where + $( + $( + $renderer: std::convert::AsMut<$other_renderer>, + <$renderer as $crate::backend::renderer::RendererSuper>::Frame: std::convert::AsMut<<$other_renderer as $crate::backend::renderer::RendererSuper>::Frame>, + <$other_renderer as $crate::backend::renderer::RendererSuper>::Error: Into<<$renderer as $crate::backend::renderer::RendererSuper>::Error>, + )* + )* + { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call $renderer $(as $other_renderer)?; draw; x, frame, src, dst, damage, opaque_regions, cache) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + #[inline] + fn underlying_storage(&self, renderer: &mut $renderer) -> Option<$crate::backend::renderer::element::UnderlyingStorage<'_>> + { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call $renderer $(as $other_renderer)?; underlying_storage; x, renderer) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn capture_framebuffer( + &self, + frame: &mut <$renderer as $crate::backend::renderer::RendererSuper>::Frame<'_, '_>, + src: $crate::utils::Rectangle, + dst: $crate::utils::Rectangle, + cache: &$crate::utils::user_data::UserDataMap, + ) -> Result<(), <$renderer as $crate::backend::renderer::RendererSuper>::Error> + where + $( + $( + $renderer: std::convert::AsMut<$other_renderer>, + <$renderer as $crate::backend::renderer::RendererSuper>::Frame: std::convert::AsMut<<$other_renderer as $crate::backend::renderer::RendererSuper>::Frame>, + <$other_renderer as $crate::backend::renderer::RendererSuper>::Error: Into<<$renderer as $crate::backend::renderer::RendererSuper>::Error>, + )* + )* + { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call $renderer $(as $other_renderer)?; capture_framebuffer; x, frame, src, dst, cache) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + }; + (@draw $renderer:ty; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + fn draw( + &self, + frame: &mut <$renderer as $crate::backend::renderer::RendererSuper>::Frame<'_, '_>, + src: $crate::utils::Rectangle, + dst: $crate::utils::Rectangle, + damage: &[$crate::utils::Rectangle], + opaque_regions: &[$crate::utils::Rectangle], + cache: Option<&$crate::utils::user_data::UserDataMap>, + ) -> Result<(), <$renderer as $crate::backend::renderer::RendererSuper>::Error> + { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call $renderer $(as $other_renderer)?; draw; x, frame, src, dst, damage, opaque_regions, cache) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + #[inline] + fn underlying_storage(&self, renderer: &mut $renderer) -> Option<$crate::backend::renderer::element::UnderlyingStorage<'_>> + { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call $renderer $(as $other_renderer)?; underlying_storage; x, renderer) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn capture_framebuffer( + &self, + frame: &mut <$renderer as $crate::backend::renderer::RendererSuper>::Frame<'_, '_>, + src: $crate::utils::Rectangle, + dst: $crate::utils::Rectangle, + cache: &$crate::utils::user_data::UserDataMap, + ) -> Result<(), <$renderer as $crate::backend::renderer::RendererSuper>::Error> + { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::render_elements_internal!(@call $renderer $(as $other_renderer)?; capture_framebuffer; x, frame, src, dst, cache) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + }; + // Generic renderer + (@impl $name:ident<$renderer:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + impl<$renderer> $crate::backend::renderer::element::Element for $name<$renderer> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$renderer> $crate::backend::renderer::element::RenderElement<$renderer> for $name<$renderer> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@draw <$renderer>; $($tail)*); + } + }; + (@impl $name:ident<$lt:lifetime, $renderer:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + impl<$lt, $renderer> $crate::backend::renderer::element::Element for $name<$lt, $renderer> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$lt, $renderer> $crate::backend::renderer::element::RenderElement<$renderer> for $name<$lt, $renderer> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@draw <$renderer>; $($tail)*); + } + }; + (@impl $name:ident<$renderer:ident, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + impl<$renderer, $($custom),+> $crate::backend::renderer::element::Element for $name<$renderer, $($custom),+> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$renderer, $($custom),+> $crate::backend::renderer::element::RenderElement<$renderer> for $name<$renderer, $($custom),+> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@draw <$renderer>; $($tail)*); + } + }; + (@impl $name:ident<$lt:lifetime, $renderer:ident, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + impl<$lt, $renderer, $($custom),+> $crate::backend::renderer::element::Element for $name<$lt, $renderer, $($custom),+> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$lt, $renderer, $($custom),+> $crate::backend::renderer::element::RenderElement<$renderer> for $name<$lt, $renderer, $($custom),+> + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@draw <$renderer>; $($tail)*); + } + }; + (@impl $name:ident; $renderer:ident; $($tail:tt)*) => { + impl $crate::backend::renderer::element::Element for $name + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$renderer> $crate::backend::renderer::element::RenderElement<$renderer> for $name + where + $renderer: $crate::backend::renderer::Renderer, + <$renderer as $crate::backend::renderer::RendererSuper>::TextureId: 'static, + { + $crate::render_elements_internal!(@draw <$renderer>; $($tail)*); + } + }; + + // Specific renderer + (@impl $name:ident<=$renderer:ty>; $($tail:tt)*) => { + impl $crate::backend::renderer::element::Element for $name + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl $crate::backend::renderer::element::RenderElement<$renderer> for $name + { + $crate::render_elements_internal!(@draw $renderer; $($tail)*); + } + }; + (@impl $name:ident<=$renderer:ty, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + impl<$($custom),+> $crate::backend::renderer::element::Element for $name<$($custom),+> + where + $( + $custom: $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$($custom),+> $crate::backend::renderer::element::RenderElement<$renderer> for $name<$($custom),+> + where + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@draw $renderer; $($tail)*); + } + }; + + (@impl $name:ident<=$renderer:ty, $lt:lifetime>; $($tail:tt)*) => { + impl<$lt> $crate::backend::renderer::element::Element for $name<$lt> + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$lt> $crate::backend::renderer::element::RenderElement<$renderer> for $name<$lt> + { + $crate::render_elements_internal!(@draw $renderer; $($tail)*); + } + }; + + (@impl $name:ident<=$renderer:ty, $lt:lifetime, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + impl<$lt, $($custom),+> $crate::backend::renderer::element::Element for $name<$lt, $($custom),+> + where + $( + $custom: $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@body $($tail)*); + } + impl<$lt, $($custom),+> $crate::backend::renderer::element::RenderElement<$renderer> for $name<$lt, $($custom),+> + where + $( + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + )+ + $($($target: $bound $(+ $additional_bound)*),+)? + { + $crate::render_elements_internal!(@draw $renderer; $($tail)*); + } + }; + + + (@from $name:ident<$renderer:ident>; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + $( + $( + #[$meta] + )* + impl<$renderer> From<$field> for $name<$renderer> + where + $renderer: $crate::backend::renderer::Renderer, + $( + $($renderer: std::convert::AsMut<$other_renderer>,)? + )* + { + #[inline] + fn from(field: $field) -> $name<$renderer> { + $name::$body(field) + } + } + )* + }; + (@from $name:ident<$renderer:ident, $custom:ident>; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + $( + $( + #[$meta] + )* + impl<$renderer, $custom> From<$field> for $name<$renderer, $custom> + where + $renderer: $crate::backend::renderer::Renderer, + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + $( + $($renderer: std::convert::AsMut<$other_renderer>,)? + )* + { + #[inline] + fn from(field: $field) -> $name<$renderer, $custom> { + $name::$body(field) + } + } + )* + }; + (@from $name:ident<$lt:lifetime, $renderer:ident>; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + $( + $( + #[$meta] + )* + impl<$lt, $renderer> From<$field> for $name<$lt, $renderer> + where + $renderer: $crate::backend::renderer::Renderer, + $( + $($renderer: std::convert::AsMut<$other_renderer>,)? + )* + { + #[inline] + fn from(field: $field) -> $name<$lt, $renderer> { + $name::$body(field) + } + } + )* + }; + (@from $name:ident<$lt:lifetime, $renderer:ident, $custom:ident>; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + $( + $( + #[$meta] + )* + impl<$lt, $renderer, $custom> From<$field> for $name<$lt, $renderer, $custom> + where + $renderer: $crate::backend::renderer::Renderer, + $custom: $crate::backend::renderer::element::RenderElement<$renderer> + $crate::backend::renderer::element::Element, + $( + $($renderer: std::convert::AsMut<$other_renderer>,)? + )* + { + #[inline] + fn from(field: $field) -> $name<$lt, $renderer, $custom> { + $name::$body(field) + } + } + )* + }; + (@from $name:ident; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + $( + $( + #[$meta] + )* + impl From<$field> for $name { + #[inline] + fn from(field: $field) -> $name { + $name::$body(field) + } + } + )* + }; + (@from $name:ident<$lt:lifetime>; $($(#[$meta:meta])* $body:ident=$field:ty $(as <$other_renderer:ty>)?),* $(,)?) => { + $( + $( + #[$meta] + )* + impl<$lt> From<$field> for $name<$lt> { + #[inline] + fn from(field: $field) -> $name<$lt> { + $name::$body(field) + } + } + )* + }; +} + +/// Aggregate multiple types implementing [`RenderElement`] into a single enum type +/// +/// ``` +/// # use smithay::{ +/// # backend::{ +/// # allocator::Fourcc, +/// # renderer::{ +/// # element::{Element, Id, RenderElement}, +/// # utils::CommitCounter, +/// # Renderer, +/// # }, +/// # }, +/// # utils::{ +/// # user_data::UserDataMap, +/// # Buffer, Point, Physical, Rectangle, Scale, Transform, +/// # }, +/// # }; +/// # +/// # struct MyRenderElement1; +/// # struct MyRenderElement2; +/// # +/// # impl Element for MyRenderElement1 { +/// # fn id(&self) -> &Id { +/// # unimplemented!() +/// # } +/// # +/// # fn current_commit(&self) -> CommitCounter { +/// # unimplemented!() +/// # } +/// # +/// # fn geometry(&self, _scale: Scale) -> Rectangle { +/// # unimplemented!() +/// # } +/// # +/// # fn src(&self) -> Rectangle { +/// # unimplemented!() +/// # } +/// # } +/// # +/// # impl RenderElement for MyRenderElement1 { +/// # fn draw( +/// # &self, +/// # _frame: &mut R::Frame<'_, '_>, +/// # _src: Rectangle, +/// # _dst: Rectangle, +/// # _damage: &[Rectangle], +/// # _opaque_regions: &[Rectangle], +/// # _cache: Option<&UserDataMap>, +/// # ) -> Result<(), R::Error> { +/// # unimplemented!() +/// # } +/// # } +/// # +/// # impl Element for MyRenderElement2 { +/// # fn id(&self) -> &Id { +/// # unimplemented!() +/// # } +/// # +/// # fn current_commit(&self) -> CommitCounter { +/// # unimplemented!() +/// # } +/// # +/// # fn geometry(&self, _scale: Scale) -> Rectangle { +/// # unimplemented!() +/// # } +/// # +/// # fn src(&self) -> Rectangle { +/// # unimplemented!() +/// # } +/// # } +/// # +/// # impl RenderElement for MyRenderElement2 { +/// # fn draw( +/// # &self, +/// # _frame: &mut R::Frame<'_, '_>, +/// # _src: Rectangle, +/// # _dst: Rectangle, +/// # _damage: &[Rectangle], +/// # _opaque_regions: &[Rectangle], +/// # _cache: Option<&UserDataMap>, +/// # ) -> Result<(), R::Error> { +/// # unimplemented!() +/// # } +/// # } +/// use smithay::backend::renderer::element::render_elements; +/// +/// render_elements! { +/// MyRenderElements; +/// First=MyRenderElement1, +/// Second=MyRenderElement2, +/// } +/// ``` +/// +/// If the [`RenderElement`] has special requirements on the [`Renderer`] you can +/// express them with a syntax similar to HRTBs. +/// +/// For example the [`MemoryRenderBufferRenderElement`](crate::backend::renderer::element::memory::MemoryRenderBufferRenderElement) requires +/// the [`Renderer`] to implement the [`ImportMem`](crate::backend::renderer::ImportMem) trait. +/// +/// ``` +/// use smithay::backend::renderer::{ +/// element::{memory::MemoryRenderBufferRenderElement, render_elements}, +/// ImportMem, +/// }; +/// +/// render_elements! { +/// MyRenderElements where R: ImportMem; +/// Memory=MemoryRenderBufferRenderElement, +/// } +/// ``` +/// +/// In case you want to use a reference or an element with an explicit lifetime the macro +/// additionally takes a lifetime on the defined enum. +/// +/// ``` +/// use smithay::backend::renderer::{ +/// element::{memory::MemoryRenderBufferRenderElement, render_elements}, +/// ImportMem, +/// }; +/// +/// render_elements! { +/// MyRenderElements<'a, R> where R: ImportMem; +/// Memory=&'a MemoryRenderBufferRenderElement, +/// } +/// ``` +/// +/// Additionally the macro can be used to define generic enums +/// +/// ``` +/// use smithay::backend::renderer::{ +/// element::{memory::MemoryRenderBufferRenderElement, render_elements}, +/// ImportMem, +/// }; +/// +/// render_elements! { +/// MyRenderElements<'a, R, A, B> where R: ImportMem; +/// Memory=&'a MemoryRenderBufferRenderElement, +/// Owned=A, +/// Borrowed=&'a B, +/// } +/// ``` +/// +/// If your elements require a specific [`Renderer`] instead of being +/// generic over it you can specify the type like in the following example. +/// +/// ``` +/// # use smithay::{ +/// # backend::{ +/// # allocator::Fourcc, +/// # renderer::{Color32F, DebugFlags, Frame, Renderer, Texture, TextureFilter, sync::SyncPoint, gles::{GlesRenderer, GlesTexture}}, +/// # }, +/// # utils::{Buffer, Physical, Rectangle, Size, Transform}, +/// # }; +/// use smithay::backend::renderer::element::{render_elements, texture::TextureRenderElement}; +/// +/// render_elements! { +/// MyRenderElements<=GlesRenderer>; +/// Texture=TextureRenderElement, +/// } +/// ``` +#[macro_export] +macro_rules! render_elements { + ($(#[$attr:meta])* $vis:vis $name:ident<=$lt:lifetime, $renderer:ty, $custom:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name $lt $custom; $($tail)*); + $crate::render_elements_internal!(@impl $name<=$renderer, $lt, $custom> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + $crate::render_elements_internal!(@from $name<$lt, $custom>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<=$lt:lifetime, $renderer:ty, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name $lt $($custom)+; $($tail)*); + $crate::render_elements_internal!(@impl $name<=$renderer, $lt, $($custom)+> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<=$lt:lifetime, $renderer:ty>; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name $lt; $($tail)*); + $crate::render_elements_internal!(@impl $name<=$renderer, $lt>; $($tail)*); + $crate::render_elements_internal!(@from $name<$lt>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<=$renderer:ty, $custom:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name $custom; $($tail)*); + $crate::render_elements_internal!(@impl $name<=$renderer, $custom> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + $crate::render_elements_internal!(@from $name<$custom>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<=$renderer:ty, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name $custom1 $custom2; $($tail)*); + $crate::render_elements_internal!(@impl $name<=$renderer, $($custom),+> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<=$renderer:ty>; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name; $($tail)*); + $crate::render_elements_internal!(@impl $name<=$renderer>; $($tail)*); + $crate::render_elements_internal!(@from $name; $($tail)*); + }; + + ($(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime, $renderer:ident, $custom:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name<$lt, $renderer, $custom>; $($tail)*); + $crate::render_elements_internal!(@impl $name<$lt, $renderer, $custom> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + $crate::render_elements_internal!(@from $name<$lt, $renderer, $custom>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime, $renderer:ident, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name<$lt, $renderer, $($custom),+>; $($tail)*); + $crate::render_elements_internal!(@impl $name<$lt, $renderer, $($custom),+> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime, $renderer:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name<$lt, $renderer>; $($tail)*); + $crate::render_elements_internal!(@impl $name<$lt, $renderer> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + $crate::render_elements_internal!(@from $name<$lt, $renderer>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<$renderer:ident, $custom:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name<$renderer, $custom>; $($tail)*); + $crate::render_elements_internal!(@impl $name<$renderer, $custom> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + $crate::render_elements_internal!(@from $name<$renderer, $custom>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<$renderer:ident, $($custom:ident),+> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name<$renderer, $($custom),+>; $($tail)*); + $crate::render_elements_internal!(@impl $name<$renderer, $($custom),+> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<$renderer:ident> $(where $($target:ty: $bound:tt $(+ $additional_bound:tt)*),+)?; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name<$renderer>; $($tail)*); + $crate::render_elements_internal!(@impl $name<$renderer> $(where $($target: $bound $(+ $additional_bound)*),+)?; $($tail)*); + $crate::render_elements_internal!(@from $name<$renderer>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident; $($tail:tt)*) => { + $crate::render_elements_internal!(@enum $(#[$attr])* $vis $name; $($tail)*); + $crate::render_elements_internal!(@impl $name; R; $($tail)*); + $crate::render_elements_internal!(@from $name; $($tail)*); + }; +} + +pub use render_elements; + +/// New-type wrapper for wrapping owned elements +/// in render_elements! +#[derive(Debug)] +pub struct Wrap(C); + +impl From for Wrap { + fn from(from: C) -> Self { + Self(from) + } +} + +impl Element for Wrap +where + C: Element, +{ + fn id(&self) -> &Id { + self.0.id() + } + + fn current_commit(&self) -> CommitCounter { + self.0.current_commit() + } + + fn location(&self, scale: Scale) -> Point { + self.0.location(scale) + } + + fn src(&self) -> Rectangle { + self.0.src() + } + + fn transform(&self) -> Transform { + self.0.transform() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.0.geometry(scale) + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + self.0.damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + self.0.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.0.alpha() + } + + fn kind(&self) -> Kind { + self.0.kind() + } + + fn is_framebuffer_effect(&self) -> bool { + self.0.is_framebuffer_effect() + } +} + +impl RenderElement for Wrap +where + R: Renderer, + C: RenderElement, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + self.0.draw(frame, src, dst, damage, opaque_regions, cache) + } + + #[inline] + fn underlying_storage(&self, renderer: &mut R) -> Option> { + self.0.underlying_storage(renderer) + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + self.0.capture_framebuffer(frame, src, dst, cache) + } +} + +#[cfg(all(test, feature = "renderer_gl"))] +#[allow(dead_code)] +mod tests; diff --git a/vendor/smithay/src/backend/renderer/element/solid.rs b/vendor/smithay/src/backend/renderer/element/solid.rs new file mode 100644 index 0000000..270464a --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/solid.rs @@ -0,0 +1,255 @@ +//! Element to render a solid color +//! +//! # How to use it +//! +//! ```no_run +//! # use smithay::{ +//! # backend::{ +//! # allocator::Fourcc, +//! # renderer::{Color32F, DebugFlags, Frame, ImportMem, Renderer, Texture, TextureFilter, sync::SyncPoint, test::{DummyRenderer, DummyFramebuffer}}, +//! # }, +//! # utils::{Buffer, Physical, Rectangle, Transform}, +//! # }; +//! use smithay::{ +//! backend::renderer::{ +//! damage::OutputDamageTracker, +//! element::{ +//! Kind, +//! solid::{SolidColorBuffer, SolidColorRenderElement}, +//! }, +//! }, +//! utils::{Point, Size}, +//! }; +//! +//! const WIDTH: i32 = 10; +//! const HEIGHT: i32 = 10; +//! const COLOR: [f32; 4] = [1f32, 0.9f32, 0.78f32, 1f32]; +//! +//! // Initialize the solid color buffer +//! let buffer = SolidColorBuffer::new((WIDTH, HEIGHT), COLOR); +//! +//! // Initialize a static damage tracked renderer +//! let mut damage_tracker = OutputDamageTracker::new((800, 600), 1.0, Transform::Normal); +//! # let mut renderer = DummyRenderer::default(); +//! # let mut framebuffer = DummyFramebuffer; +//! +//! loop { +//! // Create a render element from the buffer +//! let location = Point::from((100, 100)); +//! let render_element = SolidColorRenderElement::from_buffer(&buffer, location, 1f64, 1.0, Kind::Unspecified); +//! +//! // Render the element(s) +//! damage_tracker +//! .render_output(&mut renderer, &mut framebuffer, 0, &[&render_element], [0.8, 0.8, 0.9, 1.0]) +//! .expect("failed to render output"); +//! } +//! ``` +use crate::{ + backend::renderer::{ + Color32F, Frame, Renderer, + utils::{CommitCounter, OpaqueRegions}, + }, + utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform, user_data::UserDataMap}, +}; + +use super::{AsRenderElements, Element, Id, Kind, RenderElement}; + +/// A single color buffer +#[derive(Debug, Clone)] +pub struct SolidColorBuffer { + id: Id, + size: Size, + commit: CommitCounter, + color: Color32F, +} + +impl Default for SolidColorBuffer { + fn default() -> Self { + Self { + id: Id::new(), + size: Default::default(), + commit: Default::default(), + color: Default::default(), + } + } +} + +impl SolidColorBuffer { + /// Initialize a new solid color buffer with the specified size and color + pub fn new(size: impl Into>, color: impl Into) -> Self { + SolidColorBuffer { + id: Id::new(), + color: color.into(), + commit: CommitCounter::default(), + size: size.into(), + } + } + + /// Set the new size of this solid color buffer + /// + /// Note: If the size matches the current size this will do nothing + pub fn resize(&mut self, size: impl Into>) { + let size = size.into(); + if size != self.size { + self.size = size; + self.commit.increment(); + } + } + + /// Set a new color on this solid color buffer + /// + /// Note: If the color matches the current color this will do nothing + pub fn set_color(&mut self, color: impl Into) { + let color = color.into(); + if color != self.color { + self.color = color; + self.commit.increment(); + } + } + + /// Update the size and color of this solid color buffer + /// + /// Note: If the size and color match the current size and color this will do nothing + pub fn update(&mut self, size: impl Into>, color: impl Into) { + let color = color.into(); + let size = size.into(); + if size != self.size || color != self.color { + self.size = size; + self.color = color; + self.commit.increment(); + } + } + + /// Get the current color of this buffer + pub fn color(&self) -> Color32F { + self.color + } +} + +/// [`Element`] to render a solid color +#[derive(Debug, Clone)] +pub struct SolidColorRenderElement { + id: Id, + geometry: Rectangle, + src: Rectangle, + opaque_regions: Vec>, + commit: CommitCounter, + color: Color32F, + kind: Kind, +} + +impl SolidColorRenderElement { + /// Create a render element from a [`SolidColorBuffer`] + pub fn from_buffer( + buffer: &SolidColorBuffer, + location: impl Into>, + scale: impl Into>, + alpha: f32, + kind: Kind, + ) -> Self { + let geo = Rectangle::new(location.into(), buffer.size.to_physical_precise_round(scale)); + let color = buffer.color * alpha; + Self::new(buffer.id.clone(), geo, buffer.commit, color, kind) + } + + /// Create a new solid color render element with the specified geometry and color + pub fn new( + id: impl Into, + geometry: Rectangle, + commit: impl Into, + color: impl Into, + kind: Kind, + ) -> Self { + let color = color.into(); + let src = Rectangle::from_size(geometry.size) + .to_f64() + .to_logical(1f64) + .to_buffer(1f64, Transform::Normal, &geometry.size.to_f64().to_logical(1f64)); + let opaque_regions = if color.is_opaque() { + vec![Rectangle::from_size(geometry.size)] + } else { + vec![] + }; + SolidColorRenderElement { + id: id.into(), + geometry, + src, + opaque_regions, + commit: commit.into(), + color, + kind, + } + } + + /// Get the current color of this element + pub fn color(&self) -> Color32F { + self.color + } +} + +impl Element for SolidColorRenderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.commit + } + + fn src(&self) -> Rectangle { + self.src + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + self.geometry + } + + fn opaque_regions(&self, _scale: Scale) -> OpaqueRegions { + OpaqueRegions::from_slice(&self.opaque_regions) + } + + fn alpha(&self) -> f32 { + self.color.a() + } + + fn kind(&self) -> Kind { + self.kind + } +} + +impl RenderElement for SolidColorRenderElement { + #[profiling::function] + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + frame.draw_solid(dst, damage, self.color) + } + + #[inline] + fn underlying_storage(&self, _renderer: &mut R) -> Option> { + None + } +} + +impl AsRenderElements for SolidColorBuffer +where + R: Renderer, +{ + type RenderElement = SolidColorRenderElement; + + fn render_elements>( + &self, + _renderer: &mut R, + location: crate::utils::Point, + scale: crate::utils::Scale, + alpha: f32, + ) -> Vec { + vec![SolidColorRenderElement::from_buffer(self, location, scale, alpha, Kind::Unspecified).into()] + } +} diff --git a/vendor/smithay/src/backend/renderer/element/surface.rs b/vendor/smithay/src/backend/renderer/element/surface.rs new file mode 100644 index 0000000..1d7bffd --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/surface.rs @@ -0,0 +1,474 @@ +//! Element to render a wayland surface +//! +//! # Why use this implementation +//! +//! The [`WaylandSurfaceRenderElement`] provides an easy way to +//! integrate a [`WlSurface`](wayland_server::protocol::wl_surface::WlSurface) in the smithay rendering pipeline +//! +//! # How to use it +//! +//! [`WaylandSurfaceRenderElement::from_surface`] allows you to obtain a [`WaylandSurfaceRenderElement`] for a single [`WlSurface`](wayland_server::protocol::wl_surface::WlSurface). +//! To retrieve [`WaylandSurfaceRenderElement`]s for a whole surface tree you can use [`render_elements_from_surface_tree`]. +//! +//! ```no_run +//! # #[cfg(all( +//! # feature = "wayland_frontend", +//! # feature = "backend_egl", +//! # feature = "use_system_lib" +//! # ))] +//! # use smithay::backend::{ +//! # egl::{self, display::EGLBufferReader}, +//! # renderer::ImportEgl, +//! # }; +//! # use smithay::{ +//! # backend::allocator::{Fourcc, dmabuf::Dmabuf}, +//! # backend::renderer::{ +//! # Color32F, DebugFlags, Frame, ImportDma, ImportDmaWl, ImportMem, ImportMemWl, Renderer, Texture, +//! # TextureFilter, sync::SyncPoint, test::{DummyRenderer, DummyFramebuffer}, +//! # }, +//! # utils::{Buffer, Physical}, +//! # wayland::compositor::SurfaceData, +//! # }; +//! use smithay::{ +//! backend::renderer::{ +//! damage::OutputDamageTracker, +//! element::{ +//! Kind, +//! surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement}, +//! }, +//! }, +//! utils::{Point, Rectangle, Size, Transform}, +//! }; +//! # use wayland_server::{backend::ObjectId, protocol::wl_surface::WlSurface, Resource}; +//! # let display = wayland_server::Display::<()>::new().unwrap(); +//! # let dh = display.handle(); +//! # let surface = WlSurface::from_id(&dh, ObjectId::null()).unwrap(); +//! +//! // Initialize a static damage tracked renderer +//! let mut damage_tracker = OutputDamageTracker::new((800, 600), 1.0, Transform::Normal); +//! # let mut renderer = DummyRenderer::default(); +//! # let mut framebuffer = DummyFramebuffer; +//! +//! loop { +//! // Create the render elements from the surface +//! let location = Point::from((100, 100)); +//! let render_elements: Vec> = +//! render_elements_from_surface_tree(&mut renderer, &surface, location, 1.0, 1.0, Kind::Unspecified); +//! +//! // Render the element(s) +//! damage_tracker +//! .render_output(&mut renderer, &mut framebuffer, 0, &*render_elements, [0.8, 0.8, 0.9, 1.0]) +//! .expect("failed to render output"); +//! } +//! ``` + +use std::fmt; + +use tracing::{instrument, warn}; +use wayland_server::protocol::wl_surface; + +use crate::{ + backend::renderer::{ + Color32F, Frame, ImportAll, Renderer, Texture, + utils::{ + Buffer, DamageSet, DamageSnapshot, OpaqueRegions, RendererSurfaceState, + RendererSurfaceStateUserData, SurfaceView, + }, + }, + utils::{ + Buffer as BufferCoords, Logical, Physical, Point, Rectangle, Scale, Size, Transform, + user_data::UserDataMap, + }, + wayland::{ + alpha_modifier::AlphaModifierSurfaceCachedState, + compositor::{self, SurfaceData, TraversalAction}, + }, +}; + +use super::{CommitCounter, Element, Id, Kind, RenderElement, UnderlyingStorage}; + +/// Ways to evaluation the [`Kind`] of a surface. +pub enum KindEvaluation { + /// Static evaluation, which will always return the same [`Kind`] + Static(Kind), + /// Dynamic evaluation, which will return a [`Kind`] based on the [`SurfaceData`] + Dynamic(fn(&SurfaceData) -> Kind), + /// Dynamic evaluation, which will return a [`Kind`] based on the [`SurfaceData`] and potentially additional captured context. + Closure(Box Kind>), +} + +impl fmt::Debug for KindEvaluation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + KindEvaluation::Static(kind) => kind.fmt(f), + _ => f.write_str("Kind::"), + } + } +} + +impl From for KindEvaluation { + fn from(value: Kind) -> Self { + KindEvaluation::Static(value) + } +} + +impl Kind + 'static> From for KindEvaluation { + fn from(value: F) -> Self { + KindEvaluation::Closure(Box::new(value)) + } +} + +// TODO: specialization +/* +impl From Kind> for KindEvaluation { + fn from(value: fn(&SurfaceData) -> Kind) -> Self { + KindEvaluation::Dynamic(value) + } +} +*/ + +impl KindEvaluation { + /// Create a `KindEvaluation` from a pure function. + /// + /// Using this instead of the `From`-implementation skips boxing + /// and thus can provide better performance as it is not relying on dynamic dispatching. + pub fn from_fn(func: fn(&SurfaceData) -> Kind) -> Self { + KindEvaluation::Dynamic(func) + } + + /// Evaluates the `Kind` for a given `SurfaceData` struct. + pub fn eval(&self, data: &SurfaceData) -> Kind { + match self { + KindEvaluation::Static(res) => *res, + KindEvaluation::Dynamic(func) => func(data), + KindEvaluation::Closure(func) => func(data), + } + } +} + +/// Retrieve the [`WaylandSurfaceRenderElement`]s for a surface tree +#[instrument(level = "trace", skip(renderer, location, scale, kind))] +#[profiling::function] +pub fn render_elements_from_surface_tree( + renderer: &mut R, + surface: &wl_surface::WlSurface, + location: impl Into>, + scale: impl Into>, + alpha: f32, + kind: impl Into, +) -> Vec +where + R: Renderer + ImportAll, + R::TextureId: Clone + 'static, + E: From>, +{ + let location = location.into().to_f64(); + let scale = scale.into(); + let kind = kind.into(); + let mut surfaces: Vec = Vec::new(); + + compositor::with_surface_tree_downward( + surface, + location, + |_, states, location| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(data) = data { + if let Some(view) = data.lock().unwrap().view() { + location += view.offset.to_f64().to_physical(scale); + TraversalAction::DoChildren(location) + } else { + TraversalAction::SkipChildren + } + } else { + TraversalAction::SkipChildren + } + }, + |surface, states, location| { + let mut location = *location; + let kind = kind.eval(states); + let data = states.data_map.get::(); + + if let Some(data) = data { + let has_view = if let Some(view) = data.lock().unwrap().view() { + location += view.offset.to_f64().to_physical(scale); + true + } else { + false + }; + + if has_view { + match WaylandSurfaceRenderElement::from_surface( + renderer, surface, states, location, alpha, kind, + ) { + Ok(Some(surface)) => surfaces.push(surface.into()), + Ok(None) => {} // surface is not mapped + Err(err) => { + warn!("Failed to import surface: {}", err); + } + }; + } + } + }, + |_, _, _| true, + ); + + surfaces +} + +/// Texture used for the [`WaylandSurfaceRenderElement`] +#[derive(Debug)] +pub enum WaylandSurfaceTexture { + /// A renderer texture + Texture(R::TextureId), + /// A solid color + SolidColor(Color32F), +} + +/// A single surface render element +pub struct WaylandSurfaceRenderElement { + id: Id, + location: Point, + alpha: f32, + kind: Kind, + + view: SurfaceView, + buffer: Buffer, + buffer_scale: i32, + buffer_transform: Transform, + buffer_dimensions: Size, + damage: DamageSnapshot, + opaque_regions: OpaqueRegions, + texture: WaylandSurfaceTexture, +} + +impl fmt::Debug for WaylandSurfaceRenderElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WaylandSurfaceRenderElement") + .field("id", &self.id) + .field("location", &self.location) + .finish() + } +} + +impl WaylandSurfaceRenderElement { + /// Create a render element from a surface + #[profiling::function] + pub fn from_surface( + renderer: &mut R, + surface: &wl_surface::WlSurface, + states: &SurfaceData, + location: Point, + alpha: f32, + kind: Kind, + ) -> Result, R::Error> + where + R::TextureId: Clone + 'static, + { + let id = Id::from_wayland_resource(surface); + crate::backend::renderer::utils::import_surface(renderer, states)?; + + let mut alpha_modifier_state = states.cached_state.get::(); + let alpha_multiplier = alpha_modifier_state.current().multiplier_f32().unwrap_or(1.0); + + let Some(data_ref) = states.data_map.get::() else { + return Ok(None); + }; + Ok(Self::from_state( + renderer, + id, + location, + alpha * alpha_multiplier, + kind, + &data_ref.lock().unwrap(), + )) + } + + fn from_state( + renderer: &mut R, + id: Id, + location: Point, + alpha: f32, + kind: Kind, + data: &RendererSurfaceState, + ) -> Option + where + R::TextureId: Clone + 'static, + { + let buffer = data.buffer()?.clone(); + + let texture = if let Ok(spb) = crate::wayland::single_pixel_buffer::get_single_pixel_buffer(&buffer) { + WaylandSurfaceTexture::SolidColor(Color32F::from(spb.rgba32f())) + } else { + WaylandSurfaceTexture::Texture(data.texture(renderer.context_id())?.clone()) + }; + + Some(Self { + id, + location, + alpha, + kind, + view: data.view()?, + buffer, + buffer_scale: data.buffer_scale(), + buffer_transform: data.buffer_transform(), + buffer_dimensions: data.buffer_dimensions?, + damage: data.damage.snapshot(), + opaque_regions: data + .opaque_regions() + .map(OpaqueRegions::from_slice) + .unwrap_or_default(), + texture, + }) + } + + fn size(&self, scale: impl Into>) -> Size { + ((self.view.dst.to_f64().to_physical(scale).to_point() + self.location).to_i32_round() + - self.location.to_i32_round()) + .to_size() + } + + /// Get the buffer dimensions in logical coordinates + pub fn buffer_size(&self) -> Size { + self.buffer_dimensions + .to_logical(self.buffer_scale, self.buffer_transform) + } + + /// Get the view into the surface + pub fn view(&self) -> SurfaceView { + self.view + } + + /// Get the buffer texture + pub fn texture(&self) -> &WaylandSurfaceTexture { + &self.texture + } + + /// Get the buffer + pub fn buffer(&self) -> &Buffer { + &self.buffer + } +} + +impl Element for WaylandSurfaceRenderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.damage.current_commit() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + Rectangle::new(self.location.to_i32_round(), self.size(scale)) + } + + fn src(&self) -> Rectangle { + self.view.src.to_buffer( + self.buffer_scale as f64, + self.buffer_transform, + &self.buffer_size().to_f64(), + ) + } + + fn transform(&self) -> Transform { + self.buffer_transform + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + let dst_size = self.size(scale); + self.damage + .damage_since(commit) + .unwrap_or_else(|| DamageSet::from_slice(&[Rectangle::from_size(self.buffer_dimensions)])) + .iter() + .filter_map(|rect| { + rect.to_f64() + // first bring the damage into logical space + // Note: We use f64 for this as the damage could + // be not dividable by the buffer scale without + // a rest + .to_logical( + self.buffer_scale as f64, + self.buffer_transform, + &self.buffer_dimensions.to_f64(), + ) + // then crop by the surface view (viewporter for example could define a src rect) + .intersection(self.view.src) + // move and scale the cropped rect (viewporter could define a dst size) + .map(|rect| self.view.rect_to_global(rect).to_i32_up::()) + // now bring the damage to physical space + .map(|rect| { + // We calculate the scale between to rounded + // surface size and the scaled surface size + // and use it to scale the damage to the rounded + // surface size by multiplying the output scale + // with the result. + let surface_scale = dst_size.to_f64() / self.view.dst.to_f64().to_physical(scale); + rect.to_physical_precise_up(surface_scale * scale) + }) + }) + .collect::>() + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + if self.alpha < 1.0 { + return OpaqueRegions::default(); + } + + self.opaque_regions + .iter() + .map(|r| { + let loc = r.loc.to_physical_precise_round(scale); + let size = ((r.size.to_f64().to_physical(scale).to_point() + self.location).to_i32_round() + - self.location.to_i32_round()) + .to_size(); + Rectangle::new(loc, size) + }) + .collect::>() + } + + fn alpha(&self) -> f32 { + self.alpha + } + + fn kind(&self) -> Kind { + self.kind + } +} + +impl RenderElement for WaylandSurfaceRenderElement +where + R: Renderer + ImportAll, + R::TextureId: Texture + 'static, +{ + #[inline] + fn underlying_storage(&self, _renderer: &mut R) -> Option> { + Some(UnderlyingStorage::Wayland(&self.buffer)) + } + + #[instrument(level = "trace", skip(frame))] + #[profiling::function] + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + match self.texture { + WaylandSurfaceTexture::Texture(ref texture) => frame.render_texture_from_to( + texture, + src, + dst, + damage, + opaque_regions, + self.buffer_transform, + self.alpha, + ), + WaylandSurfaceTexture::SolidColor(color) => frame.draw_solid(dst, damage, color * self.alpha), + } + } +} diff --git a/vendor/smithay/src/backend/renderer/element/tests.rs b/vendor/smithay/src/backend/renderer/element/tests.rs new file mode 100644 index 0000000..e50c4b9 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/tests.rs @@ -0,0 +1,291 @@ +use std::marker::PhantomData; + +use crate::{ + backend::renderer::{ImportDma, ImportMem, Renderer, Texture, gles::GlesRenderer}, + utils::{Buffer, Physical, Point, Rectangle, Scale, user_data::UserDataMap}, +}; + +use super::{CommitCounter, Element, Id, RenderElement, Wrap}; + +render_elements! { + ImportMemTest where R: ImportMem; + Memory=ImportMemRenderElement, +} + +render_elements! { + ImportMemTest2<=GlesRenderer>; + Memory=ImportMemRenderElement, +} + +render_elements! { + ImportMemTest3<'a, R, T> where R: ImportMem, T: Texture; + Memory=ImportMemRenderElement, + Custom=&'a T, +} + +render_elements! { + ImportMemTest4 where R: ImportMem + ImportDma; + Memory=ImportMemRenderElement, +} + +render_elements! { + ImportMemTest5<'a, R, T> where R: ImportMem + ImportDma, T: Texture; + Memory=ImportMemRenderElement, + Custom=&'a T, +} + +render_elements! { + TextureIdTest where R: ImportMem, R::TextureId: Clone; + Memory=ImportMemRenderElement, +} + +render_elements! { + TextureIdTest1 where R: ImportMem, R::TextureId: 'static; + Memory=ImportMemRenderElement, +} + +render_elements! { + TextureIdTest2<'a, R, C> where R: ImportMem, R::TextureId: 'a; + Memory=ImportMemRenderElement, + Custom=&'a C, +} + +render_elements! { + TextureIdTest3<'a, R, C> where R: ImportMem, R::TextureId: Clone + 'a; + Memory=ImportMemRenderElement, + Custom=&'a C, +} + +render_elements! { + Test<='a, GlesRenderer>; + Surface=TestRenderElement<'a, GlesRenderer> +} + +render_elements! { + Test2<=GlesRenderer>; + Surface=TestRenderElement2 +} + +render_elements! { + Test3<='a, GlesRenderer, C>; + Surface=TestRenderElement<'a, GlesRenderer>, + Custom=&'a C, +} + +render_elements! { + Test4<=GlesRenderer, C>; + Surface=TestRenderElement2, + Custom=C +} + +render_elements! { + TestG<'a, R>; + Surface=TestRenderElement<'a, R> +} + +render_elements! { + TestG2; + Surface=TestRenderElement2 +} + +render_elements! { + TestG3<'a, R, C>; + Surface=TestRenderElement<'a, R>, + Custom=&'a C, +} + +render_elements! { + TestG4; + Surface=TestRenderElement2, + Custom=Wrap +} + +render_elements! { + TestG5; + What=Empty, +} + +render_elements! { + TestG6<'a, R, C>; + Surface=TestRenderElement<'a, R>, + Custom=&'a C, + Custom2=Wrap, +} + +render_elements! { + TestG7<'a, R, C1, C2>; + Surface=TestRenderElement<'a, R>, + Custom=Wrap, + Custom2=Wrap, +} + +struct ImportMemRenderElement {} + +impl Element for ImportMemRenderElement { + fn id(&self) -> &Id { + todo!() + } + + fn current_commit(&self) -> CommitCounter { + todo!() + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + todo!() + } + + fn src(&self) -> Rectangle { + todo!() + } +} + +impl RenderElement for ImportMemRenderElement +where + R: Renderer + ImportMem, +{ + fn draw( + &self, + _frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + todo!() + } +} + +impl Element for Empty { + fn id(&self) -> &Id { + todo!() + } + + fn current_commit(&self) -> CommitCounter { + todo!() + } + + fn location(&self, _scale: Scale) -> Point { + todo!() + } + + fn src(&self) -> Rectangle { + todo!() + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + todo!() + } +} + +impl RenderElement for Empty +where + R: Renderer, +{ + fn draw( + &self, + _frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + todo!() + } +} + +struct Empty; + +struct TestRenderElement2 { + _phantom: PhantomData, +} + +impl Element for TestRenderElement2 +where + R: Renderer, +{ + fn id(&self) -> &Id { + todo!() + } + + fn current_commit(&self) -> CommitCounter { + todo!() + } + + fn location(&self, _scale: Scale) -> Point { + todo!() + } + + fn src(&self) -> Rectangle { + todo!() + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + todo!() + } +} + +impl RenderElement for TestRenderElement2 +where + R: Renderer, +{ + fn draw( + &self, + _frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + todo!() + } +} + +struct TestRenderElement<'a, R> { + _test: &'a usize, + _phantom: PhantomData, +} + +impl Element for TestRenderElement<'_, R> +where + R: Renderer, +{ + fn id(&self) -> &Id { + todo!() + } + + fn current_commit(&self) -> CommitCounter { + todo!() + } + + fn location(&self, _scale: Scale) -> Point { + todo!() + } + + fn src(&self) -> Rectangle { + todo!() + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + todo!() + } +} + +impl RenderElement for TestRenderElement<'_, R> +where + R: Renderer, +{ + fn draw( + &self, + _frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + todo!() + } +} diff --git a/vendor/smithay/src/backend/renderer/element/texture.rs b/vendor/smithay/src/backend/renderer/element/texture.rs new file mode 100644 index 0000000..ce5845a --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/texture.rs @@ -0,0 +1,716 @@ +//! Element to render a texture +//! +//! # Why use this implementation +//! +//! This module provides the following elements +//! +//! ## Static texture +//! +//! [`TextureBuffer`] represents a static texture which is expected to not change. +//! It is possible to either use a [`pre-existing texture`](TextureBuffer::from_texture) or to create the texture +//! from [`RGBA memory`](TextureBuffer::from_memory). +//! The [`TextureBuffer`] can be used in the smithay pipeline by using [`TextureRenderElement::from_texture_buffer`]. +//! +//! ## Hardware accelerated rendering +//! +//! [`TextureRenderBuffer`] provides a solution for hardware accelerated rending with +//! proper damage tracking. It provides a [`RenderContext`] for rendering to the stored +//! texture. The collected damage from [`RenderContext::draw`] is used to report the +//! damaged regions in the [`TextureRenderElement`]. For rendering a [`TextureRenderBuffer`] you +//! can create a render element with [`TextureRenderElement::from_texture_render_buffer`]. +//! +//! # Why **not** to use this implementation +//! +//! For software rendering you may take a look at [`MemoryRenderBuffer`](super::memory::MemoryRenderBuffer) +//! which provides proper damage tracking and is agnostic over the [`Renderer`] +//! +//! # How to use it +//! +//! Both, [`TextureBuffer`] and [`TextureRenderBuffer`] represent a buffer of your texture data. +//! To render the buffer you have to create a [`TextureRenderElement`] in your render loop as +//! shown in the examples. +//! +//! To integrate custom texture based buffers the [`TextureRenderElement`] provides two functions +//! to create the render element. [`from_static_texture`](TextureRenderElement::from_static_texture) can be used to +//! create a render element from a static texture without damage tracking. +//! [`from_texture_with_damage`](TextureRenderElement::from_texture_with_damage) can be used to create +//! a render element from an existing texture with custom damage tracking. +//! In both cases you have to make sure to provide a stable [`Id`] or otherwise damage tracking will not work. +//! +//! ## [`TextureBuffer`] +//! +//! ```no_run +//! # use smithay::{ +//! # backend::renderer::{Color32F, DebugFlags, Frame, ImportMem, Renderer, Texture, TextureFilter, sync::SyncPoint, test::{DummyRenderer, DummyFramebuffer}}, +//! # utils::{Buffer, Physical, Rectangle, Size}, +//! # }; +//! use smithay::{ +//! backend::{ +//! allocator::Fourcc, +//! renderer::{ +//! damage::OutputDamageTracker, +//! element::{ +//! Kind, +//! texture::{TextureBuffer, TextureRenderElement}, +//! }, +//! }, +//! }, +//! utils::{Point, Transform}, +//! }; +//! +//! const WIDTH: i32 = 10; +//! const HEIGHT: i32 = 10; +//! +//! let memory = vec![0; (WIDTH * 4 * HEIGHT) as usize]; +//! # let mut renderer = DummyRenderer::default(); +//! # let mut framebuffer = DummyFramebuffer; +//! +//! // Create the texture buffer from a chunk of memory +//! let texture_buffer = TextureBuffer::from_memory( +//! &mut renderer, +//! &memory, +//! Fourcc::Argb8888, +//! (WIDTH, HEIGHT), +//! false, +//! 1, +//! Transform::Normal, +//! None, +//! ) +//! .expect("failed to import mem"); +//! +//! let mut damage_tracker = OutputDamageTracker::new((800, 600), 1.0, Transform::Normal); +//! +//! loop { +//! // Create a render element from the buffer +//! let location = Point::from((100.0, 100.0)); +//! let render_element = +//! TextureRenderElement::from_texture_buffer(location, &texture_buffer, None, None, None, Kind::Unspecified); +//! +//! // Render the element(s) +//! damage_tracker +//! .render_output(&mut renderer, &mut framebuffer, 0, &[&render_element], [0.8, 0.8, 0.9, 1.0]) +//! .expect("failed to render output"); +//! } +//! ``` +//! +//! ## [`TextureRenderBuffer`] +//! +//! ```no_run +//! # use smithay::{ +//! # backend::renderer::{Color32F, DebugFlags, Frame, ImportMem, Renderer, Texture, TextureFilter, sync::SyncPoint, test::{DummyRenderer, DummyFramebuffer}}, +//! # utils::{Buffer, Physical}, +//! # }; +//! use std::time::{Duration, Instant}; +//! +//! use smithay::{ +//! backend::{ +//! allocator::Fourcc, +//! renderer::{ +//! damage::OutputDamageTracker, +//! element::{ +//! Kind, +//! texture::{TextureRenderBuffer, TextureRenderElement}, +//! }, +//! }, +//! }, +//! utils::{Point, Rectangle, Size, Transform}, +//! }; +//! +//! const WIDTH: i32 = 10; +//! const HEIGHT: i32 = 10; +//! +//! let memory = vec![0; (WIDTH * 4 * HEIGHT) as usize]; +//! # let mut renderer = DummyRenderer::default(); +//! # let mut framebuffer = DummyFramebuffer; +//! +//! // Create the texture buffer from a chunk of memory +//! let mut texture_render_buffer = TextureRenderBuffer::from_memory( +//! &mut renderer, +//! &memory, +//! Fourcc::Argb8888, +//! (WIDTH, HEIGHT), +//! false, +//! 1, +//! Transform::Normal, +//! None, +//! ) +//! .expect("failed to import mem"); +//! +//! // Create the rendering context +//! let mut render_context = texture_render_buffer.render(); +//! +//! // Draw to the texture +//! render_context.draw(|_texture| { +//! // Your draw code here... +//! +//! // Return the damage areas +//! Result::<_, ()>::Ok(vec![Rectangle::from_size( +//! Size::from((WIDTH, HEIGHT)), +//! )]) +//! }); +//! +//! // Optionally update the opaque regions +//! render_context.update_opaque_regions(Some(vec![Rectangle::from_size( +//! Size::from((WIDTH, HEIGHT)), +//! )])); +//! +//! // We explicitly drop the context here to make the borrow checker happy +//! std::mem::drop(render_context); +//! +//! let mut damage_tracker = OutputDamageTracker::new((800, 600), 1.0, Transform::Normal); +//! +//! let mut last_update = Instant::now(); +//! +//! loop { +//! let now = Instant::now(); +//! if now.duration_since(last_update) >= Duration::from_secs(3) { +//! let mut render_context = texture_render_buffer.render(); +//! +//! render_context.draw(|_buffer| { +//! // Update the changed parts of the buffer +//! +//! // Return the updated parts +//! Result::<_, ()>::Ok(vec![Rectangle::from_size((WIDTH, HEIGHT).into())]) +//! }); +//! +//! last_update = now; +//! } +//! +//! // Create a render element from the buffer +//! let location = Point::from((100.0, 100.0)); +//! let render_element = TextureRenderElement::from_texture_render_buffer( +//! location, +//! &texture_render_buffer, +//! None, +//! None, +//! None, +//! Kind::Unspecified, +//! ); +//! +//! // Render the element(s) +//! damage_tracker +//! .render_output(&mut renderer, &mut framebuffer, 0, &[&render_element], [0.8, 0.8, 0.9, 1.0]) +//! .expect("failed to render output"); +//! } +//! ``` + +use std::sync::{Arc, Mutex}; + +use tracing::{instrument, warn}; + +use crate::{ + backend::{ + allocator::Fourcc, + renderer::{ + ContextId, Frame, ImportMem, Renderer, Texture, + utils::{DamageBag, DamageSet, DamageSnapshot, OpaqueRegions}, + }, + }, + utils::{ + Buffer, Coordinate, Logical, Physical, Point, Rectangle, Scale, Size, Transform, + user_data::UserDataMap, + }, +}; + +use super::{CommitCounter, Element, Id, Kind, RenderElement}; + +/// A single texture buffer +#[derive(Debug, Clone)] +pub struct TextureBuffer { + id: Id, + context_id: ContextId, + texture: T, + scale: i32, + transform: Transform, + opaque_regions: Option>>, +} + +impl TextureBuffer { + /// Create a [`TextureBuffer`] from an existing texture + pub fn from_texture>( + renderer: &R, + texture: T, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + TextureBuffer { + id: Id::new(), + context_id: renderer.context_id(), + texture, + scale, + transform, + opaque_regions, + } + } + + /// Create [`TextureBuffer`] from a chunk of memory + #[allow(clippy::too_many_arguments)] + pub fn from_memory + ImportMem>( + renderer: &mut R, + data: &[u8], + format: Fourcc, + size: impl Into>, + flipped: bool, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Result { + let texture = renderer.import_memory(data, format, size.into(), flipped)?; + Ok(TextureBuffer::from_texture( + renderer, + texture, + scale, + transform, + opaque_regions, + )) + } + + /// Format of the underlying texture + pub fn format(&self) -> Option + where + T: Texture, + { + self.texture.format() + } +} + +/// A texture backed render buffer +#[derive(Debug, Clone)] +pub struct TextureRenderBuffer { + id: Id, + context_id: ContextId, + texture: T, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + damage_tracker: Arc>>, +} + +impl TextureRenderBuffer { + /// Create [`TextureRenderBuffer`] from an existing texture + pub fn from_texture>( + renderer: &R, + texture: T, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Self { + TextureRenderBuffer { + id: Id::new(), + context_id: renderer.context_id(), + texture, + scale, + transform, + opaque_regions, + damage_tracker: Arc::new(Mutex::new(DamageBag::default())), + } + } + + /// Create [`TextureRenderBuffer`] from a chunk of memory + #[allow(clippy::too_many_arguments)] + pub fn from_memory + ImportMem>( + renderer: &mut R, + data: &[u8], + format: Fourcc, + size: impl Into>, + flipped: bool, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) -> Result { + let texture = renderer.import_memory(data, format, size.into(), flipped)?; + Ok(TextureRenderBuffer::from_texture( + renderer, + texture, + scale, + transform, + opaque_regions, + )) + } + + /// Replace the stored texture + pub fn update_from_texture>( + &mut self, + renderer: &R, + texture: T, + scale: i32, + transform: Transform, + opaque_regions: Option>>, + ) { + assert_eq!(self.context_id, renderer.context_id()); + self.texture = texture; + self.scale = scale; + self.transform = transform; + self.opaque_regions = opaque_regions; + self.damage_tracker.lock().unwrap().reset(); + } + + /// Update the texture from a chunk of memory + pub fn update_from_memory + ImportMem>( + &mut self, + renderer: &mut R, + data: &[u8], + region: Rectangle, + opaque_regions: Option>>, + ) -> Result<(), R::Error> { + assert_eq!(self.context_id, renderer.context_id()); + renderer.update_memory(&self.texture, data, region)?; + self.damage_tracker.lock().unwrap().add([region]); + self.opaque_regions = opaque_regions; + Ok(()) + } + + /// Render to the texture + pub fn render(&mut self) -> RenderContext<'_, T> { + RenderContext { + buffer: self, + damage: Vec::new(), + opaque_regions: None, + } + } + + /// Format of the underlying texture + pub fn format(&self) -> Option { + self.texture.format() + } +} + +/// A render context for [`TextureRenderBuffer`] +#[derive(Debug)] +pub struct RenderContext<'a, T: Texture> { + buffer: &'a mut TextureRenderBuffer, + damage: Vec>, + opaque_regions: Option>>>, +} + +impl RenderContext<'_, T> { + /// Draw to the buffer + pub fn draw(&mut self, f: F) -> Result<(), E> + where + F: FnOnce(&mut T) -> Result>, E>, + { + let draw_damage = f(&mut self.buffer.texture)?; + self.damage.extend(draw_damage); + Ok(()) + } + + /// Update the opaque regions + pub fn update_opaque_regions(&mut self, opaque_regions: Option>>) { + self.opaque_regions = Some(opaque_regions); + } +} + +impl Drop for RenderContext<'_, T> { + fn drop(&mut self) { + self.buffer + .damage_tracker + .lock() + .unwrap() + .add(std::mem::take(&mut self.damage)); + if let Some(opaque_regions) = self.opaque_regions.take() { + self.buffer.opaque_regions = opaque_regions; + } + } +} + +/// A render element for a [`TextureRenderBuffer`] +#[derive(Debug)] +pub struct TextureRenderElement { + location: Point, + id: Id, + context_id: ContextId, + pub(crate) texture: T, + scale: i32, + transform: Transform, + alpha: f32, + src: Option>, + size: Option>, + opaque_regions: Option>>, + snapshot: DamageSnapshot, + kind: Kind, +} + +impl TextureRenderElement { + fn damage_since(&self, commit: Option) -> DamageSet { + self.snapshot + .damage_since(commit) + .unwrap_or_else(|| DamageSet::from_slice(&[Rectangle::from_size(self.texture.size())])) + } +} + +impl TextureRenderElement { + /// Create a [`TextureRenderElement`] from a [`TextureRenderBuffer`] + pub fn from_texture_render_buffer( + location: impl Into>, + buffer: &TextureRenderBuffer, + alpha: Option, + src: Option>, + size: Option>, + kind: Kind, + ) -> Self { + TextureRenderElement::from_texture_with_damage( + buffer.id.clone(), + buffer.context_id.clone(), + location, + buffer.texture.clone(), + buffer.scale, + buffer.transform, + alpha, + src, + size, + buffer.opaque_regions.clone(), + buffer.damage_tracker.lock().unwrap().snapshot(), + kind, + ) + } + + /// Create a [`TextureRenderElement`] from a [`TextureBuffer`] + pub fn from_texture_buffer( + location: impl Into>, + buffer: &TextureBuffer, + alpha: Option, + src: Option>, + size: Option>, + kind: Kind, + ) -> Self { + TextureRenderElement::from_static_texture( + buffer.id.clone(), + buffer.context_id.clone(), + location, + buffer.texture.clone(), + buffer.scale, + buffer.transform, + alpha, + src, + size, + buffer.opaque_regions.clone(), + kind, + ) + } +} + +impl TextureRenderElement { + /// Create a [`TextureRenderElement`] from an + /// existing texture and a [`DamageSnapshot`] + #[allow(clippy::too_many_arguments)] + pub fn from_texture_with_damage( + id: Id, + context_id: ContextId, + location: impl Into>, + texture: T, + scale: i32, + transform: Transform, + alpha: Option, + src: Option>, + size: Option>, + opaque_regions: Option>>, + snapshot: DamageSnapshot, + kind: Kind, + ) -> Self { + let opaque_regions = opaque_regions.map(|regions| { + regions + .into_iter() + .map(|region| region.to_logical(scale, transform, &texture.size())) + .collect::>() + }); + TextureRenderElement { + location: location.into(), + id, + context_id, + texture, + scale, + transform, + alpha: alpha.unwrap_or(1.0), + src, + size, + opaque_regions, + snapshot, + kind, + } + } + + /// Create a static [`TextureRenderElement`] from + /// an existing texture + #[allow(clippy::too_many_arguments)] + pub fn from_static_texture( + id: Id, + context_id: ContextId, + location: impl Into>, + texture: T, + scale: i32, + transform: Transform, + alpha: Option, + src: Option>, + size: Option>, + opaque_regions: Option>>, + kind: Kind, + ) -> Self { + TextureRenderElement::from_texture_with_damage( + id, + context_id, + location, + texture, + scale, + transform, + alpha, + src, + size, + opaque_regions, + DamageSnapshot::empty(), + kind, + ) + } + + fn logical_size(&self) -> Size { + self.size + .or_else(|| { + self.src + .map(|src| Size::from((src.size.w as i32, src.size.h as i32))) + }) + .unwrap_or_else(|| self.texture.size().to_logical(self.scale, self.transform)) + } + + fn physical_size(&self, scale: Scale) -> Size { + let logical_size = self.logical_size(); + ((logical_size.to_f64().to_physical(scale).to_point() + self.location).to_i32_round() + - self.location.to_i32_round()) + .to_size() + } + + fn src(&self) -> Rectangle { + self.src + .unwrap_or_else(|| Rectangle::from_size(self.logical_size().to_f64())) + } + + fn scale(&self) -> Scale { + let size = self.logical_size(); + let src = self.src(); + Scale::from((size.w as f64 / src.size.w, size.h as f64 / src.size.h)) + } + + fn rect_to_global(&self, rect: Rectangle) -> Rectangle + where + N: Coordinate, + { + let scale = self.scale(); + let mut rect = rect.to_f64(); + rect.loc -= self.src.map(|rect| rect.loc).unwrap_or_default(); + rect.upscale(scale) + } +} + +impl Element for TextureRenderElement +where + T: Texture, +{ + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.snapshot.current_commit() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + Rectangle::new(self.location.to_i32_round(), self.physical_size(scale)) + } + + fn transform(&self) -> Transform { + self.transform + } + + fn src(&self) -> Rectangle { + let size = self.logical_size(); + self.src() + .to_buffer(self.scale as f64, self.transform, &size.to_f64()) + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + let src = self.src(); + let texture_size = self.texture.size(); + let physical_size = self.physical_size(scale); + let logical_size = self.logical_size(); + self.damage_since(commit) + .into_iter() + .filter_map(|rect| { + rect.to_f64() + .to_logical(self.scale as f64, self.transform, &texture_size.to_f64()) + .intersection(src) + .map(|rect| self.rect_to_global(rect).to_i32_up::()) + .map(|rect| { + let surface_scale = physical_size.to_f64() / logical_size.to_f64().to_physical(scale); + rect.to_physical_precise_up(surface_scale * scale) + }) + }) + .collect::>() + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + if self.alpha < 1.0 { + return OpaqueRegions::default(); + } + + let src = self.src(); + let physical_size = self.physical_size(scale); + let logical_size = self.logical_size(); + self.opaque_regions + .as_ref() + .map(|r| { + r.iter() + .filter_map(|rect| { + rect.to_f64() + .intersection(src) + .map(|rect| self.rect_to_global(rect).to_i32_up::()) + .map(|rect| { + let surface_scale = + physical_size.to_f64() / logical_size.to_f64().to_physical(scale); + rect.to_physical_precise_up(surface_scale * scale) + }) + }) + .collect::>() + }) + .unwrap_or_default() + } + + fn alpha(&self) -> f32 { + self.alpha + } + + fn kind(&self) -> Kind { + self.kind + } +} + +impl RenderElement for TextureRenderElement +where + R: Renderer, + T: Texture, +{ + #[instrument(level = "trace", skip(self, frame))] + #[profiling::function] + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + if frame.context_id() != self.context_id { + warn!("trying to render texture from different renderer context"); + return Ok(()); + } + + frame.render_texture_from_to( + &self.texture, + src, + dst, + damage, + opaque_regions, + self.transform, + self.alpha, + ) + } +} diff --git a/vendor/smithay/src/backend/renderer/element/utils/elements.rs b/vendor/smithay/src/backend/renderer/element/utils/elements.rs new file mode 100644 index 0000000..e6dff4a --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/utils/elements.rs @@ -0,0 +1,614 @@ +//! Utilities and helpers around the `Element` trait. + +use crate::{ + backend::renderer::{ + Renderer, + element::{AsRenderElements, Element, Id, Kind, RenderElement, UnderlyingStorage}, + utils::{DamageSet, OpaqueRegions}, + }, + utils::{Buffer, Physical, Point, Rectangle, Scale, user_data::UserDataMap}, +}; + +/// A element that allows to re-scale another element +#[derive(Debug)] +pub struct RescaleRenderElement { + element: E, + origin: Point, + scale: Scale, +} + +impl RescaleRenderElement { + /// Create a new re-scale element for an existing element + /// + /// The origin can be used to scale the element geometry relative to a [`Point`]. + /// One use case of this is to only scale the location for some elements in a group, like + /// a surface tree. + pub fn from_element(element: E, origin: Point, scale: impl Into>) -> Self { + RescaleRenderElement { + element, + origin, + scale: scale.into(), + } + } +} + +impl Element for RescaleRenderElement { + fn id(&self) -> &Id { + self.element.id() + } + + fn current_commit(&self) -> crate::backend::renderer::utils::CommitCounter { + self.element.current_commit() + } + + fn src(&self) -> Rectangle { + self.element.src() + } + + fn geometry(&self, scale: crate::utils::Scale) -> Rectangle { + let mut element_geometry = self.element.geometry(scale); + // First we make the element relative to the origin + element_geometry.loc -= self.origin; + // Then we scale it by our scale + element_geometry = element_geometry.to_f64().upscale(self.scale).to_i32_round(); + // At last we move it back to the origin + element_geometry.loc += self.origin; + element_geometry + } + + fn transform(&self) -> crate::utils::Transform { + self.element.transform() + } + + fn damage_since( + &self, + scale: crate::utils::Scale, + commit: Option, + ) -> DamageSet { + self.element + .damage_since(scale, commit) + .into_iter() + .map(|rect| rect.to_f64().upscale(self.scale).to_i32_up()) + .collect::>() + } + + fn opaque_regions(&self, scale: crate::utils::Scale) -> OpaqueRegions { + self.element + .opaque_regions(scale) + .into_iter() + .map(|rect| rect.to_f64().upscale(self.scale).to_i32_round()) + .collect::>() + } + + fn alpha(&self) -> f32 { + self.element.alpha() + } + + fn kind(&self) -> Kind { + self.element.kind() + } + + fn is_framebuffer_effect(&self) -> bool { + self.element.is_framebuffer_effect() + } +} + +impl> RenderElement for RescaleRenderElement { + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + self.element.draw(frame, src, dst, damage, opaque_regions, cache) + } + + #[inline] + fn underlying_storage(&self, renderer: &mut R) -> Option> { + self.element.underlying_storage(renderer) + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + self.element.capture_framebuffer(frame, src, dst, cache) + } +} + +/// A element that allows to crop another element +#[derive(Debug)] +pub struct CropRenderElement { + element: E, + src: Rectangle, + crop_rect: Rectangle, +} + +impl CropRenderElement { + /// Create a cropping render element for an existing element + /// + /// The crop rect is expected to be relative to the same origin the element is relative to. + /// It can extend outside of the element geometry but the resulting geometry will always + /// be equal or smaller than the geometry of the original element. + /// + /// The scale is used to calculate the intersection between the crop rect and the + /// original element geometry and should therefore equal the scale that was used to + /// calculate the crop rect. + /// + /// Returns `None` if there is no overlap between the crop rect and the element geometry + pub fn from_element( + element: E, + scale: impl Into>, + crop_rect: Rectangle, + ) -> Option { + let scale = scale.into(); + + let element_geometry = element.geometry(scale); + + if let Some(intersection) = element_geometry.intersection(crop_rect) { + // FIXME: intersection sometimes return a 0 size element + if intersection.is_empty() { + return None; + } + + // We need to know how much we should crop from the element + let mut element_relative_intersection = intersection; + element_relative_intersection.loc -= element_geometry.loc; + + // Now we calculate the scale from the original element geometry to the + // original element src. We need that scale to bring our intersection + // into buffer space. + // We also have to consider the buffer transform for this or otherwise + // the scale would be wrong + let element_src = element.src(); + let transform = element.transform(); + let physical_to_buffer_scale = + element_src.size / transform.invert().transform_size(element_geometry.size).to_f64(); + + // Ok, for the src we need to know how much we cropped from the element geometry + // and then bring that rectangle into buffer space. For this we have to first + // apply the element transform and then scale it to buffer space. + let mut src = element_relative_intersection.to_f64().to_logical(1.0).to_buffer( + physical_to_buffer_scale, + transform, + &element_geometry.size.to_f64().to_logical(1.0), + ); + + // Ensure cropping of the existing element is respected. + src.loc += element_src.loc; + + Some(CropRenderElement { + element, + src, + crop_rect, + }) + } else { + None + } + } + + fn element_crop_rect(&self, scale: Scale) -> Option> { + let element_geometry = self.element.geometry(scale); + + if let Some(mut intersection) = element_geometry.intersection(self.crop_rect) { + // FIXME: intersection sometimes return a 0 size element + if intersection.is_empty() { + return None; + } + + intersection.loc -= element_geometry.loc; + Some(intersection) + } else { + None + } + } +} + +impl Element for CropRenderElement { + fn id(&self) -> &Id { + self.element.id() + } + + fn current_commit(&self) -> crate::backend::renderer::utils::CommitCounter { + self.element.current_commit() + } + + fn src(&self) -> Rectangle { + self.src + } + + fn geometry(&self, scale: Scale) -> Rectangle { + let element_geometry = self.element.geometry(scale); + if let Some(intersection) = element_geometry.intersection(self.crop_rect) { + // FIXME: intersection sometimes return a 0 size element + if intersection.is_empty() { + return Default::default(); + } + + intersection + } else { + Default::default() + } + } + + fn transform(&self) -> crate::utils::Transform { + self.element.transform() + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + if let Some(element_crop_rect) = self.element_crop_rect(scale) { + self.element + .damage_since(scale, commit) + .into_iter() + .flat_map(|rect| { + rect.intersection(element_crop_rect).map(|mut rect| { + rect.loc -= element_crop_rect.loc; + rect + }) + }) + .collect::>() + } else { + Default::default() + } + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + if let Some(element_crop_rect) = self.element_crop_rect(scale) { + self.element + .opaque_regions(scale) + .into_iter() + .flat_map(|rect| { + rect.intersection(element_crop_rect).map(|mut rect| { + rect.loc -= element_crop_rect.loc; + rect + }) + }) + .collect::>() + } else { + Default::default() + } + } + + fn alpha(&self) -> f32 { + self.element.alpha() + } + + fn kind(&self) -> Kind { + self.element.kind() + } + + fn is_framebuffer_effect(&self) -> bool { + self.element.is_framebuffer_effect() + } +} + +impl> RenderElement for CropRenderElement { + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + self.element.draw(frame, src, dst, damage, opaque_regions, cache) + } + + #[inline] + fn underlying_storage(&self, renderer: &mut R) -> Option> { + self.element.underlying_storage(renderer) + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + self.element.capture_framebuffer(frame, src, dst, cache) + } +} + +/// Defines how the location parameter should apply in [`RelocateRenderElement::from_element`] +#[derive(Debug, Copy, Clone)] +pub enum Relocate { + /// The supplied location replaces the original location + Absolute, + /// The supplied location offsets the original location + Relative, +} + +/// A element that allows to offset the location of an existing element +#[derive(Debug)] +pub struct RelocateRenderElement { + element: E, + relocate: Relocate, + location: Point, +} + +impl RelocateRenderElement { + /// Crate an re-locate element for an existing element + pub fn from_element(element: E, location: impl Into>, relocate: Relocate) -> Self { + let location = location.into(); + + RelocateRenderElement { + element, + location, + relocate, + } + } +} + +impl Element for RelocateRenderElement { + fn id(&self) -> &Id { + self.element.id() + } + + fn current_commit(&self) -> crate::backend::renderer::utils::CommitCounter { + self.element.current_commit() + } + + fn src(&self) -> Rectangle { + self.element.src() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + let mut geo = self.element.geometry(scale); + match self.relocate { + Relocate::Absolute => geo.loc = self.location, + Relocate::Relative => geo.loc += self.location, + } + geo + } + + fn location(&self, scale: Scale) -> Point { + match self.relocate { + Relocate::Absolute => self.location, + Relocate::Relative => self.element.location(scale) + self.location, + } + } + + fn transform(&self) -> crate::utils::Transform { + self.element.transform() + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + self.element.damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + self.element.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.element.alpha() + } + + fn kind(&self) -> Kind { + self.element.kind() + } + + fn is_framebuffer_effect(&self) -> bool { + self.element.is_framebuffer_effect() + } +} + +impl> RenderElement for RelocateRenderElement { + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + self.element.draw(frame, src, dst, damage, opaque_regions, cache) + } + + #[inline] + fn underlying_storage(&self, renderer: &mut R) -> Option> { + self.element.underlying_storage(renderer) + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + self.element.capture_framebuffer(frame, src, dst, cache) + } +} + +/// Defines the scale behavior for the constrain +#[derive(Debug, Copy, Clone)] +pub enum ConstrainScaleBehavior { + /// Fit the element into the size, nothing will be cropped + Fit, + /// Zoom the element into the size, crops if the aspect ratio + /// of the element does not match the aspect of the constrain + /// size + Zoom, + /// Always stretch the element to the constrain size + Stretch, + /// Do not scale, but cut off at the constrain size + CutOff, +} + +bitflags::bitflags! { + /// Defines how the elements should be aligned during constrain + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct ConstrainAlign: u8 { + /// Align to the top + const TOP = 0b000001; + /// Align to the left + const LEFT = 0b000010; + /// Align to the right + const RIGHT = 0b000100; + /// Align to the bottom + const BOTTOM = 0b001000; + /// Align to the top left + /// + /// Equals TOP | LEFT + const TOP_LEFT = Self::TOP.bits() | Self::LEFT.bits(); + /// Align to the top right + /// + /// Equals TOP | RIGHT + const TOP_RIGHT = Self::TOP.bits() | Self::RIGHT.bits(); + /// Align to the bottom left + /// + /// Equals BOTTOM | LEFt + const BOTTOM_LEFT = Self::BOTTOM.bits() | Self::LEFT.bits(); + /// Align to the bottom left + /// + /// Equals BOTTOM | LEFT + const BOTTOM_RIGHT = Self::BOTTOM.bits() | Self::RIGHT.bits(); + /// Align to the center + /// + /// Equals TOP | LEFT | BOTTOM | RIGHT + const CENTER = Self::TOP.bits() | Self::LEFT.bits() | Self::BOTTOM.bits() | Self::RIGHT.bits(); + } +} + +/// Convenience function to constrain something that implements [`AsRenderElements`] +/// +/// See [`constrain_render_elements`] for more information +#[profiling::function] +#[allow(clippy::too_many_arguments)] +pub fn constrain_as_render_elements( + element: &E, + renderer: &mut R, + location: L, + alpha: f32, + constrain: Rectangle, + reference: Rectangle, + behavior: ConstrainScaleBehavior, + align: ConstrainAlign, + output_scale: S, +) -> impl Iterator + use +where + R: Renderer, + E: AsRenderElements, + C: From< + CropRenderElement< + RelocateRenderElement>::RenderElement>>, + >, + >, + L: Into>, + S: Into>, +{ + let location = location.into(); + let output_scale = output_scale.into(); + let elements: Vec<>::RenderElement> = + AsRenderElements::::render_elements(element, renderer, location, output_scale, alpha); + constrain_render_elements( + elements, + location, + constrain, + reference, + behavior, + align, + output_scale, + ) + .map(C::from) +} + +/// Constrain render elements on a specific location with a specific size +/// +/// * `origin` - Defines the origin for re-scaling +/// * `constrain` - Defines the rectangle on screen the elements should be constrain within +/// * `reference` - Defines the reference that should be used for constraining the elements, +/// this is most commonly the bounding box or geometry of the elements +/// * `behavior` - Defines the behavior for scaling the elements reference in the constrain +/// * `align` - Defines how the scaled elements should be aligned within the constrain +/// * `scale` - The scale that was used to create the original elements +#[profiling::function] +pub fn constrain_render_elements( + elements: impl IntoIterator, + origin: impl Into>, + constrain: Rectangle, + reference: Rectangle, + behavior: ConstrainScaleBehavior, + align: ConstrainAlign, + scale: impl Into>, +) -> impl Iterator>>> +where + E: Element, +{ + let location = origin.into(); + let scale = scale.into(); + + let element_scale = match behavior { + ConstrainScaleBehavior::Fit => { + let reference = reference.to_f64(); + let size = constrain.size.to_f64(); + let element_scale: Scale = size / reference.size; + Scale::from(f64::min(element_scale.x, element_scale.y)) + } + ConstrainScaleBehavior::Zoom => { + let reference = reference.to_f64(); + let size = constrain.size.to_f64(); + let element_scale: Scale = size / reference.size; + Scale::from(f64::max(element_scale.x, element_scale.y)) + } + ConstrainScaleBehavior::Stretch => { + let reference = reference.to_f64(); + let size = constrain.size.to_f64(); + size / reference.size + } + ConstrainScaleBehavior::CutOff => Scale::from(1.0), + }; + + let scaled_reference = reference.to_f64().upscale(element_scale); + + // Calculate the align offset + let top_offset: f64 = if align.contains(ConstrainAlign::TOP | ConstrainAlign::BOTTOM) { + (constrain.size.h as f64 - scaled_reference.size.h) / 2f64 + } else if align.contains(ConstrainAlign::BOTTOM) { + constrain.size.h as f64 - scaled_reference.size.h + } else { + 0f64 + }; + + let left_offset: f64 = if align.contains(ConstrainAlign::LEFT | ConstrainAlign::RIGHT) { + (constrain.size.w as f64 - scaled_reference.size.w) / 2f64 + } else if align.contains(ConstrainAlign::RIGHT) { + constrain.size.w as f64 - scaled_reference.size.w + } else { + 0f64 + }; + + let align_offset: Point = Point::from((left_offset, top_offset)); + + // We need to offset the elements by the reference loc or otherwise + // the element could be positioned outside of our constrain rect. + let reference_offset = + reference.loc.to_f64() - Point::from((scaled_reference.loc.x, scaled_reference.loc.y)); + + // Final offset + let offset = (reference_offset + align_offset).to_i32_round(); + + elements + .into_iter() + .map(move |e| RescaleRenderElement::from_element(e, location, element_scale)) + .map(move |e| RelocateRenderElement::from_element(e, offset, Relocate::Relative)) + .filter_map(move |e| CropRenderElement::from_element(e, scale, constrain)) +} diff --git a/vendor/smithay/src/backend/renderer/element/utils/mod.rs b/vendor/smithay/src/backend/renderer/element/utils/mod.rs new file mode 100644 index 0000000..d97af5b --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/utils/mod.rs @@ -0,0 +1,9 @@ +//! Utilities and helpers around the `Element` trait. + +mod elements; +#[cfg(feature = "wayland_frontend")] +mod wayland; + +pub use elements::*; +#[cfg(feature = "wayland_frontend")] +pub use wayland::*; diff --git a/vendor/smithay/src/backend/renderer/element/utils/wayland.rs b/vendor/smithay/src/backend/renderer/element/utils/wayland.rs new file mode 100644 index 0000000..8c040b5 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/element/utils/wayland.rs @@ -0,0 +1,33 @@ +use crate::{ + backend::renderer::element::{Id, RenderElementPresentationState, RenderElementStates, RenderingReason}, + wayland::dmabuf::DmabufFeedback, +}; + +/// Select a [`DmabufFeedback`] based on the [`RenderElementPresentationState`] for a single [`Element`](crate::backend::renderer::element::Element) +/// +/// Returns the provided scan-out feedback if the element has been successfully assigned for scan-out or +/// was selected for scan-out but failed the scan-out test. +/// Otherwise the provided default feedback is returned. +pub fn select_dmabuf_feedback<'a>( + element: impl Into, + render_element_states: &RenderElementStates, + default_feedback: &'a DmabufFeedback, + scanout_feedback: &'a DmabufFeedback, +) -> &'a DmabufFeedback { + let id = element.into(); + + let Some(state) = render_element_states.element_render_state(id) else { + return default_feedback; + }; + + match state.presentation_state { + RenderElementPresentationState::Rendering { reason } => match reason { + Some(RenderingReason::FormatUnsupported) | Some(RenderingReason::ScanoutFailed) => { + scanout_feedback + } + None => default_feedback, + }, + RenderElementPresentationState::ZeroCopy => scanout_feedback, + RenderElementPresentationState::Skipped => default_feedback, + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/element.rs b/vendor/smithay/src/backend/renderer/gles/element.rs new file mode 100644 index 0000000..1c943d3 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/element.rs @@ -0,0 +1,227 @@ +//! RenderElements specific to using a `GlesRenderer` + +use crate::{ + backend::renderer::{ + element::{Element, Id, Kind, RenderElement, UnderlyingStorage, texture::TextureRenderElement}, + utils::{CommitCounter, DamageSet, OpaqueRegions}, + }, + utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform, user_data::UserDataMap}, +}; + +use super::{GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform}; + +/// Render element for drawing with a gles2 pixel shader +#[derive(Debug, Clone)] +pub struct PixelShaderElement { + shader: GlesPixelProgram, + id: Id, + commit_counter: CommitCounter, + area: Rectangle, + opaque_regions: Vec>, + alpha: f32, + additional_uniforms: Vec>, + kind: Kind, +} + +impl PixelShaderElement { + /// Create a new [`PixelShaderElement`] from a [`GlesPixelProgram`], + /// which can be constructed using [`GlesRenderer::compile_custom_pixel_shader`] + pub fn new( + shader: GlesPixelProgram, + area: Rectangle, + opaque_regions: Option>>, + alpha: f32, + additional_uniforms: Vec>, + kind: Kind, + ) -> Self { + PixelShaderElement { + shader, + id: Id::new(), + commit_counter: CommitCounter::default(), + area, + opaque_regions: opaque_regions.unwrap_or_default(), + alpha, + additional_uniforms: additional_uniforms.into_iter().map(|u| u.into_owned()).collect(), + kind, + } + } + + /// Resize the canvas area + pub fn resize( + &mut self, + area: Rectangle, + opaque_regions: Option>>, + ) { + let opaque_regions = opaque_regions.unwrap_or_default(); + if self.area != area || self.opaque_regions != opaque_regions { + self.area = area; + self.opaque_regions = opaque_regions; + self.commit_counter.increment(); + } + } + + /// Update the additional uniforms + /// (see [`GlesRenderer::compile_custom_pixel_shader`] and [`GlesFrame::render_pixel_shader_to`]). + /// + /// This replaces the stored uniforms, you have to update all of them, partial updates are not possible. + pub fn update_uniforms(&mut self, additional_uniforms: Vec>) { + self.additional_uniforms = additional_uniforms.into_iter().map(|u| u.into_owned()).collect(); + self.commit_counter.increment(); + } +} + +impl Element for PixelShaderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.commit_counter + } + + fn src(&self) -> Rectangle { + Rectangle::from_size(self.area.size.to_f64().to_buffer(1.0, Transform::Normal)) + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.area.to_physical_precise_round(scale) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + self.opaque_regions + .iter() + .map(|region| region.to_physical_precise_round(scale)) + .collect() + } + + fn alpha(&self) -> f32 { + self.alpha + } + + fn kind(&self) -> Kind { + self.kind + } +} + +impl RenderElement for PixelShaderElement { + #[profiling::function] + fn draw( + &self, + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), GlesError> { + frame.render_pixel_shader_to( + &self.shader, + src, + dst, + self.area.size.to_buffer(1, Transform::Normal), + Some(damage), + self.alpha, + &self.additional_uniforms, + ) + } + + #[inline] + fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option> { + None + } +} + +/// Render element for drawing with a gles2 texture shader +#[derive(Debug)] +pub struct TextureShaderElement { + inner: TextureRenderElement, + program: GlesTexProgram, + id: Id, + additional_uniforms: Vec>, +} + +impl TextureShaderElement { + /// Create a new [`TextureShaderElement`] from a [`TextureRenderElement`] and a + /// [`GlesTexProgram`], which can be constructed using + /// [`GlesRenderer::compile_custom_texture_shader`] + pub fn new( + inner: TextureRenderElement, + program: GlesTexProgram, + additional_uniforms: Vec>, + ) -> Self { + Self { + inner, + program, + id: Id::new(), + + additional_uniforms: additional_uniforms.into_iter().map(|u| u.into_owned()).collect(), + } + } +} + +impl Element for TextureShaderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.inner.current_commit() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.inner.geometry(scale) + } + + fn transform(&self) -> Transform { + self.inner.transform() + } + + fn src(&self) -> Rectangle { + self.inner.src() + } + + fn damage_since(&self, scale: Scale, commit: Option) -> DamageSet { + self.inner.damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + self.inner.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.inner.alpha() + } + + fn kind(&self) -> Kind { + self.inner.kind() + } + + fn location(&self, scale: Scale) -> Point { + self.inner.location(scale) + } +} + +impl RenderElement for TextureShaderElement { + #[profiling::function] + fn draw( + &self, + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), GlesError> { + frame.render_texture_from_to( + &self.inner.texture, + src, + dst, + damage, + opaque_regions, + self.transform(), + self.alpha(), + Some(&self.program), + &self.additional_uniforms, + ) + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/error.rs b/vendor/smithay/src/backend/renderer/gles/error.rs new file mode 100644 index 0000000..5dfb364 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/error.rs @@ -0,0 +1,149 @@ +use super::*; +use crate::backend::SwapBuffersError; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_shm; + +/// Error returned during rendering using GL ES +#[derive(thiserror::Error, Debug)] +pub enum GlesError { + /// A shader could not be compiled + #[error("Failed to compile Shader")] + ShaderCompileError, + /// A program could not be linked + #[error("Failed to link Program")] + ProgramLinkError, + /// A framebuffer could not be bound + #[error("Failed to bind Framebuffer")] + FramebufferBindingError, + /// Required GL functions could not be loaded + #[error("Failed to load GL functions from EGL")] + GLFunctionLoaderError, + /// Required GL extension are not supported by the underlying implementation + #[error( + "None of the following GL extensions is supported by the underlying GL implementation, at least one is required: {0:?}" + )] + GLExtensionNotSupported(&'static [&'static str]), + /// Required EGL extension are not supported by the underlying implementation + #[error( + "None of the following EGL extensions is supported by the underlying implementation, at least one is required: {0:?}" + )] + EGLExtensionNotSupported(&'static [&'static str]), + /// Required GL version is not available by the underlying implementation + #[error("The OpenGL ES version of the underlying GL implementation is too low, at least required: {0:?}")] + GLVersionNotSupported(version::GlVersion), + /// The underlying egl context could not be activated + #[error("Failed to active egl context")] + ContextActivationError(#[from] crate::backend::egl::MakeCurrentError), + ///The given dmabuf could not be converted to an EGLImage for framebuffer use + #[error("Failed to convert between dmabuf and EGLImage")] + BindBufferEGLError(#[source] crate::backend::egl::Error), + /// The given buffer has an unknown pixel format + #[error("Unknown pixel format")] + UnknownPixelFormat, + /// The given buffer has an unsupported pixel format + #[error("Unsupported pixel format: {0:?}")] + UnsupportedPixelFormat(Fourcc), + /// The given buffer has an unknown pixel layout + #[error("Unsupported pixel layout")] + UnsupportedPixelLayout, + /// The given wl buffer has an unsupported pixel format + #[error("Unsupported wl_shm format: {0:?}")] + #[cfg(feature = "wayland_frontend")] + UnsupportedWlPixelFormat(wl_shm::Format), + /// The given buffer was not accessible + #[error("Error accessing the buffer ({0:?})")] + #[cfg(feature = "wayland_frontend")] + BufferAccessError(crate::wayland::shm::BufferAccessError), + /// The given egl buffer was not accessible + #[error("Error accessing the buffer ({0:?})")] + #[cfg(feature = "wayland_frontend")] + EGLBufferAccessError(crate::backend::egl::BufferAccessError), + /// There was an error mapping the buffer + #[error("Error mapping the buffer")] + MappingError, + /// The provided buffer's size did not match the requested one. + #[error("Error reading buffer, size is too small for the given dimensions")] + UnexpectedSize, + /// Unable to determine the size of the framebuffer + #[error("Error determining the size of the provided framebuffer")] + UnknownSize, + /// The blitting operation was unsuccessful + #[error("Error blitting between framebuffers")] + BlitError, + /// An error occurred while creating the shader object. + #[error("An error occurred while creating the shader object.")] + CreateShaderObject, + /// Uniform was not declared when compiling shader + #[error("Uniform {0:?} was not declared when compiling the provided shader")] + UnknownUniform(String), + /// The provided uniform has a different type then was provided when compiling the shader + #[error("Uniform with different type (got {provided:?}, expected: {declared:?})")] + UniformTypeMismatch { + /// Uniform type that was provided during the call + provided: UniformType, + /// Uniform type that was declared when compiling + declared: UniformType, + }, + /// Blocking for a synchronization primitive failed + #[error("Blocking for a synchronization primitive got interrupted")] + SyncInterrupted, +} + +impl From for SwapBuffersError { + #[cfg(feature = "wayland_frontend")] + #[inline] + fn from(err: GlesError) -> SwapBuffersError { + match err { + x @ GlesError::ShaderCompileError + | x @ GlesError::ProgramLinkError + | x @ GlesError::GLFunctionLoaderError + | x @ GlesError::GLExtensionNotSupported(_) + | x @ GlesError::EGLExtensionNotSupported(_) + | x @ GlesError::GLVersionNotSupported(_) => SwapBuffersError::ContextLost(Box::new(x)), + GlesError::ContextActivationError(err) => err.into(), + x @ GlesError::FramebufferBindingError + | x @ GlesError::BindBufferEGLError(_) + | x @ GlesError::UnknownPixelFormat + | x @ GlesError::UnsupportedPixelFormat(_) + | x @ GlesError::UnsupportedWlPixelFormat(_) + | x @ GlesError::UnsupportedPixelLayout + | x @ GlesError::BufferAccessError(_) + | x @ GlesError::MappingError + | x @ GlesError::UnexpectedSize + | x @ GlesError::UnknownSize + | x @ GlesError::BlitError + | x @ GlesError::CreateShaderObject + | x @ GlesError::UniformTypeMismatch { .. } + | x @ GlesError::UnknownUniform(_) + | x @ GlesError::EGLBufferAccessError(_) + | x @ GlesError::SyncInterrupted => SwapBuffersError::TemporaryFailure(Box::new(x)), + } + } + #[cfg(not(feature = "wayland_frontend"))] + #[inline] + fn from(err: GlesError) -> SwapBuffersError { + match err { + x @ GlesError::ShaderCompileError + | x @ GlesError::ProgramLinkError + | x @ GlesError::GLFunctionLoaderError + | x @ GlesError::GLExtensionNotSupported(_) + | x @ GlesError::EGLExtensionNotSupported(_) + | x @ GlesError::GLVersionNotSupported(_) => SwapBuffersError::ContextLost(Box::new(x)), + GlesError::ContextActivationError(err) => err.into(), + x @ GlesError::FramebufferBindingError + | x @ GlesError::MappingError + | x @ GlesError::UnknownPixelFormat + | x @ GlesError::UnsupportedPixelFormat(_) + | x @ GlesError::UnsupportedPixelLayout + | x @ GlesError::UnexpectedSize + | x @ GlesError::UnknownSize + | x @ GlesError::BlitError + | x @ GlesError::CreateShaderObject + | x @ GlesError::UniformTypeMismatch { .. } + | x @ GlesError::UnknownUniform(_) + | x @ GlesError::BindBufferEGLError(_) + | x @ GlesError::SyncInterrupted => SwapBuffersError::TemporaryFailure(Box::new(x)), + } + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/format.rs b/vendor/smithay/src/backend/renderer/gles/format.rs new file mode 100644 index 0000000..d7cee49 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/format.rs @@ -0,0 +1,74 @@ +//! GL color format conversion helpers + +use super::ffi::{self, types::GLenum}; +use crate::backend::allocator::{ + Fourcc, + format::{get_transparent, has_alpha}, +}; + +/// Returns (internal_format, read_format, type) +pub const fn fourcc_to_gl_formats(value: Fourcc) -> Option<(GLenum, GLenum, GLenum)> { + let Some(value) = (if has_alpha(value) { + Some(value) + } else { + get_transparent(value) + }) else { + return None; // ? not allowed in const fn + }; + + match value { + Fourcc::Abgr8888 => Some((ffi::RGBA8, ffi::RGBA, ffi::UNSIGNED_BYTE)), + Fourcc::Argb8888 => Some((ffi::BGRA_EXT, ffi::BGRA_EXT, ffi::UNSIGNED_BYTE)), + Fourcc::Abgr2101010 => Some((ffi::RGB10_A2, ffi::RGBA, ffi::UNSIGNED_INT_2_10_10_10_REV)), + Fourcc::Abgr16161616f => Some((ffi::RGBA16F, ffi::RGBA, ffi::HALF_FLOAT)), + _ => None, + } +} + +/// Returns the fourcc for a given internal format +pub const fn gl_internal_format_to_fourcc(format: GLenum) -> Option { + match format { + ffi::RGBA | ffi::RGBA8 => Some(Fourcc::Abgr8888), + ffi::BGRA_EXT => Some(Fourcc::Argb8888), + ffi::RGB8 => Some(Fourcc::Bgr888), + ffi::RGB10_A2 => Some(Fourcc::Abgr2101010), + ffi::RGBA16F => Some(Fourcc::Abgr16161616f), + _ => None, + } +} + +/// Returns the fourcc for a given read format and type +pub const fn gl_read_format_to_fourcc(format: GLenum, type_: GLenum) -> Option { + match (format, type_) { + (ffi::RGBA, ffi::UNSIGNED_BYTE) => Some(Fourcc::Abgr8888), + (ffi::BGRA_EXT, ffi::UNSIGNED_BYTE) => Some(Fourcc::Argb8888), + (ffi::RGB, ffi::UNSIGNED_BYTE) => Some(Fourcc::Bgr888), + (ffi::RGBA, ffi::UNSIGNED_INT_2_10_10_10_REV) => Some(Fourcc::Abgr2101010), + (ffi::RGBA, ffi::HALF_FLOAT) => Some(Fourcc::Abgr16161616f), + _ => None, + } +} + +/// Returns a recommended read format and type for a given internal format +pub const fn gl_read_for_internal(format: GLenum) -> Option<(GLenum, GLenum)> { + match format { + ffi::RGBA | ffi::RGBA8 => Some((ffi::RGBA, ffi::UNSIGNED_BYTE)), + ffi::BGRA_EXT => Some((ffi::BGRA_EXT, ffi::UNSIGNED_BYTE)), + ffi::RGB8 => Some((ffi::RGB, ffi::UNSIGNED_BYTE)), + ffi::RGB10_A2 => Some((ffi::RGBA, ffi::UNSIGNED_INT_2_10_10_10_REV)), + ffi::RGBA16F => Some((ffi::RGBA, ffi::HALF_FLOAT)), + _ => None, + } +} + +/// Returns the bits per pixel for a given read format and type +pub const fn gl_bpp(format: GLenum, type_: GLenum) -> Option { + match (format, type_) { + (ffi::RGB, ffi::UNSIGNED_BYTE) => Some(24), + (ffi::RGBA, ffi::UNSIGNED_BYTE) + | (ffi::BGRA_EXT, ffi::UNSIGNED_BYTE) + | (ffi::RGBA, ffi::UNSIGNED_INT_2_10_10_10_REV) => Some(32), + (ffi::RGBA, ffi::HALF_FLOAT) => Some(64), + _ => None, + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/mod.rs b/vendor/smithay/src/backend/renderer/gles/mod.rs new file mode 100644 index 0000000..576b89f --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/mod.rs @@ -0,0 +1,3702 @@ +//! Implementation of the rendering traits using OpenGL ES 2 + +// GL calls are all unsafe, so not very helpful in this module. +#![allow(unsafe_op_in_unsafe_fn)] + +use core::slice; +use glam::{Affine2, Mat3, Vec2}; +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + fmt, + marker::PhantomData, + mem, + os::raw::c_char, + ptr, + rc::Rc, + sync::{ + Arc, Mutex, RwLock, RwLockWriteGuard, TryLockError, + atomic::{AtomicBool, AtomicPtr, Ordering}, + mpsc::{self, Sender}, + }, +}; +use tracing::{Level, debug, error, info, info_span, instrument, span, span::EnteredSpan, trace, warn}; + +pub mod element; +mod error; +pub mod format; +pub mod profiler; +mod shaders; +mod texture; +mod uniform; +mod version; + +pub use error::*; +use format::*; +pub use shaders::*; +pub use texture::*; +pub use uniform::*; + +use crate::{backend::renderer::FrameContext, gpu_span_location}; +use profiler::SpanLocation; + +use self::version::GlVersion; + +use super::{ + Bind, Blit, BlitFrame, Color32F, ContextId, DebugFlags, ExportMem, Frame, ImportDma, ImportMem, + Offscreen, Renderer, RendererSuper, Texture, TextureFilter, TextureMapping, sync::SyncPoint, +}; +use crate::{ + backend::{ + allocator::{ + Buffer, Format, Fourcc, + dmabuf::{Dmabuf, WeakDmabuf}, + format::{FormatSet, get_bpp, get_opaque, has_alpha}, + }, + egl::{ + EGLContext, EGLDevice, EGLSurface, MakeCurrentError, + fence::EGLFence, + ffi::egl::{self as ffi_egl, types::EGLImage}, + }, + }, + utils::{Buffer as BufferCoord, Physical, Rectangle, Size, Transform}, +}; + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use super::ImportEgl; +#[cfg(feature = "wayland_frontend")] +use super::{ImportDmaWl, ImportMemWl}; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use crate::backend::egl::{Format as EGLFormat, display::EGLBufferReader}; +#[cfg(feature = "wayland_frontend")] +use crate::wayland::shm::shm_format_to_fourcc; +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_buffer; + +#[allow(clippy::all, missing_docs, missing_debug_implementations)] +pub mod ffi { + include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs")); +} + +enum CleanupResource { + Texture(ffi::types::GLuint), + FramebufferObject(ffi::types::GLuint), + RenderbufferObject(ffi::types::GLuint), + EGLImage(EGLImage), + Mapping(ffi::types::GLuint, *const std::ffi::c_void), + Program(ffi::types::GLuint), + Sync(ffi::types::GLsync), +} +unsafe impl Send for CleanupResource {} + +#[derive(Debug)] +struct GlesBufferInner { + dmabuf: WeakDmabuf, + image: EGLImage, + rbo: ffi::types::GLuint, + fbo: ffi::types::GLuint, + destruction_callback_sender: Sender, +} + +impl Drop for GlesBufferInner { + fn drop(&mut self) { + let _ = self + .destruction_callback_sender + .send(CleanupResource::FramebufferObject(self.fbo)); + let _ = self + .destruction_callback_sender + .send(CleanupResource::RenderbufferObject(self.rbo)); + let _ = self + .destruction_callback_sender + .send(CleanupResource::EGLImage(self.image)); + } +} + +#[derive(Debug, Clone)] +struct GlesBuffer(Rc); + +/// Offscreen render surface +/// +/// Usually more performant than using a texture as a framebuffer. +/// Can be read out, but not used like a texture otherwise. +#[derive(Debug, Clone)] +pub struct GlesRenderbuffer(Arc); + +#[derive(Debug)] +struct GlesRenderbufferInternal { + rbo: ffi::types::GLuint, + format: ffi::types::GLenum, + has_alpha: bool, + size: Size, + destruction_callback_sender: Sender, +} +unsafe impl Send for GlesRenderbufferInternal {} +unsafe impl Sync for GlesRenderbufferInternal {} + +impl GlesRenderbuffer { + /// Size of the renderbuffer + pub fn size(&self) -> Size { + self.0.size + } + + /// Internal format of the renderbuffer + pub fn format(&self) -> Option { + let fmt = gl_internal_format_to_fourcc(self.0.format); + if self.0.has_alpha { + fmt + } else { + fmt.and_then(get_opaque) + } + } +} + +impl Drop for GlesRenderbufferInternal { + fn drop(&mut self) { + let _ = self + .destruction_callback_sender + .send(CleanupResource::RenderbufferObject(self.rbo)); + } +} + +/// A GL framebuffer +#[derive(Debug)] +pub struct GlesTarget<'a>(GlesTargetInternal<'a>); + +#[derive(Debug)] +enum GlesTargetInternal<'a> { + Image { + // TODO: Ideally we would be able to share the texture between renderers with shared EGLContexts though. + // But we definitely don't want to add user data to a dmabuf to facilitate this. Maybe use the EGLContexts userdata for storing the buffers? + buf: GlesBuffer, + dmabuf: &'a mut Dmabuf, + }, + Surface { + surface: &'a mut EGLSurface, + }, + Texture { + texture: GlesTexture, + sync_lock: RwLockWriteGuard<'a, TextureSync>, + fbo: ffi::types::GLuint, + destruction_callback_sender: Sender, + }, + Renderbuffer { + buf: &'a mut GlesRenderbuffer, + fbo: ffi::types::GLuint, + }, +} + +impl Texture for GlesTarget<'_> { + fn height(&self) -> u32 { + self.size().h as u32 + } + + fn width(&self) -> u32 { + self.size().w as u32 + } + + fn size(&self) -> Size { + match &self.0 { + GlesTargetInternal::Image { dmabuf, .. } => dmabuf.size(), + GlesTargetInternal::Surface { surface } => surface + .get_size() + .expect("a bound EGLSurface needs to have a size") + .to_logical(1) + .to_buffer(1, Transform::Normal), + GlesTargetInternal::Texture { texture, .. } => texture.size(), + GlesTargetInternal::Renderbuffer { buf, .. } => buf.size(), + } + } + + fn format(&self) -> Option { + let (gl_format, _) = self.0.format()?; + gl_internal_format_to_fourcc(gl_format) + } +} + +impl GlesTargetInternal<'_> { + fn format(&self) -> Option<(ffi::types::GLenum, bool)> { + match self { + GlesTargetInternal::Image { dmabuf, .. } => { + let format = crate::backend::allocator::Buffer::format(*dmabuf).code; + let has_alpha = has_alpha(format); + let (format, _, _) = fourcc_to_gl_formats(format)?; + + Some((format, has_alpha)) + } + GlesTargetInternal::Surface { surface, .. } => { + let format = surface.pixel_format(); + let format = match (format.color_bits, format.alpha_bits) { + (24, 8) => ffi::RGB8, + (30, 2) => ffi::RGB10_A2, + (48, 16) => ffi::RGB16F, + _ => return None, + }; + + Some((format, true)) + } + GlesTargetInternal::Texture { texture, .. } => Some((texture.0.format?, texture.0.has_alpha)), + GlesTargetInternal::Renderbuffer { buf, .. } => Some((buf.0.format, buf.0.has_alpha)), + } + } + + #[profiling::function] + fn make_current(&self, gl: &ffi::Gles2, egl: &EGLContext) -> Result<(), MakeCurrentError> { + unsafe { + if let GlesTargetInternal::Surface { surface, .. } = self { + egl.make_current_with_surface(surface)?; + gl.BindFramebuffer(ffi::FRAMEBUFFER, 0); + } else { + egl.make_current()?; + match self { + GlesTargetInternal::Image { buf, .. } => gl.BindFramebuffer(ffi::FRAMEBUFFER, buf.0.fbo), + GlesTargetInternal::Texture { fbo, .. } => gl.BindFramebuffer(ffi::FRAMEBUFFER, *fbo), + GlesTargetInternal::Renderbuffer { fbo, .. } => { + gl.BindFramebuffer(ffi::FRAMEBUFFER, *fbo) + } + _ => unreachable!(), + } + } + Ok(()) + } + } +} + +impl Drop for GlesTargetInternal<'_> { + fn drop(&mut self) { + match self { + GlesTargetInternal::Texture { + fbo, + destruction_callback_sender, + .. + } => { + let _ = destruction_callback_sender.send(CleanupResource::FramebufferObject(*fbo)); + } + GlesTargetInternal::Renderbuffer { buf, fbo, .. } => { + let _ = buf + .0 + .destruction_callback_sender + .send(CleanupResource::FramebufferObject(*fbo)); + } + _ => {} + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// Capabilities of the [`GlesRenderer`] +pub enum Capability { + /// GlesRenderer supports Instancing for render optimizations + Instancing, + /// GlesRenderer supports blitting between framebuffers + Blit, + /// GlesRenderer supports 10 bit formats + _10Bit, + /// GlesRenderer supports creating of Renderbuffers with usable formats + Renderbuffer, + /// GlesRenderer supports fencing + Fencing, + /// GlesRenderer supports fencing and exporting it to EGL + ExportFence, + /// GlesRenderer supports GL debug + Debug, +} + +/// GL resources need to be destroyed with a context active on the current thread, +/// so `Drop` impls don't directly destroy objects, and instead send messages +/// to free resources on periodic calls to `cleanup`. +/// +/// Stored as `EGLContext` user data, so shared contexts can share cleanup. And +/// destroying one renderer will not leak resources, but allow another renderer +/// with a shared context to clean them up. +struct GlesCleanup { + receiver: Mutex>, + sender: mpsc::Sender, +} + +impl Default for GlesCleanup { + fn default() -> Self { + let (sender, receiver) = mpsc::channel(); + Self { + receiver: Mutex::new(receiver), + sender, + } + } +} + +impl GlesCleanup { + fn cleanup(&self, egl: &EGLContext, gl: &ffi::Gles2) -> Result<(), MakeCurrentError> { + let receiver = match self.receiver.try_lock() { + Ok(receiver) => receiver, + Err(TryLockError::Poisoned(err)) => { + self.receiver.clear_poison(); + err.into_inner() + } + Err(TryLockError::WouldBlock) => { + // Another thread is running `cleanup`, so just return + return Ok(()); + } + }; + let Ok(first) = receiver.try_recv() else { + return Ok(()); + }; + // Only activate the context once there is something to destroy: activating an idle + // GPU's context wakes it from runtime suspend. + if !egl.is_current() { + if let Err(err) = unsafe { egl.make_current() } { + let _ = self.sender.send(first); + return Err(err); + } + } + for resource in std::iter::once(first).chain(receiver.try_iter()) { + match resource { + CleanupResource::Texture(texture) => unsafe { + gl.DeleteTextures(1, &texture); + }, + CleanupResource::EGLImage(image) => unsafe { + ffi_egl::DestroyImageKHR(**egl.display().get_display_handle(), image); + }, + CleanupResource::FramebufferObject(fbo) => unsafe { + gl.DeleteFramebuffers(1, &fbo); + }, + CleanupResource::RenderbufferObject(rbo) => unsafe { + gl.DeleteRenderbuffers(1, &rbo); + }, + CleanupResource::Mapping(pbo, mapping) => unsafe { + if !mapping.is_null() { + gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, pbo); + gl.UnmapBuffer(ffi::PIXEL_PACK_BUFFER); + gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0); + } + gl.DeleteBuffers(1, &pbo); + }, + CleanupResource::Program(program) => unsafe { + gl.DeleteProgram(program); + }, + CleanupResource::Sync(sync) => unsafe { + gl.DeleteSync(sync); + }, + } + } + Ok(()) + } +} + +/// A renderer utilizing OpenGL ES +pub struct GlesRenderer { + // state + min_filter: TextureFilter, + max_filter: TextureFilter, + debug_flags: DebugFlags, + + // internals + egl: EGLContext, + #[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] + egl_reader: Option, + gl: ffi::Gles2, + + // optionals + gl_version: GlVersion, + pub(crate) extensions: Vec, + is_software: bool, + capabilities: Vec, + + // shaders + tex_program: GlesTexProgram, + solid_program: GlesSolidProgram, + + // caches + buffers: Vec, + dmabuf_cache: HashMap, + vbos: [ffi::types::GLuint; 2], + vertices: Vec, + non_opaque_damage: Vec>, + opaque_damage: Vec>, + + // markers + _not_send: PhantomData<*mut ()>, + + // debug + span: tracing::Span, + gl_debug_span: Option<*mut tracing::Span>, + + // profiling + profiler: profiler::GpuProfiler, +} + +/// Handle to the currently rendered frame during [`GlesRenderer::render`](Renderer::render). +/// +/// Leaking this frame will cause a variety of problems: +/// - It might prevent the frame from synchronizing the rendered framebuffer causing glitches. +/// - Depending on the bound target this can deadlock, if the same target is used later in any way. +/// - Additionally parts of the GL state might not be reset correctly, causing unexpected results for later render commands. +/// - The internal GL context and framebuffer will remain valid, no re-creation will be necessary. +pub struct GlesFrame<'frame, 'buffer> { + renderer: &'frame mut GlesRenderer, + target: &'frame mut GlesTarget<'buffer>, + current_projection: Mat3, + transform: Transform, + size: Size, + tex_program_override: Option<(GlesTexProgram, Vec>)>, + finished: AtomicBool, + + span: EnteredSpan, + + gpu_span: Option, +} + +impl fmt::Debug for GlesFrame<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GlesFrame") + .field("renderer", &self.renderer) + .field("target", &self.target) + .field("current_projection", &self.current_projection) + .field("transform", &self.transform) + .field("tex_program_override", &self.tex_program_override) + .field("size", &self.size) + .field("finished", &self.finished) + .finish_non_exhaustive() + } +} + +impl fmt::Debug for GlesRenderer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GlesRenderer") + .field("buffers", &self.buffers) + .field("extensions", &self.extensions) + .field("capabilities", &self.capabilities) + .field("tex_program", &self.tex_program) + .field("solid_program", &self.solid_program) + .field("dmabuf_cache", &self.dmabuf_cache) + .field("egl", &self.egl) + .field("gl_version", &self.gl_version) + // ffi::Gles does not implement Debug + .field("vbos", &self.vbos) + .field("min_filter", &self.min_filter) + .field("max_filter", &self.max_filter) + .finish() + } +} + +extern "system" fn gl_debug_log( + _source: ffi::types::GLenum, + gltype: ffi::types::GLenum, + _id: ffi::types::GLuint, + _severity: ffi::types::GLenum, + _length: ffi::types::GLsizei, + message: *const ffi::types::GLchar, + user_param: *mut std::ffi::c_void, +) { + let _ = std::panic::catch_unwind(move || unsafe { + let span = &mut *(user_param as *mut tracing::Span); + let _guard = span.enter(); + let msg = CStr::from_ptr(message); + let message_utf8 = msg.to_string_lossy(); + match gltype { + ffi::DEBUG_TYPE_ERROR | ffi::DEBUG_TYPE_UNDEFINED_BEHAVIOR => { + error!("[GL] {}", message_utf8) + } + ffi::DEBUG_TYPE_DEPRECATED_BEHAVIOR => warn!("[GL] {}", message_utf8), + _ => debug!("[GL] {}", message_utf8), + }; + }); +} + +impl GlesRenderer { + /// Get the supported [`Capabilities`](Capability) of the renderer + /// + /// # Safety + /// + /// This operation will cause undefined behavior if the given EGLContext is active in another thread. + pub unsafe fn supported_capabilities(context: &EGLContext) -> Result, GlesError> { + context.make_current()?; + + let gl = ffi::Gles2::load_with(|s| crate::backend::egl::get_proc_address(s) as *const _); + let ext_ptr = gl.GetString(ffi::EXTENSIONS) as *const c_char; + if ext_ptr.is_null() { + return Err(GlesError::GLFunctionLoaderError); + } + + let exts = { + let p = CStr::from_ptr(ext_ptr); + let list = String::from_utf8(p.to_bytes().to_vec()).unwrap_or_else(|_| String::new()); + list.split(' ').map(|e| e.to_string()).collect::>() + }; + + let gl_version = version::GlVersion::try_from(&gl).unwrap_or_else(|_| { + warn!("Failed to detect GLES version, defaulting to 2.0"); + version::GLES_2_0 + }); + + let mut capabilities = Vec::new(); + // required for more optimized rendering, otherwise we render in batches + if gl_version >= version::GLES_3_0 + || (exts.iter().any(|ext| ext == "GL_EXT_instanced_arrays") + && exts.iter().any(|ext| ext == "GL_EXT_draw_instanced")) + { + capabilities.push(Capability::Instancing); + debug!("Instancing is supported"); + } + // required to use 8-bit color formats in renderbuffers, we don't deal with anything lower as a render target + if gl_version >= version::GLES_3_0 || exts.iter().any(|ext| ext == "GL_OES_rgb8_rgba8") { + capabilities.push(Capability::Renderbuffer); + debug!("Rgba8 Renderbuffers are supported"); + } + // required for blit operations + if gl_version >= version::GLES_3_0 { + capabilities.push(Capability::Blit); + debug!("Blitting is supported"); + capabilities.push(Capability::_10Bit); + debug!("10-bit formats are supported"); + capabilities.push(Capability::Fencing); + debug!("Fencing is supported"); + } + + if exts.iter().any(|ext| ext == "GL_OES_EGL_sync") { + debug!("EGL Fencing is supported"); + capabilities.push(Capability::ExportFence); + } + + if exts.iter().any(|ext| ext == "GL_KHR_debug") { + capabilities.push(Capability::Debug); + debug!("GL Debug is supported"); + } + + Ok(capabilities) + } + + /// Creates a new OpenGL ES renderer from a given [`EGLContext`] + /// with all [`supported capabilities`](Self::supported_capabilities). + /// + /// # Safety + /// + /// This operation will cause undefined behavior if the given EGLContext is active in another thread. + /// + /// See: [`with_capabilities`](Self::with_capabilities) for more information + pub unsafe fn new(context: EGLContext) -> Result { + let supported_capabilities = Self::supported_capabilities(&context)?; + Self::with_capabilities(context, supported_capabilities) + } + + /// Creates a new OpenGL ES renderer from a given [`EGLContext`] + /// with the specified [`Capabilities`](Capability). If a requested [`Capability`] is not supported an + /// error will be returned. + /// + /// # Safety + /// + /// This operation will cause undefined behavior if the given EGLContext is active in another thread. + /// + /// # Implementation details + /// + /// - Texture handles created by the resulting renderer are valid for every rendered created with an + /// `EGLContext` shared with the given one (see `EGLContext::new_shared`) and can be used on + /// any of these renderers. + /// - This renderer has no default framebuffer, use `Bind::bind` before rendering. + /// - Shm buffers can be released after a successful import, without the texture handle becoming invalid. + /// - Texture filtering starts with Linear-downscaling and Linear-upscaling. + /// - If OpenGL ES 3.0 is not available and the underlying [`EGLContext`] is shared, memory textures + /// will insert `glFinish`-calls into the pipeline. Consider not sharing contexts, if OpenGL ES 3 isn't available. + pub unsafe fn with_capabilities( + context: EGLContext, + capabilities: impl IntoIterator, + ) -> Result { + let span = info_span!(parent: &context.span, "renderer_gles2"); + let _guard = span.enter(); + + context.make_current()?; + + // TODO: check for angle as well? + let is_software = EGLDevice::device_for_display(context.display()) + .map(|dev| dev.is_software()) + .unwrap_or(false); + + let supported_capabilities = Self::supported_capabilities(&context)?; + let requested_capabilities = capabilities.into_iter().collect::>(); + + let unsupported_capabilities = requested_capabilities + .iter() + .copied() + .filter(|c| !supported_capabilities.contains(c)) + .collect::>(); + + if let Some(missing_capability) = unsupported_capabilities.first() { + let err = match missing_capability { + Capability::Instancing => { + GlesError::GLExtensionNotSupported(&["GL_EXT_instanced_arrays", "GL_EXT_draw_instanced"]) + } + Capability::Blit | Capability::_10Bit | Capability::Fencing => { + GlesError::GLVersionNotSupported(version::GLES_3_0) + } + Capability::Renderbuffer => GlesError::GLExtensionNotSupported(&["GL_OES_rgb8_rgba8"]), + Capability::ExportFence => GlesError::GLExtensionNotSupported(&["GL_OES_EGL_sync"]), + Capability::Debug => GlesError::GLExtensionNotSupported(&["GL_KHR_debug"]), + }; + return Err(err); + }; + + let (gl, gl_version, exts, capabilities, gl_debug_span) = { + let gl = ffi::Gles2::load_with(|s| crate::backend::egl::get_proc_address(s) as *const _); + let ext_ptr = gl.GetString(ffi::EXTENSIONS) as *const c_char; + if ext_ptr.is_null() { + return Err(GlesError::GLFunctionLoaderError); + } + + let exts = { + let p = CStr::from_ptr(ext_ptr); + let list = String::from_utf8(p.to_bytes().to_vec()).unwrap_or_else(|_| String::new()); + list.split(' ').map(|e| e.to_string()).collect::>() + }; + + info!("Initializing OpenGL ES Renderer"); + info!( + "GL Version: {:?}", + CStr::from_ptr(gl.GetString(ffi::VERSION) as *const c_char) + ); + info!( + "GL Vendor: {:?}", + CStr::from_ptr(gl.GetString(ffi::VENDOR) as *const c_char) + ); + info!( + "GL Renderer: {:?}", + CStr::from_ptr(gl.GetString(ffi::RENDERER) as *const c_char) + ); + info!("Supported GL Extensions: {:?}", exts); + + let gl_version = version::GlVersion::try_from(&gl).unwrap_or_else(|_| { + warn!("Failed to detect GLES version, defaulting to 2.0"); + version::GLES_2_0 + }); + + // required for the mandatory wl_shm formats + if !exts.iter().any(|ext| ext == "GL_EXT_texture_format_BGRA8888") { + return Err(GlesError::GLExtensionNotSupported(&[ + "GL_EXT_texture_format_BGRA8888", + ])); + } + + // required for buffers without linear memory layout + if gl_version < version::GLES_3_0 && !exts.iter().any(|ext| ext == "GL_EXT_unpack_subimage") { + return Err(GlesError::GLExtensionNotSupported(&["GL_EXT_unpack_subimage"])); + } + + let gl_debug_span = if requested_capabilities.contains(&Capability::Debug) { + gl.Enable(ffi::DEBUG_OUTPUT); + gl.Enable(ffi::DEBUG_OUTPUT_SYNCHRONOUS); + let span = Box::into_raw(Box::new(span.clone())); + gl.DebugMessageCallback(Some(gl_debug_log), span as *mut _); + Some(span) + } else { + None + }; + + (gl, gl_version, exts, requested_capabilities, gl_debug_span) + }; + + let gles_cleanup = context.user_data().get_or_insert_threadsafe(GlesCleanup::default); + + let tex_program = texture_program(&gl, shaders::FRAGMENT_SHADER, &[], gles_cleanup.sender.clone())?; + let solid_program = solid_program(&gl)?; + + // Initialize vertices based on drawing methodology. + let vertices: &[ffi::types::GLfloat] = if capabilities.contains(&Capability::Instancing) { + &INSTANCED_VERTS + } else { + &TRIANGLE_VERTS + }; + + let mut vbos = [0; 2]; + gl.GenBuffers(vbos.len() as i32, vbos.as_mut_ptr()); + gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[0]); + gl.BufferData( + ffi::ARRAY_BUFFER, + std::mem::size_of_val(vertices) as isize, + vertices.as_ptr() as *const _, + ffi::STATIC_DRAW, + ); + gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[1]); + gl.BufferData( + ffi::ARRAY_BUFFER, + (std::mem::size_of::() * OUTPUT_VERTS.len()) as isize, + OUTPUT_VERTS.as_ptr() as *const _, + ffi::STATIC_DRAW, + ); + gl.BindBuffer(ffi::ARRAY_BUFFER, 0); + + context + .user_data() + .insert_if_missing_threadsafe(ContextId::::new); + + drop(_guard); + + let profiler = profiler::GpuProfiler::new(&gl, &exts); + + let renderer = GlesRenderer { + gl, + egl: context, + #[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] + egl_reader: None, + + extensions: exts, + is_software, + gl_version, + capabilities, + + tex_program, + solid_program, + vbos, + min_filter: TextureFilter::Linear, + max_filter: TextureFilter::Linear, + + buffers: Vec::new(), + dmabuf_cache: std::collections::HashMap::new(), + vertices: Vec::with_capacity(6 * 16), + non_opaque_damage: Vec::with_capacity(16), + opaque_damage: Vec::with_capacity(16), + + debug_flags: DebugFlags::empty(), + _not_send: PhantomData, + span, + gl_debug_span, + + profiler, + }; + renderer.egl.unbind()?; + Ok(renderer) + } + + fn bind_texture<'a>(&mut self, texture: &'a GlesTexture) -> Result, GlesError> { + unsafe { + self.egl.make_current()?; + } + + let bind = || { + let mut sync_lock = texture.0.sync.write().unwrap(); + let mut fbo = 0; + unsafe { + sync_lock.wait_for_all(&self.gl); + self.gl.GenFramebuffers(1, &mut fbo as *mut _); + self.gl.BindFramebuffer(ffi::FRAMEBUFFER, fbo); + + // glFramebufferTexture2D only accepts FRAMEBUFFER on GLES 2.0, + // READ_FRAMEBUFFER and DRAW_FRAMEBUFFER need GLES 3.0 and higher. + // FRAMEBUFFER equals DRAW_FRAMEBUFFER on GLES 3.0, so the extra + // READ attach below keeps the blit read path covered. + self.gl.FramebufferTexture2D( + ffi::FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::TEXTURE_2D, + texture.0.texture, + 0, + ); + + if self.gl_version >= version::GLES_3_0 { + self.gl.FramebufferTexture2D( + ffi::READ_FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::TEXTURE_2D, + texture.0.texture, + 0, + ); + } + + let status = self.gl.CheckFramebufferStatus(ffi::FRAMEBUFFER); + self.gl.BindFramebuffer(ffi::FRAMEBUFFER, 0); + + if status != ffi::FRAMEBUFFER_COMPLETE { + self.gl.DeleteFramebuffers(1, &mut fbo as *mut _); + return Err(GlesError::FramebufferBindingError); + } + } + + Ok(GlesTarget(GlesTargetInternal::Texture { + texture: texture.clone(), + sync_lock, + destruction_callback_sender: self.gles_cleanup().sender.clone(), + fbo, + })) + }; + + bind().inspect_err(|_| { + if let Err(err) = self.unbind() { + self.span.in_scope(|| warn!(?err, "Failed to unbind on err")); + } + }) + } + + #[profiling::function] + fn unbind(&mut self) -> Result<(), GlesError> { + unsafe { + self.egl.make_current()?; + } + unsafe { self.gl.BindFramebuffer(ffi::FRAMEBUFFER, 0) }; + self.cleanup()?; + self.egl.unbind()?; + Ok(()) + } + + fn gles_cleanup(&self) -> &GlesCleanup { + self.egl_context().user_data().get().unwrap() + } + + #[profiling::function] + fn cleanup(&mut self) -> Result<(), GlesError> { + self.dmabuf_cache.retain(|entry, _tex| !entry.is_gone()); + self.buffers.retain(|buffer| !buffer.0.dmabuf.is_gone()); + self.gles_cleanup().cleanup(&self.egl, &self.gl)?; + Ok(()) + } + + /// Returns the supported [`Capabilities`](Capability) of this renderer. + pub fn capabilities(&self) -> &[Capability] { + &self.capabilities + } + + /// Returns whether the underlying EGLContext is known to be a software renderer. + pub fn is_software(&self) -> bool { + self.is_software + } + + fn export_sync_point(&self) -> Option { + if self.capabilities.contains(&Capability::ExportFence) { + if let Ok(fence) = EGLFence::create(self.egl.display()) { + unsafe { + self.gl.Flush(); + } + return Some(SyncPoint::from(fence)); + } + } + None + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportMemWl for GlesRenderer { + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_shm_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result { + use crate::wayland::shm::with_buffer_contents; + + // why not store a `GlesTexture`? because the user might do so. + // this is guaranteed a non-public internal type, so we are good. + type CacheMap = HashMap, Arc>; + static SHM_FORMAT_CHANGE_LOGS: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + + let mut surface_lock = surface.as_ref().map(|surface_data| { + surface_data + .data_map + .get_or_insert_threadsafe(|| Arc::new(Mutex::new(CacheMap::new()))) + .lock() + .unwrap() + }); + + with_buffer_contents(buffer, |ptr, len, data| { + let offset = data.offset; + let width = data.width; + let height = data.height; + let stride = data.stride; + let fourcc = + shm_format_to_fourcc(data.format).ok_or(GlesError::UnsupportedWlPixelFormat(data.format))?; + + if self.gl_version.major >= 3 { + if !SUPPORTED_MEM_FORMATS_3.contains(&fourcc) { + return Err(GlesError::UnsupportedWlPixelFormat(data.format)); + } + } else if !SUPPORTED_MEM_FORMATS_2.contains(&fourcc) { + return Err(GlesError::UnsupportedWlPixelFormat(data.format)); + } + + let has_alpha = has_alpha(fourcc); + let (mut internal_format, read_format, type_) = + fourcc_to_gl_formats(fourcc).ok_or(GlesError::UnsupportedWlPixelFormat(data.format))?; + if self.gl_version.major == 2 { + // es 2.0 doesn't define sized variants + internal_format = match internal_format { + ffi::BGRA_EXT => ffi::BGRA_EXT, + ffi::RGBA8 => ffi::RGBA, + ffi::RGB8 => ffi::RGB, + _ => unreachable!(), + }; + } + + // number of bytes per pixel + let pixelsize = gl_bpp(read_format, type_).expect("We check the format before") / 8; + // ensure consistency, the SHM handler of smithay should ensure this + assert!((offset + (height - 1) * stride + width * pixelsize as i32) as usize <= len); + + let mut upload_full = false; + + unsafe { + self.egl.make_current()?; + } + + let id = self.context_id(); + let texture = GlesTexture( + surface_lock + .as_ref() + .and_then(|cache| cache.get(&id).cloned()) + .filter(|texture| { + let format_matches = + texture.format == Some(internal_format) && texture.has_alpha == has_alpha; + if !format_matches + && tracing::enabled!(tracing::Level::DEBUG) + && SHM_FORMAT_CHANGE_LOGS + .fetch_update( + std::sync::atomic::Ordering::Relaxed, + std::sync::atomic::Ordering::Relaxed, + |count| (count < 16).then_some(count + 1), + ) + .is_ok() + { + tracing::debug!( + old_width = texture.size.w, + old_height = texture.size.h, + new_width = width, + new_height = height, + old_internal_format = texture.format.unwrap_or(0), + new_internal_format = internal_format, + old_has_alpha = texture.has_alpha, + new_has_alpha = has_alpha, + "SHM texture cache format changed" + ); + } + texture.size == (width, height).into() && format_matches + }) + .unwrap_or_else(|| { + let mut tex = 0; + unsafe { self.gl.GenTextures(1, &mut tex) }; + // new texture, upload in full + upload_full = true; + let new = Arc::new(GlesTextureInternal { + texture: tex, + sync: RwLock::default(), + format: Some(internal_format), + has_alpha, + is_external: false, + y_inverted: false, + size: (width, height).into(), + egl_images: None, + destruction_callback_sender: self.gles_cleanup().sender.clone(), + }); + if let Some(cache) = surface_lock.as_mut() { + cache.insert(id, new.clone()); + } + new + }), + ); + + let mut sync_lock = texture.0.sync.write().unwrap(); + unsafe { + sync_lock.wait_for_all(&self.gl); + self.gl.BindTexture(ffi::TEXTURE_2D, texture.0.texture); + self.gl + .TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::CLAMP_TO_EDGE as i32); + self.gl + .TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::CLAMP_TO_EDGE as i32); + self.gl + .PixelStorei(ffi::UNPACK_ROW_LENGTH, stride / pixelsize as i32); + + if upload_full || damage.is_empty() { + trace!("Uploading shm texture"); + self.gl.TexImage2D( + ffi::TEXTURE_2D, + 0, + internal_format as i32, + width, + height, + 0, + read_format, + type_, + ptr.offset(offset as isize) as *const _, + ); + } else { + let buffer_rect = Rectangle::from_size(Size::from((width, height))); + for region in damage.iter().filter_map(|r| r.intersection(buffer_rect)) { + trace!("Uploading partial shm texture"); + self.gl.PixelStorei(ffi::UNPACK_SKIP_PIXELS, region.loc.x); + self.gl.PixelStorei(ffi::UNPACK_SKIP_ROWS, region.loc.y); + self.gl.TexSubImage2D( + ffi::TEXTURE_2D, + 0, + region.loc.x, + region.loc.y, + region.size.w, + region.size.h, + read_format, + type_, + ptr.offset(offset as isize) as *const _, + ); + self.gl.PixelStorei(ffi::UNPACK_SKIP_PIXELS, 0); + self.gl.PixelStorei(ffi::UNPACK_SKIP_ROWS, 0); + } + } + + self.gl.PixelStorei(ffi::UNPACK_ROW_LENGTH, 0); + self.gl.BindTexture(ffi::TEXTURE_2D, 0); + + if self.capabilities.contains(&Capability::Fencing) { + sync_lock.update_write(&self.gl); + } else if self.egl.is_shared() { + self.gl.Finish(); + } + } + std::mem::drop(sync_lock); + + Ok(texture) + }) + .map_err(GlesError::BufferAccessError)? + } +} + +const SUPPORTED_MEM_FORMATS_2: &[Fourcc] = &[ + Fourcc::Abgr8888, + Fourcc::Xbgr8888, + Fourcc::Argb8888, + Fourcc::Xrgb8888, +]; +const SUPPORTED_MEM_FORMATS_3: &[Fourcc] = &[ + Fourcc::Abgr8888, + Fourcc::Xbgr8888, + Fourcc::Argb8888, + Fourcc::Xrgb8888, + Fourcc::Abgr2101010, + Fourcc::Xbgr2101010, + Fourcc::Abgr16161616f, + Fourcc::Xbgr16161616f, +]; + +impl ImportMem for GlesRenderer { + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_memory( + &mut self, + data: &[u8], + format: Fourcc, + size: Size, + flipped: bool, + ) -> Result { + if data.len() + < (size.w * size.h) as usize + * (get_bpp(format).ok_or(GlesError::UnsupportedPixelFormat(format))? / 8) + { + return Err(GlesError::UnexpectedSize); + } + + if self.gl_version.major >= 3 { + if !SUPPORTED_MEM_FORMATS_3.contains(&format) { + return Err(GlesError::UnsupportedPixelFormat(format)); + } + } else if !SUPPORTED_MEM_FORMATS_2.contains(&format) { + return Err(GlesError::UnsupportedPixelFormat(format)); + } + + let has_alpha = has_alpha(format); + let (mut internal, format, layout) = + fourcc_to_gl_formats(format).expect("We check the format before"); + if self.gl_version.major == 2 { + // es 2.0 doesn't define sized variants + internal = match internal { + ffi::RGBA8 => ffi::RGBA, + ffi::RGB8 => ffi::RGB, + ffi::BGRA_EXT => ffi::BGRA_EXT, + _ => unreachable!(), + }; + } + + let texture = GlesTexture(Arc::new({ + let mut tex = 0; + unsafe { + self.egl.make_current()?; + self.gl.GenTextures(1, &mut tex); + self.gl.BindTexture(ffi::TEXTURE_2D, tex); + self.gl + .TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::CLAMP_TO_EDGE as i32); + self.gl + .TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::CLAMP_TO_EDGE as i32); + self.gl.TexImage2D( + ffi::TEXTURE_2D, + 0, + internal as i32, + size.w, + size.h, + 0, + format, + layout, + data.as_ptr() as *const _, + ); + self.gl.BindTexture(ffi::TEXTURE_2D, 0); + } + + let mut sync = RwLock::::default(); + if self.capabilities.contains(&Capability::Fencing) { + sync.get_mut().unwrap().update_write(&self.gl); + } else if self.egl.is_shared() { + unsafe { + self.gl.Finish(); + } + }; + + // new texture, upload in full + GlesTextureInternal { + texture: tex, + sync, + format: Some(internal), + has_alpha, + is_external: false, + y_inverted: flipped, + size, + egl_images: None, + destruction_callback_sender: self.gles_cleanup().sender.clone(), + } + })); + + Ok(texture) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn update_memory( + &mut self, + texture: &Self::TextureId, + data: &[u8], + region: Rectangle, + ) -> Result<(), Self::Error> { + if texture.0.format.is_none() { + return Err(GlesError::UnknownPixelFormat); + } + if texture.0.is_external { + return Err(GlesError::UnsupportedPixelLayout); + } + let (read_format, type_) = gl_read_for_internal(texture.0.format.expect("We check that before")) + .ok_or(GlesError::UnknownPixelFormat)?; + + if data.len() + < (region.size.w * region.size.h) as usize + * (gl_bpp(read_format, type_).ok_or(GlesError::UnknownPixelFormat)? / 8) + { + return Err(GlesError::UnexpectedSize); + } + + let mut sync_lock = texture.0.sync.write().unwrap(); + unsafe { + self.egl.make_current()?; + sync_lock.wait_for_all(&self.gl); + self.gl.BindTexture(ffi::TEXTURE_2D, texture.0.texture); + self.gl + .TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::CLAMP_TO_EDGE as i32); + self.gl + .TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::CLAMP_TO_EDGE as i32); + self.gl.PixelStorei(ffi::UNPACK_ROW_LENGTH, texture.0.size.w); + self.gl.PixelStorei(ffi::UNPACK_SKIP_PIXELS, region.loc.x); + self.gl.PixelStorei(ffi::UNPACK_SKIP_ROWS, region.loc.y); + self.gl.TexSubImage2D( + ffi::TEXTURE_2D, + 0, + region.loc.x, + region.loc.y, + region.size.w, + region.size.h, + read_format, + type_, + data.as_ptr() as *const _, + ); + self.gl.PixelStorei(ffi::UNPACK_ROW_LENGTH, 0); + self.gl.PixelStorei(ffi::UNPACK_SKIP_PIXELS, 0); + self.gl.PixelStorei(ffi::UNPACK_SKIP_ROWS, 0); + self.gl.BindTexture(ffi::TEXTURE_2D, 0); + + if self.capabilities.contains(&Capability::Fencing) { + sync_lock.update_write(&self.gl); + } else if self.egl.is_shared() { + self.gl.Finish(); + } + } + + Ok(()) + } + + fn mem_formats(&self) -> Box> { + if self.gl_version.major >= 3 { + Box::new(SUPPORTED_MEM_FORMATS_3.iter().copied()) + } else { + Box::new(SUPPORTED_MEM_FORMATS_2.iter().copied()) + } + } +} + +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +impl ImportEgl for GlesRenderer { + fn bind_wl_display( + &mut self, + display: &wayland_server::DisplayHandle, + ) -> Result<(), crate::backend::egl::Error> { + self.egl_reader = Some(self.egl.display().bind_wl_display(display)?); + Ok(()) + } + + fn unbind_wl_display(&mut self) { + self.egl_reader = None; + } + + fn egl_reader(&self) -> Option<&EGLBufferReader> { + self.egl_reader.as_ref() + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_egl_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + _surface: Option<&crate::wayland::compositor::SurfaceData>, + _damage: &[Rectangle], + ) -> Result { + if !self.extensions.iter().any(|ext| ext == "GL_OES_EGL_image") { + return Err(GlesError::GLExtensionNotSupported(&["GL_OES_EGL_image"])); + } + + if self.egl_reader().is_none() { + return Err(GlesError::EGLBufferAccessError( + crate::backend::egl::BufferAccessError::NotManaged(crate::backend::egl::EGLError::BadDisplay), + )); + } + + // We can not use the caching logic for textures here as the + // egl buffers a potentially managed external which will fail the + // clean up check if the buffer is still alive. For wl_drm the + // is_alive check will always return true and the cache entry + // will never be cleaned up. + let egl = self + .egl_reader + .as_ref() + .unwrap() + .egl_buffer_contents(buffer) + .map_err(GlesError::EGLBufferAccessError)?; + + let tex = self.import_egl_image(egl.image(0).unwrap(), egl.format == EGLFormat::External, None)?; + + let texture = GlesTexture(Arc::new(GlesTextureInternal { + texture: tex, + sync: RwLock::default(), + format: match egl.format { + EGLFormat::RGB | EGLFormat::RGBA => Some(ffi::RGBA8), + EGLFormat::External => None, + _ => unreachable!("EGLBuffer currently does not expose multi-planar buffers to us"), + }, + has_alpha: !matches!(egl.format, EGLFormat::RGB), + is_external: egl.format == EGLFormat::External, + y_inverted: egl.y_inverted, + size: egl.size, + egl_images: Some(egl.into_images()), + destruction_callback_sender: self.gles_cleanup().sender.clone(), + })); + + Ok(texture) + } +} + +impl ImportDma for GlesRenderer { + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_dmabuf( + &mut self, + buffer: &Dmabuf, + _damage: Option<&[Rectangle]>, + ) -> Result { + use crate::backend::allocator::Buffer; + if !self.extensions.iter().any(|ext| ext == "GL_OES_EGL_image") { + return Err(GlesError::GLExtensionNotSupported(&["GL_OES_EGL_image"])); + } + + self.existing_dmabuf_texture(buffer)?.map(Ok).unwrap_or_else(|| { + let is_external = !self.egl.dmabuf_render_formats().contains(&buffer.format()); + let image = self + .egl + .display() + .create_image_from_dmabuf(buffer) + .map_err(GlesError::BindBufferEGLError)?; + + let tex = match self.import_egl_image(image, is_external, None) { + Ok(tex) => tex, + Err(err) => { + unsafe { + ffi_egl::DestroyImageKHR(**self.egl.display().get_display_handle(), image); + } + return Err(err); + } + }; + let format = fourcc_to_gl_formats(buffer.format().code) + .map(|(internal, _, _)| internal) + .unwrap_or(ffi::RGBA8); + let has_alpha = has_alpha(buffer.format().code); + let texture = GlesTexture(Arc::new(GlesTextureInternal { + texture: tex, + sync: RwLock::default(), + format: Some(format), + has_alpha, + is_external, + y_inverted: buffer.y_inverted(), + size: buffer.size(), + egl_images: Some(vec![image]), + destruction_callback_sender: self.gles_cleanup().sender.clone(), + })); + self.dmabuf_cache.insert(buffer.weak(), texture.clone()); + Ok(texture) + }) + } + + fn dmabuf_formats(&self) -> FormatSet { + self.egl.dmabuf_texture_formats().clone() + } + + fn has_dmabuf_format(&self, format: Format) -> bool { + self.egl.dmabuf_texture_formats().contains(&format) + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportDmaWl for GlesRenderer {} + +impl GlesRenderer { + #[profiling::function] + fn existing_dmabuf_texture(&self, buffer: &Dmabuf) -> Result, GlesError> { + let Some(texture) = self.dmabuf_cache.get(&buffer.weak()) else { + return Ok(None); + }; + + trace!("Re-using texture {:?} for {:?}", texture.0.texture, buffer); + if let Some(egl_images) = texture.0.egl_images.as_ref() { + if egl_images[0] == ffi_egl::NO_IMAGE_KHR { + return Ok(None); + } + let tex = Some(texture.0.texture); + self.import_egl_image(egl_images[0], texture.0.is_external, tex)?; + } + Ok(Some(texture.clone())) + } + + #[profiling::function] + fn import_egl_image( + &self, + image: EGLImage, + is_external: bool, + tex: Option, + ) -> Result { + unsafe { + self.egl.make_current()?; + } + let tex = tex.unwrap_or_else(|| unsafe { + let mut tex = 0; + self.gl.GenTextures(1, &mut tex); + tex + }); + let target = if is_external { + ffi::TEXTURE_EXTERNAL_OES + } else { + ffi::TEXTURE_2D + }; + unsafe { + self.gl.BindTexture(target, tex); + self.gl.EGLImageTargetTexture2DOES(target, image); + self.gl.BindTexture(target, 0); + } + + Ok(tex) + } +} + +impl ExportMem for GlesRenderer { + type TextureMapping = GlesMapping; + + #[instrument(level = "trace", parent = &self.span, skip(self, target))] + #[profiling::function] + fn copy_framebuffer( + &mut self, + target: &GlesTarget<'_>, + region: Rectangle, + fourcc: Fourcc, + ) -> Result { + target.0.make_current(&self.gl, &self.egl)?; + + let (_, has_alpha) = target.0.format().ok_or(GlesError::UnknownPixelFormat)?; + let (_, format, layout) = fourcc_to_gl_formats(fourcc).ok_or(GlesError::UnknownPixelFormat)?; + let bpp = gl_bpp(format, layout).ok_or(GlesError::UnsupportedPixelLayout)? / 8; + + let mut pbo = 0; + let err = unsafe { + self.gl.GetError(); // clear errors + self.gl.GenBuffers(1, &mut pbo); + self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, pbo); + let size = (region.size.w * region.size.h * bpp as i32) as isize; + self.gl + .BufferData(ffi::PIXEL_PACK_BUFFER, size, ptr::null(), ffi::STREAM_DRAW); + self.gl + .ReadBuffer(if matches!(target.0, GlesTargetInternal::Surface { .. }) { + ffi::BACK + } else { + ffi::COLOR_ATTACHMENT0 + }); + self.gl.ReadPixels( + region.loc.x, + region.loc.y, + region.size.w, + region.size.h, + format, + layout, + ptr::null_mut(), + ); + self.gl.ReadBuffer(ffi::NONE); + self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0); + self.gl.GetError() + }; + + match err { + ffi::NO_ERROR => Ok(GlesMapping { + pbo, + format, + layout, + has_alpha, + size: region.size, + mapping: AtomicPtr::new(ptr::null_mut()), + destruction_callback_sender: self.gles_cleanup().sender.clone(), + }), + err => { + unsafe { self.gl.DeleteBuffers(1, &pbo) }; + Err(if matches!(err, ffi::INVALID_ENUM | ffi::INVALID_OPERATION) { + GlesError::UnsupportedPixelFormat(fourcc) + } else { + GlesError::UnknownPixelFormat + }) + } + } + } + + fn can_read_texture(&mut self, texture: &Self::TextureId) -> Result { + // if we can't bind the texture, we can't read it + Ok(self.bind_texture(texture).is_ok()) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn copy_texture( + &mut self, + texture: &Self::TextureId, + region: Rectangle, + fourcc: Fourcc, + ) -> Result { + let mut pbo = 0; + let target = self.bind_texture(texture)?; + target.0.make_current(&self.gl, &self.egl)?; + + let (_, format, layout) = fourcc_to_gl_formats(fourcc).ok_or(GlesError::UnknownPixelFormat)?; + let bpp = gl_bpp(format, layout).expect("We check the format before") / 8; + + let err = unsafe { + self.gl.GetError(); // clear errors + self.gl.GenBuffers(1, &mut pbo); + self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, pbo); + self.gl.BufferData( + ffi::PIXEL_PACK_BUFFER, + (region.size.w * region.size.h * bpp as i32) as isize, + ptr::null(), + ffi::STREAM_DRAW, + ); + self.gl.ReadBuffer(ffi::COLOR_ATTACHMENT0); + self.gl.ReadPixels( + region.loc.x, + region.loc.y, + region.size.w, + region.size.h, + format, + layout, + ptr::null_mut(), + ); + self.gl.ReadBuffer(ffi::NONE); + self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0); + self.gl.GetError() + }; + + match err { + ffi::NO_ERROR => Ok(GlesMapping { + pbo, + format, + layout, + has_alpha: texture.0.has_alpha, + size: region.size, + mapping: AtomicPtr::new(ptr::null_mut()), + destruction_callback_sender: self.gles_cleanup().sender.clone(), + }), + err => { + unsafe { self.gl.DeleteBuffers(1, &pbo) }; + Err(if matches!(err, ffi::INVALID_ENUM | ffi::INVALID_OPERATION) { + GlesError::UnsupportedPixelFormat(fourcc) + } else { + GlesError::UnknownPixelFormat + }) + } + } + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn map_texture<'a>( + &mut self, + texture_mapping: &'a Self::TextureMapping, + ) -> Result<&'a [u8], Self::Error> { + unsafe { + self.egl.make_current()?; + } + + let size = texture_mapping.size(); + let len = size.w * size.h * 4; + + let mapping_ptr = texture_mapping.mapping.load(Ordering::SeqCst); + let ptr = if mapping_ptr.is_null() { + unsafe { + self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, texture_mapping.pbo); + let ptr = self + .gl + .MapBufferRange(ffi::PIXEL_PACK_BUFFER, 0, len as isize, ffi::MAP_READ_BIT); + self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0); + + if ptr.is_null() { + return Err(GlesError::MappingError); + } + + texture_mapping.mapping.store(ptr, Ordering::SeqCst); + ptr + } + } else { + mapping_ptr + }; + + unsafe { Ok(slice::from_raw_parts(ptr as *const u8, len as usize)) } + } +} + +impl Bind for GlesRenderer { + fn bind<'a>(&mut self, surface: &'a mut EGLSurface) -> Result, GlesError> { + Ok(GlesTarget(GlesTargetInternal::Surface { surface })) + } +} + +impl Bind for GlesRenderer { + fn bind<'a>(&mut self, dmabuf: &'a mut Dmabuf) -> Result, GlesError> { + let mut bind = |dmabuf: &'a mut Dmabuf| { + let texture = self.import_dmabuf(dmabuf, None)?; + if texture.0.is_external { + return Err(GlesError::FramebufferBindingError); + } + + if let Ok(target) = self + .bind_texture(&texture) + // SAFETY: The lifetime of the target only depends on the dmabuf, + // as the GlesTexture is cloned internally. + .map(|tex| unsafe { std::mem::transmute::, GlesTarget<'a>>(tex) }) + { + return Ok(target); + } + + let buf = self + .buffers + .iter_mut() + .find(|buffer| { + if let Some(dma) = buffer.0.dmabuf.upgrade() { + dma == *dmabuf + } else { + false + } + }) + .map(|buf| Ok(buf.clone())) + .unwrap_or_else(|| { + unsafe { + self.egl.make_current()?; + } + + trace!("Creating EGLImage for Dmabuf: {:?}", dmabuf); + let image = self + .egl + .display() + .create_image_from_dmabuf(dmabuf) + .map_err(GlesError::BindBufferEGLError)?; + + unsafe { + let mut rbo = 0; + self.gl.GenRenderbuffers(1, &mut rbo as *mut _); + self.gl.BindRenderbuffer(ffi::RENDERBUFFER, rbo); + self.gl + .EGLImageTargetRenderbufferStorageOES(ffi::RENDERBUFFER, image); + self.gl.BindRenderbuffer(ffi::RENDERBUFFER, 0); + + let mut fbo = 0; + self.gl.GenFramebuffers(1, &mut fbo as *mut _); + self.gl.BindFramebuffer(ffi::FRAMEBUFFER, fbo); + self.gl.FramebufferRenderbuffer( + ffi::FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::RENDERBUFFER, + rbo, + ); + let status = self.gl.CheckFramebufferStatus(ffi::FRAMEBUFFER); + self.gl.BindFramebuffer(ffi::FRAMEBUFFER, 0); + + if status != ffi::FRAMEBUFFER_COMPLETE { + self.gl.DeleteFramebuffers(1, &mut fbo as *mut _); + self.gl.DeleteRenderbuffers(1, &mut rbo as *mut _); + ffi_egl::DestroyImageKHR(**self.egl.display().get_display_handle(), image); + return Err(GlesError::FramebufferBindingError); + } + let buf = GlesBuffer(Rc::new(GlesBufferInner { + dmabuf: dmabuf.weak(), + image, + rbo, + fbo, + destruction_callback_sender: self.gles_cleanup().sender.clone(), + })); + + self.buffers.push(buf.clone()); + + Ok(buf) + } + })?; + + Ok(GlesTarget(GlesTargetInternal::Image { buf, dmabuf })) + }; + + bind(dmabuf).inspect_err(|_| { + if let Err(err) = self.unbind() { + self.span.in_scope(|| warn!(?err, "Failed to unbind on err")); + } + }) + } + + fn supported_formats(&self) -> Option { + Some(self.egl.display().dmabuf_render_formats().clone()) + } +} + +impl Bind for GlesRenderer { + fn bind<'a>(&mut self, texture: &'a mut GlesTexture) -> Result, GlesError> { + self.bind_texture(texture) + } +} + +impl Bind for GlesRenderer { + fn bind<'a>(&mut self, renderbuffer: &'a mut GlesRenderbuffer) -> Result, GlesError> { + unsafe { + self.egl.make_current()?; + } + + let bind = |renderbuffer: &'a mut GlesRenderbuffer| { + let mut fbo = 0; + unsafe { + self.gl.GenFramebuffers(1, &mut fbo as *mut _); + self.gl.BindFramebuffer(ffi::FRAMEBUFFER, fbo); + self.gl.BindRenderbuffer(ffi::RENDERBUFFER, renderbuffer.0.rbo); + self.gl.FramebufferRenderbuffer( + ffi::FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::RENDERBUFFER, + renderbuffer.0.rbo, + ); + let status = self.gl.CheckFramebufferStatus(ffi::FRAMEBUFFER); + self.gl.BindFramebuffer(ffi::FRAMEBUFFER, 0); + self.gl.BindRenderbuffer(ffi::RENDERBUFFER, 0); + + if status != ffi::FRAMEBUFFER_COMPLETE { + self.gl.DeleteFramebuffers(1, &mut fbo as *mut _); + return Err(GlesError::FramebufferBindingError); + } + } + + Ok(GlesTarget(GlesTargetInternal::Renderbuffer { + buf: renderbuffer, + fbo, + })) + }; + + bind(renderbuffer).inspect_err(|_| { + if let Err(err) = self.unbind() { + self.span.in_scope(|| warn!(?err, "Failed to unbind on err")); + } + }) + } +} + +impl Offscreen for GlesRenderer { + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn create_buffer( + &mut self, + format: Fourcc, + size: Size, + ) -> Result { + let has_alpha = has_alpha(format); + let (internal, format, layout) = + fourcc_to_gl_formats(format).ok_or(GlesError::UnsupportedPixelFormat(format))?; + if (internal != ffi::RGBA8 && internal != ffi::BGRA_EXT) + && !self.capabilities.contains(&Capability::_10Bit) + { + return Err(GlesError::UnsupportedPixelLayout); + } + + let tex = unsafe { + self.egl.make_current()?; + let mut tex = 0; + self.gl.GenTextures(1, &mut tex); + self.gl.BindTexture(ffi::TEXTURE_2D, tex); + self.gl.TexImage2D( + ffi::TEXTURE_2D, + 0, + internal as i32, + size.w, + size.h, + 0, + format, + layout, + std::ptr::null(), + ); + tex + }; + + Ok(unsafe { GlesTexture::from_raw(self, Some(internal), !has_alpha, tex, size) }) + } +} + +impl Offscreen for GlesRenderer { + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn create_buffer( + &mut self, + format: Fourcc, + size: Size, + ) -> Result { + if !self.capabilities.contains(&Capability::Renderbuffer) { + return Err(GlesError::UnsupportedPixelFormat(format)); + } + let has_alpha = has_alpha(format); + let (internal, _, _) = + fourcc_to_gl_formats(format).ok_or(GlesError::UnsupportedPixelFormat(format))?; + + if internal != ffi::RGBA8 && !self.capabilities.contains(&Capability::_10Bit) { + return Err(GlesError::UnsupportedPixelLayout); + } + + unsafe { + self.egl.make_current()?; + + let mut rbo = 0; + self.gl.GenRenderbuffers(1, &mut rbo); + self.gl.BindRenderbuffer(ffi::RENDERBUFFER, rbo); + self.gl + .RenderbufferStorage(ffi::RENDERBUFFER, internal, size.w, size.h); + self.gl.BindRenderbuffer(ffi::RENDERBUFFER, 0); + + Ok(GlesRenderbuffer(Arc::new(GlesRenderbufferInternal { + rbo, + format: internal, + has_alpha, + size, + destruction_callback_sender: self.gles_cleanup().sender.clone(), + }))) + } + } +} + +impl<'buffer> BlitFrame> for GlesFrame<'_, '_> { + fn blit_to( + &mut self, + to: &mut GlesTarget<'buffer>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + unsafe { + self.renderer.gl.Disable(ffi::SCISSOR_TEST); + } + let res = self.renderer.blit(self.target, to, src, dst, filter); + unsafe { + self.renderer.gl.Enable(ffi::SCISSOR_TEST); + } + self.target + .0 + .make_current(&self.renderer.gl, &self.renderer.egl)?; + res + } + + fn blit_from( + &mut self, + from: &GlesTarget<'buffer>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + let res = self.renderer.blit(from, self.target, src, dst, filter); + self.target + .0 + .make_current(&self.renderer.gl, &self.renderer.egl)?; + res + } +} + +impl Blit for GlesRenderer { + #[instrument(level = "trace", parent = &self.span, skip(self, src_target, dst_target))] + #[profiling::function] + fn blit( + &mut self, + src_target: &GlesTarget<'_>, + dst_target: &mut GlesTarget<'_>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + // glBlitFramebuffer is sadly only available for GLES 3.0 and higher + if self.gl_version < version::GLES_3_0 { + return Err(GlesError::GLVersionNotSupported(version::GLES_3_0)); + } + + match (&src_target.0, &dst_target.0) { + ( + GlesTargetInternal::Surface { surface: src, .. }, + GlesTargetInternal::Surface { surface: dst, .. }, + ) => unsafe { + self.egl.make_current_with_draw_and_read_surface(dst, src)?; + }, + (GlesTargetInternal::Surface { surface: src, .. }, _) => unsafe { + self.egl.make_current_with_surface(src)?; + }, + (_, GlesTargetInternal::Surface { surface: dst, .. }) => unsafe { + self.egl.make_current_with_surface(dst)?; + }, + (_, _) => unsafe { + self.egl.make_current()?; + }, + } + + self.profiler.collect(&self.gl); + let scope = self.profiler.scope(gpu_span_location!("blit"), &self.gl); + + match &src_target.0 { + GlesTargetInternal::Image { buf, .. } => unsafe { + self.gl.BindFramebuffer(ffi::READ_FRAMEBUFFER, buf.0.fbo) + }, + GlesTargetInternal::Texture { fbo, .. } => unsafe { + self.gl.BindFramebuffer(ffi::READ_FRAMEBUFFER, *fbo) + }, + GlesTargetInternal::Renderbuffer { fbo, .. } => unsafe { + self.gl.BindFramebuffer(ffi::READ_FRAMEBUFFER, *fbo) + }, + _ => {} // Note: The only target missing is `Surface` and handled above + } + match &dst_target.0 { + GlesTargetInternal::Image { buf, .. } => unsafe { + self.gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, buf.0.fbo) + }, + GlesTargetInternal::Texture { fbo, .. } => unsafe { + self.gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, *fbo) + }, + GlesTargetInternal::Renderbuffer { fbo, .. } => unsafe { + self.gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, *fbo) + }, + _ => {} // Note: The only target missing is `Surface` and handled above + } + + let status = unsafe { self.gl.CheckFramebufferStatus(ffi::FRAMEBUFFER) }; + if status != ffi::FRAMEBUFFER_COMPLETE { + drop(scope); + let _ = self.unbind(); + return Err(GlesError::FramebufferBindingError); + } + + let errno = unsafe { + while self.gl.GetError() != ffi::NO_ERROR {} // clear flag before + self.gl.BlitFramebuffer( + src.loc.x, + src.loc.y, + src.loc.x + src.size.w, + src.loc.y + src.size.h, + dst.loc.x, + dst.loc.y, + dst.loc.x + dst.size.w, + dst.loc.y + dst.size.h, + ffi::COLOR_BUFFER_BIT, + match filter { + TextureFilter::Linear => ffi::LINEAR, + TextureFilter::Nearest => ffi::NEAREST, + }, + ); + self.gl.GetError() + }; + drop(scope); + + if errno == ffi::INVALID_OPERATION { + Err(GlesError::BlitError) + } else { + if let Some(sync_point) = self.export_sync_point() { + // Sync after glFlush in export_sync_point() and right before returning. + self.profiler.sync_gpu(&self.gl); + return Ok(sync_point); + } + self.profiler.sync_gpu(&self.gl); + + unsafe { + self.gl.Finish(); + } + Ok(SyncPoint::signaled()) + } + } +} + +impl Drop for GlesRenderer { + fn drop(&mut self) { + let _guard = self.span.enter(); + unsafe { + if self.egl.make_current().is_ok() { + self.gl.BindFramebuffer(ffi::FRAMEBUFFER, 0); + self.gl.DeleteProgram(self.solid_program.program); + self.gl.DeleteBuffers(self.vbos.len() as i32, self.vbos.as_ptr()); + + self.profiler.cleanup(Some(&self.gl)); + + if self.extensions.iter().any(|ext| ext == "GL_KHR_debug") { + self.gl.Disable(ffi::DEBUG_OUTPUT); + self.gl.DebugMessageCallback(None, ptr::null()); + } + + #[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] + let _ = self.egl_reader.take(); + let _ = self.egl.unbind(); + } else { + self.profiler.cleanup(None); + } + + if let Some(gl_debug_ptr) = self.gl_debug_span.take() { + let _ = Box::from_raw(gl_debug_ptr); + } + } + } +} + +impl GlesRenderer { + /// Get access to the underlying [`EGLContext`]. + /// + /// *Note*: Modifying the context state, might result in rendering issues. + /// The context state is considered an implementation detail + /// and no guarantee is made about what can or cannot be changed. + /// To make sure a certain modification does not interfere with + /// the renderer's behaviour, check the source. + pub fn egl_context(&self) -> &EGLContext { + &self.egl + } + + /// Run custom code in the GL context owned by this renderer. + /// + /// The OpenGL state of the renderer is considered an implementation detail + /// and no guarantee is made about what can or cannot be changed, + /// as such you should reset everything you change back to its previous value + /// or check the source code of the version of Smithay you are using to ensure + /// your changes don't interfere with the renderer's behavior. + /// Doing otherwise can lead to rendering errors while using other functions of this renderer. + #[instrument(level = "trace", parent = &self.span, skip_all)] + pub fn with_context(&mut self, func: F) -> Result + where + F: FnOnce(&ffi::Gles2) -> R, + { + unsafe { + self.egl.make_current()?; + } + Ok(func(&self.gl)) + } + + /// Run custom code in the GL context with GPU profiling. + /// + /// Sets up a GPU profiling span, calls [`with_context()`](Self::with_context), then finishes + /// the span, calls `glFlush()` and synchronizes the CPU/GPU timestamp. + pub fn with_profiled_context(&mut self, location: SpanLocation, func: F) -> Result + where + F: FnOnce(&ffi::Gles2) -> R, + { + unsafe { + self.egl.make_current()?; + } + + self.profiler.collect(&self.gl); + + let result = { + let _scope = self.profiler.scope(location, &self.gl); + func(&self.gl) + }; + + unsafe { self.gl.Flush() }; + self.profiler.sync_gpu(&self.gl); + + Ok(result) + } + + /// Compile a custom pixel shader for rendering with [`GlesFrame::render_pixel_shader_to`]. + /// + /// Pixel shaders can be used for completely shader-driven drawing into a given region. + /// + /// They need to handle the following #define variants: + /// - `DEBUG_FLAGS` see below + /// + /// They receive the following variables: + /// - *varying* v_coords `vec2` - contains the position from the vertex shader + /// - *uniform* size `vec2` - size of the viewport in pixels + /// - *uniform* alpha `float` - for the alpha value passed by the renderer + /// - *uniform* tint `float` - for the tint passed by the renderer (either 0.0 or 1.0) - only if `DEBUG_FLAGS` was defined + /// + /// Additional uniform values can be defined by passing `UniformName`s to the `additional_uniforms` argument + /// and can then be set in functions utilizing `GlesPixelProgram` (like [`GlesFrame::render_pixel_shader_to`]). + /// + /// The shader must **not** contain a `#version` directive. It will be interpreted as version 100. + /// + /// ## Panics + /// + /// Panics if any of the names of the passed additional uniforms contains a `\0`/NUL-byte. + pub fn compile_custom_pixel_shader( + &mut self, + src: impl AsRef, + additional_uniforms: &[UniformName<'_>], + ) -> Result { + unsafe { + self.egl.make_current()?; + } + + let shader = format!("#version 100\n{}", src.as_ref()); + let program = unsafe { link_program(&self.gl, shaders::VERTEX_SHADER, &shader)? }; + let debug_shader = format!("#version 100\n#define {}\n{}", shaders::DEBUG_FLAGS, src.as_ref()); + let debug_program = unsafe { link_program(&self.gl, shaders::VERTEX_SHADER, &debug_shader)? }; + + let vert = c"vert"; + let vert_position = c"vert_position"; + let matrix = c"matrix"; + let tex_matrix = c"tex_matrix"; + let size = c"size"; + let alpha = c"alpha"; + let tint = c"tint"; + + unsafe { + Ok(GlesPixelProgram(Arc::new(GlesPixelProgramInner { + normal: GlesPixelProgramInternal { + program, + uniform_matrix: self + .gl + .GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar), + uniform_tex_matrix: self + .gl + .GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar), + uniform_alpha: self + .gl + .GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar), + uniform_size: self + .gl + .GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar), + attrib_vert: self + .gl + .GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar), + attrib_position: self + .gl + .GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar), + additional_uniforms: additional_uniforms + .iter() + .map(|uniform| { + let name = CString::new(uniform.name.as_bytes()).expect("Interior null in name"); + let location = self + .gl + .GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar); + ( + uniform.name.clone().into_owned(), + UniformDesc { + location, + type_: uniform.type_, + }, + ) + }) + .collect(), + }, + debug: GlesPixelProgramInternal { + program: debug_program, + uniform_matrix: self + .gl + .GetUniformLocation(debug_program, matrix.as_ptr() as *const ffi::types::GLchar), + uniform_tex_matrix: self + .gl + .GetUniformLocation(debug_program, tex_matrix.as_ptr() as *const ffi::types::GLchar), + uniform_alpha: self + .gl + .GetUniformLocation(debug_program, alpha.as_ptr() as *const ffi::types::GLchar), + uniform_size: self + .gl + .GetUniformLocation(debug_program, size.as_ptr() as *const ffi::types::GLchar), + attrib_vert: self + .gl + .GetAttribLocation(debug_program, vert.as_ptr() as *const ffi::types::GLchar), + attrib_position: self.gl.GetAttribLocation( + debug_program, + vert_position.as_ptr() as *const ffi::types::GLchar, + ), + additional_uniforms: additional_uniforms + .iter() + .map(|uniform| { + let name = CString::new(uniform.name.as_bytes()).expect("Interior null in name"); + let location = self.gl.GetUniformLocation( + debug_program, + name.as_ptr() as *const ffi::types::GLchar, + ); + ( + uniform.name.clone().into_owned(), + UniformDesc { + location, + type_: uniform.type_, + }, + ) + }) + .collect(), + }, + destruction_callback_sender: self.gles_cleanup().sender.clone(), + uniform_tint: self + .gl + .GetUniformLocation(debug_program, tint.as_ptr() as *const ffi::types::GLchar), + }))) + } + } + + /// Compile a custom texture shader for rendering with [`GlesFrame::render_texture`] or [`GlesFrame::render_texture_from_to`]. + /// + /// They need to handle the following #define variants: + /// - `EXTERNAL` uses samplerExternalOES instead of sampler2D, requires the GL_OES_EGL_image_external extension + /// - `NO_ALPHA` needs to ignore the alpha channel of the texture and replace it with 1.0 + /// - `DEBUG_FLAGS` see below + /// + /// They receive the following variables: + /// - *varying* v_coords `vec2` - contains the position from the vertex shader + /// - *uniform* tex `sample2d` - texture sampler + /// - *uniform* alpha `float` - for the alpha value passed by the renderer + /// - *uniform* tint `float` - for the tint passed by the renderer (either 0.0 or 1.0) - only if `DEBUG_FLAGS` was defined + /// + /// Additional uniform values can be defined by passing `UniformName`s to the `additional_uniforms` argument + /// and can then be set in functions utilizing `GlesTexProgram` (like [`GlesFrame::render_texture`] or [`GlesFrame::render_texture_from_to`]). + /// + /// The shader must contain a line only containing `//_DEFINES`. It will be replaced by the renderer with corresponding `#define` directives. + /// + /// ## Panics + /// + /// Panics if any of the names of the passed additional uniforms contains a `\0`/NUL-byte. + pub fn compile_custom_texture_shader( + &mut self, + shader: impl AsRef, + additional_uniforms: &[UniformName<'_>], + ) -> Result { + unsafe { + self.egl.make_current()?; + } + + unsafe { + texture_program( + &self.gl, + shader.as_ref(), + additional_uniforms, + self.gles_cleanup().sender.clone(), + ) + } + } +} + +impl GlesFrame<'_, '_> { + /// Run custom code in the GL context owned by this renderer. + /// + /// The OpenGL state of the renderer is considered an implementation detail + /// and no guarantee is made about what can or cannot be changed, + /// as such you should reset everything you change back to its previous value + /// or check the source code of the version of Smithay you are using to ensure + /// your changes don't interfere with the renderer's behavior. + /// Doing otherwise can lead to rendering errors while using other functions of this renderer. + #[instrument(level = "trace", parent = &self.span, skip_all)] + pub fn with_context(&mut self, func: F) -> Result + where + F: FnOnce(&ffi::Gles2) -> R, + { + Ok(func(&self.renderer.gl)) + } + + /// Run custom code in the GL context with GPU profiling. + /// + /// Calls [`with_context()`](Self::with_context) inside + /// [`with_gpu_span()`](Self::with_gpu_span). + pub fn with_profiled_context(&mut self, location: SpanLocation, func: F) -> Result + where + F: FnOnce(&ffi::Gles2) -> R, + { + self.with_gpu_span(location, |frame| frame.with_context(func)) + } + + /// Run a function in a GPU profiling span. + pub fn with_gpu_span(&mut self, location: SpanLocation, func: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + let span = self.renderer.profiler.enter(location, &self.renderer.gl); + let result = func(self); + self.renderer.profiler.exit(&self.renderer.gl, span); + result + } +} + +impl RendererSuper for GlesRenderer { + type Error = GlesError; + type TextureId = GlesTexture; + type Framebuffer<'buffer> = GlesTarget<'buffer>; + type Frame<'frame, 'buffer> + = GlesFrame<'frame, 'buffer> + where + 'buffer: 'frame; +} + +impl Renderer for GlesRenderer { + fn context_id(&self) -> ContextId { + self.egl + .user_data() + .get::>() + .unwrap() + .clone() + } + + fn downscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.min_filter = filter; + Ok(()) + } + fn upscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.max_filter = filter; + Ok(()) + } + + fn set_debug_flags(&mut self, flags: DebugFlags) { + self.debug_flags = flags; + } + + fn debug_flags(&self) -> DebugFlags { + self.debug_flags + } + + #[profiling::function] + fn render<'frame, 'buffer>( + &'frame mut self, + target: &'frame mut GlesTarget<'buffer>, + mut output_size: Size, + transform: Transform, + ) -> Result, GlesError> + where + 'buffer: 'frame, + { + target.0.make_current(&self.gl, &self.egl)?; + + // Collect last frame's timestamps. + self.profiler.collect(&self.gl); + + let gpu_span = self.profiler.enter(gpu_span_location!("render"), &self.gl); + + unsafe { + self.gl.Viewport(0, 0, output_size.w, output_size.h); + + self.gl.Scissor(0, 0, output_size.w, output_size.h); + self.gl.Enable(ffi::SCISSOR_TEST); + + self.gl.Enable(ffi::BLEND); + self.gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA); + } + + // Handle the width/height swap when the output is rotated by 90°/270°. + if let Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 = transform { + mem::swap(&mut output_size.w, &mut output_size.h); + } + + // replicate https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glOrtho.xml + // glOrtho(0, width, 0, height, 1, 1); + let mut renderer = Affine2::IDENTITY; + let t = Affine2::IDENTITY; + let x = 2.0 / (output_size.w as f32); + let y = 2.0 / (output_size.h as f32); + + // Rotation & Reflection + renderer.x_axis.x = x * t.x_axis.x; + renderer.y_axis.x = x * t.x_axis.y; + renderer.x_axis.y = y * -t.y_axis.x; + renderer.y_axis.y = y * -t.y_axis.y; + + //Translation + renderer.z_axis.x = -(1.0f32.copysign(renderer.x_axis.x + renderer.y_axis.x)); + renderer.z_axis.y = -(1.0f32.copysign(renderer.x_axis.y + renderer.y_axis.y)); + + // We account for OpenGLs coordinate system here + let flip180 = Affine2::from_cols_array(&[1.0, 0.0, 0.0, -1.0, 0.0, 0.0]); + + let current_projection = (flip180 * transform.matrix() * renderer).into(); + let span = span!(parent: &self.span, Level::DEBUG, "renderer_gles2_frame", current_projection = ?current_projection, size = ?output_size, transform = ?transform).entered(); + + Ok(GlesFrame { + renderer: self, + target, + // output transformation passed in by the user + current_projection, + transform, + size: output_size, + tex_program_override: None, + finished: AtomicBool::new(false), + + span, + + gpu_span: Some(gpu_span), + }) + } + + #[profiling::function] + fn wait(&mut self, sync: &super::sync::SyncPoint) -> Result<(), Self::Error> { + unsafe { + self.egl.make_current()?; + } + + let display = self.egl_context().display(); + + // if the sync point holds a EGLFence we can try + // to directly insert it in our context + if let Some(fence) = sync.get::() { + if fence.wait(display).is_ok() { + return Ok(()); + } + } + + // alternative we try to create a temporary fence + // out of the native fence if available and try + // to insert it in our context + if let Some(native) = EGLFence::supports_importing(display) + .then(|| sync.export()) + .flatten() + { + if let Ok(fence) = EGLFence::import(display, native) { + if fence.wait(display).is_ok() { + return Ok(()); + } + } + } + + // if everything above failed we can only + // block until the sync point has been reached + sync.wait().map_err(|_| GlesError::SyncInterrupted) + } + + #[profiling::function] + fn cleanup_texture_cache(&mut self) -> Result<(), Self::Error> { + self.cleanup() + } + + #[profiling::function] + fn invalidate_caches(&mut self) -> Result<(), Self::Error> { + self.dmabuf_cache.clear(); + self.buffers.clear(); + self.cleanup() + } +} + +/// Vertices for instanced rendering. +static INSTANCED_VERTS: [ffi::types::GLfloat; 8] = [ + 1.0, 0.0, // top right + 0.0, 0.0, // top left + 1.0, 1.0, // bottom right + 0.0, 1.0, // bottom left +]; + +/// Vertices for rendering individual triangles. +const MAX_RECTS_PER_DRAW: usize = 10; +const TRIANGLE_VERTS: [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] = triangle_verts(); +const fn triangle_verts() -> [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] { + let mut verts = [0.; 12 * MAX_RECTS_PER_DRAW]; + let mut i = 0; + loop { + // Top Left. + verts[i * 12] = 0.0; + verts[i * 12 + 1] = 0.0; + + // Bottom left. + verts[i * 12 + 2] = 0.0; + verts[i * 12 + 3] = 1.0; + + // Bottom right. + verts[i * 12 + 4] = 1.0; + verts[i * 12 + 5] = 1.0; + + // Top left. + verts[i * 12 + 6] = 0.0; + verts[i * 12 + 7] = 0.0; + + // Bottom right. + verts[i * 12 + 8] = 1.0; + verts[i * 12 + 9] = 1.0; + + // Top right. + verts[i * 12 + 10] = 1.0; + verts[i * 12 + 11] = 0.0; + + i += 1; + if i == MAX_RECTS_PER_DRAW { + break; + } + } + verts +} + +/// Vertices for output rendering. +static OUTPUT_VERTS: [ffi::types::GLfloat; 8] = [ + -1.0, 1.0, // top right + -1.0, -1.0, // top left + 1.0, 1.0, // bottom right + 1.0, -1.0, // bottom left +]; + +impl Frame for GlesFrame<'_, '_> { + type Error = GlesError; + type TextureId = GlesTexture; + + fn context_id(&self) -> ContextId { + self.renderer.context_id() + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn clear(&mut self, color: Color32F, at: &[Rectangle]) -> Result<(), GlesError> { + if at.is_empty() { + return Ok(()); + } + + let scope = self + .renderer + .profiler + .enter(gpu_span_location!("clear"), &self.renderer.gl); + unsafe { + self.renderer.gl.Disable(ffi::BLEND); + } + + let res = self.draw_solid(Rectangle::from_size(self.size), at, color); + + unsafe { + self.renderer.gl.Enable(ffi::BLEND); + self.renderer.gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA); + } + self.renderer.profiler.exit(&self.renderer.gl, scope); + + res + } + + #[instrument(level = "trace", skip(self), parent = &self.span)] + #[profiling::function] + fn draw_solid( + &mut self, + dst: Rectangle, + damage: &[Rectangle], + color: Color32F, + ) -> Result<(), Self::Error> { + if damage.is_empty() { + return Ok(()); + } + + let is_opaque = color.is_opaque(); + + let scope = self + .renderer + .profiler + .enter(gpu_span_location!("draw_solid"), &self.renderer.gl); + if is_opaque { + unsafe { + self.renderer.gl.Disable(ffi::BLEND); + } + } + + let res = self.draw_solid(dst, damage, color); + + if is_opaque { + unsafe { + self.renderer.gl.Enable(ffi::BLEND); + self.renderer.gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA); + } + } + self.renderer.profiler.exit(&self.renderer.gl, scope); + + res + } + + #[instrument(level = "trace", skip(self), parent = &self.span)] + #[profiling::function] + fn render_texture_from_to( + &mut self, + texture: &GlesTexture, + src: Rectangle, + dest: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + transform: Transform, + alpha: f32, + ) -> Result<(), GlesError> { + self.render_texture_from_to( + texture, + src, + dest, + damage, + opaque_regions, + transform, + alpha, + None, + &[], + ) + } + + fn transformation(&self) -> Transform { + self.transform + } + + fn output_size(&self) -> Size { + self.size + } + + #[profiling::function] + fn wait(&mut self, sync: &SyncPoint) -> Result<(), Self::Error> { + self.renderer.wait(sync) + } + + #[profiling::function] + fn finish(mut self) -> Result { + self.finish_internal() + } +} + +impl GlesFrame<'_, '_> { + #[profiling::function] + fn finish_internal(&mut self) -> Result { + let _guard = self.span.enter(); + + if self.finished.swap(true, Ordering::SeqCst) { + return Ok(SyncPoint::signaled()); + } + + let finish_gpu_span = self + .renderer + .profiler + .enter(gpu_span_location!("finish_internal"), &self.renderer.gl); + unsafe { + self.renderer.gl.Disable(ffi::SCISSOR_TEST); + self.renderer.gl.Disable(ffi::BLEND); + } + + if let GlesTargetInternal::Texture { sync_lock, .. } = &mut self.target.0 { + sync_lock.update_write(&self.renderer.gl); + } + + // delayed destruction until the next frame rendering. + { + let scope = self + .renderer + .profiler + .enter(gpu_span_location!("cleanup"), &self.renderer.gl); + self.renderer.cleanup()?; + self.renderer.profiler.exit(&self.renderer.gl, scope); + } + + self.renderer.profiler.exit(&self.renderer.gl, finish_gpu_span); + if let Some(span) = self.gpu_span.take() { + self.renderer.profiler.exit(&self.renderer.gl, span); + } + + // if we support egl fences we should use it + if let Some(sync_point) = self.renderer.export_sync_point() { + // Sync after glFlush in export_sync_point() and right before returning. + self.renderer.profiler.sync_gpu(&self.renderer.gl); + return Ok(sync_point); + } + + self.renderer.profiler.sync_gpu(&self.renderer.gl); + + // as a last option we force finish, this is unlikely to happen + unsafe { + self.renderer.gl.Finish(); + } + Ok(SyncPoint::signaled()) + } + + /// Overrides the default texture shader used, if none is specified. + /// + /// This affects calls to [`Frame::render_texture_at`] or [`Frame::render_texture_from_to`] as well as + /// calls to [`GlesFrame::render_texture_from_to`] or [`GlesFrame::render_texture`], if the passed in `program` is `None`. + /// + /// Override is active only for the lifetime of this `GlesFrame` and can be reset via [`GlesFrame::clear_tex_program_override`]. + pub fn override_default_tex_program( + &mut self, + program: GlesTexProgram, + additional_uniforms: Vec>, + ) { + self.tex_program_override = Some((program, additional_uniforms)); + } + + /// Resets a texture shader override previously set by [`GlesFrame::override_default_tex_program`]. + pub fn clear_tex_program_override(&mut self) { + self.tex_program_override = None; + } + + /// Draw a solid color to the current target at the specified destination with the specified color. + #[instrument(level = "trace", skip(self), parent = &self.span)] + #[profiling::function] + pub fn draw_solid( + &mut self, + dest: Rectangle, + damage: &[Rectangle], + color: Color32F, + ) -> Result<(), GlesError> { + if damage.is_empty() { + return Ok(()); + } + + let mut mat = Mat3::IDENTITY; + mat = self.current_projection * mat; + + // prepare the vertices + self.renderer.vertices.clear(); + if self.renderer.capabilities.contains(&Capability::Instancing) { + self.renderer.vertices.extend(damage.iter().flat_map(|rect| { + let dest_size = dest.size; + + let rect_constrained_loc = rect.loc.constrain(Rectangle::from_size(dest_size)); + let rect_clamped_size = rect + .size + .clamp((0, 0), (dest_size.to_point() - rect_constrained_loc).to_size()); + + let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size); + [ + (dest.loc.x + rect.loc.x) as f32, + (dest.loc.y + rect.loc.y) as f32, + rect.size.w as f32, + rect.size.h as f32, + ] + })) + } else { + self.renderer.vertices.extend(damage.iter().flat_map(|rect| { + let dest_size = dest.size; + + let rect_constrained_loc = rect.loc.constrain(Rectangle::from_size(dest_size)); + let rect_clamped_size = rect + .size + .clamp((0, 0), (dest_size.to_point() - rect_constrained_loc).to_size()); + + let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size); + // Add the 4 f32s per damage rectangle for each of the 6 vertices. + (0..6).flat_map(move |_| { + [ + (dest.loc.x + rect.loc.x) as f32, + (dest.loc.y + rect.loc.y) as f32, + rect.size.w as f32, + rect.size.h as f32, + ] + }) + })); + } + + let gl = &self.renderer.gl; + let _scope = self + .renderer + .profiler + .scope(gpu_span_location!("draw_solid"), &self.renderer.gl); + unsafe { + gl.UseProgram(self.renderer.solid_program.program); + gl.Uniform4f( + self.renderer.solid_program.uniform_color, + color.r(), + color.g(), + color.b(), + color.a(), + ); + gl.UniformMatrix3fv( + self.renderer.solid_program.uniform_matrix, + 1, + ffi::FALSE, + mat.as_ref().as_ptr(), + ); + + gl.EnableVertexAttribArray(self.renderer.solid_program.attrib_vert as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, self.renderer.vbos[0]); + gl.VertexAttribPointer( + self.renderer.solid_program.attrib_vert as u32, + 2, + ffi::FLOAT, + ffi::FALSE, + 0, + std::ptr::null(), + ); + + gl.EnableVertexAttribArray(self.renderer.solid_program.attrib_position as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, 0); + + gl.VertexAttribPointer( + self.renderer.solid_program.attrib_position as u32, + 4, + ffi::FLOAT, + ffi::FALSE, + 0, + self.renderer.vertices.as_ptr() as *const _, + ); + + let damage_len = damage.len() as i32; + if self.renderer.capabilities.contains(&Capability::Instancing) { + gl.VertexAttribDivisor(self.renderer.solid_program.attrib_vert as u32, 0); + + gl.VertexAttribDivisor(self.renderer.solid_program.attrib_position as u32, 1); + + gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len); + } else { + // When we have more than 10 rectangles, draw them in batches of 10. + for i in 0..(damage_len - 1) / 10 { + gl.DrawArrays(ffi::TRIANGLES, 0, 60); + + // Set damage pointer to the next 10 rectangles. + gl.VertexAttribPointer( + self.renderer.solid_program.attrib_position as u32, + 4, + ffi::FLOAT, + ffi::FALSE, + 0, + self.renderer.vertices.as_ptr().add((i + 1) as usize * 60 * 4) as *const _, + ); + } + + // Draw the up to 10 remaining rectangles. + let count = ((damage_len - 1) % 10 + 1) * 6; + gl.DrawArrays(ffi::TRIANGLES, 0, count); + } + + gl.DisableVertexAttribArray(self.renderer.solid_program.attrib_vert as u32); + gl.DisableVertexAttribArray(self.renderer.solid_program.attrib_position as u32); + } + + Ok(()) + } + + /// Render part of a texture as given by src to the current target into the rectangle described by dst + /// as a flat 2d-plane after applying the inverse of the given transformation. + /// (Meaning `src_transform` should match the orientation of surface being rendered). + /// + /// Optionally allows a custom texture program and matching additional uniforms to be passed in. + #[instrument(level = "trace", skip(self), parent = &self.span)] + #[profiling::function] + #[allow(clippy::too_many_arguments)] + pub fn render_texture_from_to( + &mut self, + texture: &GlesTexture, + src: Rectangle, + dest: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + transform: Transform, + alpha: f32, + program: Option<&GlesTexProgram>, + additional_uniforms: &[Uniform<'_>], + ) -> Result<(), GlesError> { + let mut mat = Mat3::IDENTITY; + + // dest position and scale + mat *= Mat3::from_translation(Vec2::new(dest.loc.x as f32, dest.loc.y as f32)); + + // src scale, position, transform and y_inverted + let tex_size = texture.size(); + let src_size = src.size; + + if src_size.is_empty() || tex_size.is_empty() { + return Ok(()); + } + + let mut tex_mat = build_texture_mat(src, dest, tex_size, transform); + if texture.0.y_inverted { + tex_mat = Mat3::from_cols_array(&[1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0]) * tex_mat; + } + + let render_texture = |renderer: &mut Self, damage: &[Rectangle]| { + let instances = damage.iter().flat_map(|rect| { + let dest_size = dest.size; + + let rect_constrained_loc = rect.loc.constrain(Rectangle::from_size(dest_size)); + let rect_clamped_size = rect + .size + .clamp((0, 0), (dest_size.to_point() - rect_constrained_loc).to_size()); + + let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size); + [ + rect.loc.x as f32, + rect.loc.y as f32, + rect.size.w as f32, + rect.size.h as f32, + ] + }); + + renderer.render_texture( + texture, + tex_mat, + mat, + Some(instances), + alpha, + program, + additional_uniforms, + ) + }; + + // We split the damage in opaque and non opaque regions, for opaque regions we can + // disable blending. Most likely we did not clear regions marked as opaque, which can + // result in read-back when not disabling blending. This can be problematic on tile based + // renderers. + let mut non_opaque_damage = std::mem::take(&mut self.renderer.non_opaque_damage); + let mut opaque_damage = std::mem::take(&mut self.renderer.opaque_damage); + non_opaque_damage.clear(); + opaque_damage.clear(); + + // If drawing is implicit opaque and we have no custom program we + // can skip some logic and save a few operations. In case we have + // some user-provided alpha we can not disable blending, but should + // also have cleared the region previously anyway. In case we have + // no opaque regions we can also short cut the logic a bit. + let is_implicit_opaque = !texture.0.has_alpha && alpha == 1f32; + if is_implicit_opaque && program.is_none() && self.tex_program_override.is_none() { + opaque_damage.extend_from_slice(damage); + } else if alpha != 1f32 || opaque_regions.is_empty() { + non_opaque_damage.extend_from_slice(damage); + } else { + non_opaque_damage.extend_from_slice(damage); + opaque_damage.extend_from_slice(damage); + + non_opaque_damage = + Rectangle::subtract_rects_many_in_place(non_opaque_damage, opaque_regions.iter().copied()); + opaque_damage = + Rectangle::subtract_rects_many_in_place(opaque_damage, non_opaque_damage.iter().copied()); + } + + tracing::trace!(non_opaque_damage = ?non_opaque_damage, opaque_damage = ?opaque_damage, "drawing texture"); + + let non_opaque_render_res = if !non_opaque_damage.is_empty() { + render_texture(self, &non_opaque_damage) + } else { + Ok(()) + }; + + let opaque_render_res = if !opaque_damage.is_empty() { + unsafe { + self.renderer.gl.Disable(ffi::BLEND); + } + + let res = render_texture(self, &opaque_damage); + + unsafe { + self.renderer.gl.Enable(ffi::BLEND); + self.renderer.gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA); + } + + res + } else { + Ok(()) + }; + + // Return the damage(s) to be able to re-use the allocation(s) + std::mem::swap(&mut self.renderer.non_opaque_damage, &mut non_opaque_damage); + std::mem::swap(&mut self.renderer.opaque_damage, &mut opaque_damage); + + non_opaque_render_res?; + opaque_render_res?; + + Ok(()) + } + + /// Render a texture to the current target using given projection matrix and alpha. + /// + /// The instances are used to define the regions which should get drawn. + /// Each instance has to define 4 [`GLfloat`](ffi::types::GLfloat) which define the + /// relative offset and scale for the vertex position and range from `0.0` to `1.0`. + /// The first 2 [`GLfloat`](ffi::types::GLfloat) define the relative x and y offset. + /// The remaining 2 [`GLfloat`](ffi::types::GLfloat) define the x and y scale. + /// This can be used to only update parts of the texture on screen. + /// + /// The given texture matrix is used to transform the instances into texture coordinates. + /// In case the texture is rotated, flipped or y-inverted the matrix has to be set up accordingly. + /// Additionally the matrix can be used to crop the texture. + /// + /// Optionally allows a custom texture program and matching additional uniforms to be passed in. + #[instrument(level = "trace", skip(self, instances), parent = &self.span)] + #[profiling::function] + #[allow(clippy::too_many_arguments)] + pub fn render_texture( + &mut self, + tex: &GlesTexture, + tex_matrix: Mat3, + mut matrix: Mat3, + instances: Option>, + alpha: f32, + program: Option<&GlesTexProgram>, + additional_uniforms: &[Uniform<'_>], + ) -> Result<(), GlesError> { + // prepare the vertices + self.renderer.vertices.clear(); + let damage_len = if let Some(instances) = instances { + if self.renderer.capabilities.contains(&Capability::Instancing) { + self.renderer.vertices.extend(instances); + self.renderer.vertices.len() / 4 + } else { + let mut damage = 0; + let mut instances = instances.into_iter(); + while let Some(first) = instances.next() { + damage += 1; + let vertices = [ + first, + instances.next().unwrap(), + instances.next().unwrap(), + instances.next().unwrap(), + ]; + // Add the 4 f32s per damage rectangle for each of the 6 vertices. + for _ in 0..6 { + self.renderer.vertices.extend_from_slice(&vertices); + } + } + damage + } + } else if self.renderer.capabilities.contains(&Capability::Instancing) { + self.renderer.vertices.extend_from_slice(&[0.0, 0.0, 1.0, 1.0]); + 1 + } else { + // Add the 4 f32s per damage rectangle for each of the 6 vertices. + for _ in 0..6 { + self.renderer.vertices.extend_from_slice(&[0.0, 0.0, 1.0, 1.0]); + } + 1 + }; + + if self.renderer.vertices.is_empty() { + return Ok(()); + } + + //apply output transformation + matrix = self.current_projection * matrix; + + let target = if tex.0.is_external { + ffi::TEXTURE_EXTERNAL_OES + } else { + ffi::TEXTURE_2D + }; + let (tex_program, additional_uniforms) = program + .map(|p| (p, additional_uniforms)) + .or_else(|| self.tex_program_override.as_ref().map(|(p, a)| (p, &**a))) + .unwrap_or((&self.renderer.tex_program, &[])); + let program_variant = tex_program.variant_for_format( + if !tex.0.is_external { tex.0.format } else { None }, + tex.0.has_alpha, + ); + let program = if self.renderer.debug_flags.is_empty() { + &program_variant.normal + } else { + &program_variant.debug + }; + + // render + let gl = &self.renderer.gl; + let sync_lock = tex.0.sync.read().unwrap(); + unsafe { + sync_lock.wait_for_upload(gl); + let scope = self + .renderer + .profiler + .scope(gpu_span_location!("render_texture"), gl); + gl.ActiveTexture(ffi::TEXTURE0); + gl.BindTexture(target, tex.0.texture); + gl.TexParameteri( + target, + ffi::TEXTURE_MIN_FILTER, + match self.renderer.min_filter { + TextureFilter::Nearest => ffi::NEAREST as i32, + TextureFilter::Linear => ffi::LINEAR as i32, + }, + ); + gl.TexParameteri( + target, + ffi::TEXTURE_MAG_FILTER, + match self.renderer.max_filter { + TextureFilter::Nearest => ffi::NEAREST as i32, + TextureFilter::Linear => ffi::LINEAR as i32, + }, + ); + gl.UseProgram(program.program); + + gl.Uniform1i(program.uniform_tex, 0); + gl.UniformMatrix3fv(program.uniform_matrix, 1, ffi::FALSE, matrix.as_ref().as_ptr()); + gl.UniformMatrix3fv( + program.uniform_tex_matrix, + 1, + ffi::FALSE, + tex_matrix.as_ref().as_ptr(), + ); + gl.Uniform1f(program.uniform_alpha, alpha); + + if !self.renderer.debug_flags.is_empty() { + let tint = if self.renderer.debug_flags.contains(DebugFlags::TINT) { + 1.0f32 + } else { + 0.0f32 + }; + gl.Uniform1f(program_variant.uniform_tint, tint); + } + + for uniform in additional_uniforms { + let desc = program + .additional_uniforms + .get(&*uniform.name) + .ok_or_else(|| GlesError::UnknownUniform(uniform.name.clone().into_owned()))?; + uniform.value.set(gl, desc)?; + } + + gl.EnableVertexAttribArray(program.attrib_vert as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, self.renderer.vbos[0]); + gl.VertexAttribPointer( + program.attrib_vert as u32, + 2, + ffi::FLOAT, + ffi::FALSE, + 0, + std::ptr::null(), + ); + + // vert_position + gl.EnableVertexAttribArray(program.attrib_vert_position as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, 0); + + gl.VertexAttribPointer( + program.attrib_vert_position as u32, + 4, + ffi::FLOAT, + ffi::FALSE, + 0, + self.renderer.vertices.as_ptr() as *const _, + ); + + if self.renderer.capabilities.contains(&Capability::Instancing) { + let _scope = self + .renderer + .profiler + .scope(gpu_span_location!("draw instanced"), gl); + gl.VertexAttribDivisor(program.attrib_vert as u32, 0); + gl.VertexAttribDivisor(program.attrib_vert_position as u32, 1); + + gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len as i32); + } else { + let _scope = self + .renderer + .profiler + .scope(gpu_span_location!("draw batched"), gl); + + // When we have more than 10 rectangles, draw them in batches of 10. + for i in 0..(damage_len - 1) / 10 { + gl.DrawArrays(ffi::TRIANGLES, 0, 60); + + // Set damage pointer to the next 10 rectangles. + gl.VertexAttribPointer( + self.renderer.solid_program.attrib_position as u32, + 4, + ffi::FLOAT, + ffi::FALSE, + 0, + self.renderer.vertices.as_ptr().add((i + 1) * 60 * 4) as *const _, + ); + } + + // Draw the up to 10 remaining rectangles. + let count = ((damage_len - 1) % 10 + 1) * 6; + gl.DrawArrays(ffi::TRIANGLES, 0, count as i32); + } + + gl.BindTexture(target, 0); + gl.DisableVertexAttribArray(program.attrib_vert as u32); + gl.DisableVertexAttribArray(program.attrib_vert_position as u32); + drop(scope); + + if self.renderer.capabilities.contains(&Capability::Fencing) { + sync_lock.update_read(gl); + } else if self.renderer.egl.is_shared() { + gl.Finish(); + }; + } + + Ok(()) + } + + /// Render a pixel shader into the current target at a given `dest`-region. + #[profiling::function] + #[allow(clippy::too_many_arguments)] + pub fn render_pixel_shader_to( + &mut self, + pixel_shader: &GlesPixelProgram, + src: Rectangle, + dest: Rectangle, + size: Size, + damage: Option<&[Rectangle]>, + alpha: f32, + additional_uniforms: &[Uniform<'_>], + ) -> Result<(), GlesError> { + let fallback_damage = &[Rectangle::from_size(dest.size)]; + let damage = damage.unwrap_or(fallback_damage); + + // prepare the vertices + self.renderer.vertices.clear(); + if self.renderer.capabilities.contains(&Capability::Instancing) { + self.renderer.vertices.extend(damage.iter().flat_map(|rect| { + let dest_size = dest.size; + + let rect_constrained_loc = rect.loc.constrain(Rectangle::from_size(dest_size)); + let rect_clamped_size = rect + .size + .clamp((0, 0), (dest_size.to_point() - rect_constrained_loc).to_size()); + + let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size); + [ + rect.loc.x as f32, + rect.loc.y as f32, + rect.size.w as f32, + rect.size.h as f32, + ] + })); + } else { + self.renderer.vertices.extend(damage.iter().flat_map(|rect| { + let dest_size = dest.size; + + let rect_constrained_loc = rect.loc.constrain(Rectangle::from_size(dest_size)); + let rect_clamped_size = rect + .size + .clamp((0, 0), (dest_size.to_point() - rect_constrained_loc).to_size()); + + let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size); + // Add the 4 f32s per damage rectangle for each of the 6 vertices. + (0..6).flat_map(move |_| { + [ + rect.loc.x as f32, + rect.loc.y as f32, + rect.size.w as f32, + rect.size.h as f32, + ] + }) + })); + } + + if self.renderer.vertices.is_empty() { + return Ok(()); + } + + let mut matrix = Mat3::IDENTITY; + let tex_matrix = build_texture_mat(src, dest, size, Transform::Normal); + + // dest position and scale + matrix *= Mat3::from_translation(Vec2::new(dest.loc.x as f32, dest.loc.y as f32)); + + //apply output transformation + matrix = self.current_projection * matrix; + + let program = if self.renderer.debug_flags.is_empty() { + &pixel_shader.0.normal + } else { + &pixel_shader.0.debug + }; + + // render + let gl = &self.renderer.gl; + unsafe { + let _scope = self + .renderer + .profiler + .scope(gpu_span_location!("render_pixel_shader_to"), gl); + gl.UseProgram(program.program); + + gl.UniformMatrix3fv(program.uniform_matrix, 1, ffi::FALSE, matrix.as_ref().as_ptr()); + gl.UniformMatrix3fv( + program.uniform_tex_matrix, + 1, + ffi::FALSE, + tex_matrix.as_ref().as_ptr(), + ); + gl.Uniform2f(program.uniform_size, size.w as f32, size.h as f32); + gl.Uniform1f(program.uniform_alpha, alpha); + let tint = if self.renderer.debug_flags.contains(DebugFlags::TINT) { + 1.0f32 + } else { + 0.0f32 + }; + + if !self.renderer.debug_flags.is_empty() { + gl.Uniform1f(pixel_shader.0.uniform_tint, tint); + } + + for uniform in additional_uniforms { + let desc = program + .additional_uniforms + .get(&*uniform.name) + .ok_or_else(|| GlesError::UnknownUniform(uniform.name.clone().into_owned()))?; + uniform.value.set(gl, desc)?; + } + + gl.EnableVertexAttribArray(program.attrib_vert as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, self.renderer.vbos[0]); + gl.VertexAttribPointer( + program.attrib_vert as u32, + 2, + ffi::FLOAT, + ffi::FALSE, + 0, + std::ptr::null(), + ); + + // vert_position + gl.EnableVertexAttribArray(program.attrib_position as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, 0); + + gl.VertexAttribPointer( + program.attrib_position as u32, + 4, + ffi::FLOAT, + ffi::FALSE, + 0, + self.renderer.vertices.as_ptr() as *const _, + ); + + let damage_len = damage.len() as i32; + if self.renderer.capabilities.contains(&Capability::Instancing) { + gl.VertexAttribDivisor(program.attrib_vert as u32, 0); + gl.VertexAttribDivisor(program.attrib_position as u32, 1); + + gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len); + } else { + let count = damage_len * 6; + gl.DrawArrays(ffi::TRIANGLES, 0, count); + } + + gl.DisableVertexAttribArray(program.attrib_vert as u32); + gl.DisableVertexAttribArray(program.attrib_position as u32); + } + + Ok(()) + } + + /// Projection matrix for this frame + pub fn projection(&self) -> &[f32; 9] { + self.current_projection.as_ref() + } + + /// Get access to the underlying [`EGLContext`]. + /// + /// *Note*: Modifying the context state, might result in rendering issues. + /// The context state is considered an implementation detail + /// and no guarantee is made about what can or cannot be changed. + /// To make sure a certain modification does not interfere with + /// the renderer's behaviour, check the source. + pub fn egl_context(&self) -> &EGLContext { + self.renderer.egl_context() + } + + /// Returns the supported [`Capabilities`](Capability) of the underlying renderer. + pub fn capabilities(&self) -> &[Capability] { + self.renderer.capabilities() + } + + /// Returns the current enabled [`DebugFlags`] of the underlying renderer. + pub fn debug_flags(&self) -> DebugFlags { + self.renderer.debug_flags() + } +} + +impl Drop for GlesFrame<'_, '_> { + fn drop(&mut self) { + match self.finish_internal() { + Ok(sync) => { + let _ = sync.wait(); // nothing we can do + } + Err(err) => { + warn!("Ignored error finishing GlesFrame on drop: {}", err); + } + } + } +} + +fn build_texture_mat( + src: Rectangle, + dest: Rectangle, + texture: Size, + transform: Transform, +) -> Mat3 { + let dst_src_size = transform.transform_size(src.size); + let scale = dst_src_size.to_f64() / dest.size.to_f64(); + + let mut tex_mat = Affine2::IDENTITY; + + // first bring the damage into src scale + tex_mat = Affine2::from_scale(Vec2::new(scale.x as f32, scale.y as f32)) * tex_mat; + + // then compensate for the texture transform + let transform_mat = transform.matrix(); + let translation = match transform { + Transform::Normal => Affine2::IDENTITY, + Transform::_90 => Affine2::from_translation(Vec2::new(0f32, dst_src_size.w as f32)), + Transform::_180 => Affine2::from_translation(Vec2::new(dst_src_size.w as f32, dst_src_size.h as f32)), + Transform::_270 => Affine2::from_translation(Vec2::new(dst_src_size.h as f32, 0f32)), + Transform::Flipped => Affine2::from_translation(Vec2::new(dst_src_size.w as f32, 0f32)), + Transform::Flipped90 => Affine2::IDENTITY, + Transform::Flipped180 => Affine2::from_translation(Vec2::new(0f32, dst_src_size.h as f32)), + Transform::Flipped270 => { + Affine2::from_translation(Vec2::new(dst_src_size.h as f32, dst_src_size.w as f32)) + } + }; + tex_mat = transform_mat * tex_mat; + tex_mat = translation * tex_mat; + + // now we can add the src crop loc, the size already done implicit by the src size + tex_mat = Affine2::from_translation(Vec2::new(src.loc.x as f32, src.loc.y as f32)) * tex_mat; + + // at last we have to normalize the values for UV space + tex_mat = Affine2::from_scale(Vec2::new( + (1.0f64 / texture.w as f64) as f32, + (1.0f64 / texture.h as f64) as f32, + )) * tex_mat; + + tex_mat.into() +} + +/// Guard type wrapping the underlying [`GlesRenderer`] of a [`GlesFrame`]. +#[derive(Debug)] +pub struct GlesFrameGuard<'a, 'frame, 'buffer> { + renderer: &'a mut &'frame mut GlesRenderer, + target: &'a mut &'frame mut GlesTarget<'buffer>, + old_size: Size, + old_transform: Transform, +} + +impl AsRef for GlesFrameGuard<'_, '_, '_> { + fn as_ref(&self) -> &GlesRenderer { + self.renderer + } +} + +impl AsMut for GlesFrameGuard<'_, '_, '_> { + fn as_mut(&mut self) -> &mut GlesRenderer { + self.renderer + } +} + +impl<'a, 'frame, 'buffer> FrameContext<'a, 'frame, 'buffer, GlesRenderer> for GlesFrame<'frame, 'buffer> +where + 'frame: 'a, +{ + type Guard = GlesFrameGuard<'a, 'frame, 'buffer>; + + fn renderer(&'a mut self) -> Self::Guard { + GlesFrameGuard { + renderer: &mut self.renderer, + target: &mut self.target, + old_size: self.size, + old_transform: self.transform, + } + } +} + +impl Drop for GlesFrameGuard<'_, '_, '_> { + fn drop(&mut self) { + if let Err(err) = self.target.0.make_current(&self.renderer.gl, &self.renderer.egl) { + warn!(?err, "Failed to restore previous render target"); + return; + } + + let mut output_size = self.old_size; + if let Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 = + self.old_transform + { + mem::swap(&mut output_size.w, &mut output_size.h); + } + + unsafe { + self.renderer.gl.Viewport(0, 0, output_size.w, output_size.h); + + self.renderer.gl.Scissor(0, 0, output_size.w, output_size.h); + self.renderer.gl.Enable(ffi::SCISSOR_TEST); + + self.renderer.gl.Enable(ffi::BLEND); + self.renderer.gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA); + } + } +} + +#[cfg(test)] +mod tests { + use super::build_texture_mat; + use crate::utils::{Buffer, Physical, Rectangle, Size, Transform}; + use glam::Vec3; + + #[test] + fn texture_normal_double_size() { + let src: Rectangle = Rectangle::from_size((1000f64, 500f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((1000, 500)); + let transform = Transform::Normal; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(0f32, 0f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(1f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(1f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(0f32, 1f32, 1f32)); + } + + #[test] + fn texture_scaler_crop() { + let src: Rectangle = Rectangle::new((42.5f64, 50.5f64).into(), (110f64, 154f64).into()); + let dest: Rectangle = Rectangle::new((813, 214).into(), (55, 77).into()); + let texture_size: Size = Size::from((842, 674)); + let transform = Transform::Normal; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(0.05047506f32, 0.07492582f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(0.1811164f32, 0.07492582f32, 1f32)); + assert_eq!( + tex_mat * bottom_right, + Vec3::new(0.1811164f32, 0.30341247f32, 1f32) + ); + assert_eq!( + tex_mat * bottom_left, + Vec3::new(0.05047506f32, 0.30341247f32, 1f32) + ); + } + + #[test] + fn texture_normal() { + let src: Rectangle = Rectangle::from_size((500f64, 250f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((500, 250)); + let transform = Transform::Normal; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(0f32, 0f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(1f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(1f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(0f32, 1f32, 1f32)); + } + + #[test] + fn texture_flipped() { + let src: Rectangle = Rectangle::from_size((500f64, 250f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((500, 250)); + let transform = Transform::Flipped; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(1f32, 0f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(0f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(0f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(1f32, 1f32, 1f32)); + } + + #[test] + fn texture_90() { + let src: Rectangle = Rectangle::from_size((250f64, 500f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((250, 500)); + let transform = Transform::_90; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(0f32, 1f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(0f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(1f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(1f32, 1f32, 1f32)); + } + + #[test] + fn texture_180() { + let src: Rectangle = Rectangle::from_size((500f64, 250f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((500, 250)); + let transform = Transform::_180; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(1f32, 1f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(0f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(0f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(1f32, 0f32, 1f32)); + } + + #[test] + fn texture_270() { + let src: Rectangle = Rectangle::from_size((250f64, 500f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((250, 500)); + let transform = Transform::_270; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(1f32, 0f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(1f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(0f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(0f32, 0f32, 1f32)); + } + + #[test] + fn texture_flipped_90() { + let src: Rectangle = Rectangle::from_size((250f64, 500f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((250, 500)); + let transform = Transform::Flipped90; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(0f32, 0f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(0f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(1f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(1f32, 0f32, 1f32)); + } + + #[test] + fn texture_flipped_180() { + let src: Rectangle = Rectangle::from_size((500f64, 250f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((500, 250)); + let transform = Transform::Flipped180; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(0f32, 1f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(1f32, 1f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(1f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(0f32, 0f32, 1f32)); + } + + #[test] + fn texture_flipped_270() { + let src: Rectangle = Rectangle::from_size((250f64, 500f64).into()); + let dest: Rectangle = Rectangle::new((442, 144).into(), (500, 250).into()); + let texture_size: Size = Size::from((250, 500)); + let transform = Transform::Flipped270; + + let tex_mat = build_texture_mat(src, dest, texture_size, transform); + + let top_left = Vec3::new(0f32, 0f32, 1f32); + let top_right = Vec3::new(dest.size.w as f32, 0f32, 1f32); + let bottom_right = Vec3::new(dest.size.w as f32, dest.size.h as f32, 1f32); + let bottom_left = Vec3::new(0f32, dest.size.h as f32, 1f32); + + assert_eq!(tex_mat * top_left, Vec3::new(1f32, 1f32, 1f32)); + assert_eq!(tex_mat * top_right, Vec3::new(1f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_right, Vec3::new(0f32, 0f32, 1f32)); + assert_eq!(tex_mat * bottom_left, Vec3::new(0f32, 1f32, 1f32)); + } +} + +#[cfg(test)] +mod context_activation_tests { + use super::*; + use crate::backend::egl::EGLDisplay; + + // Prefer software rendering so the tests can run without a GPU: Mesa offers a software + // device even on a machine with no DRM node. + fn test_renderer() -> GlesRenderer { + let mut devices = EGLDevice::enumerate() + .expect("EGL device enumeration failed; Mesa provides the software device these tests need") + .collect::>(); + devices.sort_by_key(|device| !device.is_software()); + devices + .into_iter() + .find_map(|device| { + let display = unsafe { EGLDisplay::new(device) }.ok()?; + let context = EGLContext::new(&display).ok()?; + unsafe { GlesRenderer::new(context) }.ok() + }) + .expect("no usable EGL device; Mesa provides the software device these tests need") + } + + fn drain_and_unbind(renderer: &mut GlesRenderer) { + renderer.cleanup_texture_cache().unwrap(); + renderer.egl_context().unbind().unwrap(); + assert!(!renderer.egl_context().is_current()); + } + + #[test] + fn cleanup_texture_cache_leaves_an_idle_context_unbound() { + let mut renderer = test_renderer(); + drain_and_unbind(&mut renderer); + + renderer.cleanup_texture_cache().unwrap(); + + assert!(!renderer.egl_context().is_current()); + } + + #[test] + fn cleanup_texture_cache_frees_a_dropped_texture_from_an_unbound_context() { + let mut renderer = test_renderer(); + let texture = renderer + .import_memory(&[0u8; 4], Fourcc::Argb8888, Size::from((1, 1)), false) + .unwrap(); + let texture_id = texture.tex_id(); + assert_eq!(unsafe { renderer.gl.IsTexture(texture_id) }, ffi::TRUE); + drain_and_unbind(&mut renderer); + drop(texture); + + renderer.cleanup_texture_cache().unwrap(); + + unsafe { renderer.egl_context().make_current().unwrap() }; + assert_eq!(unsafe { renderer.gl.IsTexture(texture_id) }, ffi::FALSE); + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/profiler.rs b/vendor/smithay/src/backend/renderer/gles/profiler.rs new file mode 100644 index 0000000..deb06f1 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/profiler.rs @@ -0,0 +1,328 @@ +//! GPU profiling helpers + +use super::ffi; + +/// GPU profiling span location. +/// +/// Create using the [`crate::gpu_span_location!`] macro. +/// +/// When `tracy_gpu_profiling` feature is enabled, this wraps a Tracy span location. +/// When disabled, this is a zero-sized no-op type. +#[derive(Clone, Copy)] +#[allow(missing_debug_implementations)] // Tracy SpanLocation doesn't impl Debug. +pub struct SpanLocation(#[cfg(feature = "tracy_gpu_profiling")] pub &'static tracy_client::SpanLocation); + +/// Creates a GPU span location for profiling. +/// +/// When `tracy_gpu_profiling` feature is enabled, this wraps `tracy_client::span_location!`. +/// When disabled, returns a no-op placeholder. +#[cfg(feature = "tracy_gpu_profiling")] +#[macro_export] +macro_rules! gpu_span_location { + () => { + $crate::backend::renderer::gles::profiler::SpanLocation( + $crate::reexports::tracy_client::span_location!(), + ) + }; + ($name:expr) => { + $crate::backend::renderer::gles::profiler::SpanLocation( + $crate::reexports::tracy_client::span_location!($name), + ) + }; +} + +/// Creates a GPU span location for profiling. +/// +/// When `tracy_gpu_profiling` feature is enabled, this wraps `tracy_client::span_location!`. +/// When disabled, returns a no-op placeholder. +#[cfg(not(feature = "tracy_gpu_profiling"))] +#[macro_export] +macro_rules! gpu_span_location { + () => { + $crate::backend::renderer::gles::profiler::SpanLocation() + }; + ($name:expr) => { + $crate::backend::renderer::gles::profiler::SpanLocation() + }; +} + +/// An active GPU profiling span. +/// +/// This type represents a GPU profiling span that has been entered but not yet exited. +/// It must be passed to `exit_gpu_span()` when the profiled code section is complete. +/// +/// # Panics +/// +/// Dropping this type without calling `exit_gpu_span()` will panic to ensure GPU profiling +/// spans are properly closed. +#[derive(Debug)] +pub struct GpuSpan { + active: bool, +} + +impl Drop for GpuSpan { + fn drop(&mut self) { + debug_assert!(!self.active, "GPU span must be properly exited"); + } +} + +pub(crate) struct ScopedGpuSpan<'a, 'b> { + span: Option, + profiler: &'a GpuProfiler, + gl: &'b ffi::Gles2, +} + +impl<'a, 'b> Drop for ScopedGpuSpan<'a, 'b> { + fn drop(&mut self) { + let span = self.span.take().unwrap(); + self.profiler.exit(self.gl, span); + } +} + +#[cfg(feature = "tracy_gpu_profiling")] +mod imp { + use std::cell::Cell; + use std::ffi::CStr; + use std::os::raw::c_char; + + use tracing::warn; + + use super::{GpuSpan, SpanLocation, ffi}; + + // Number of timestamp queries in the pool. Limited by Tracy's use of u16 for query IDs. + const MAX_QUERIES: usize = u16::MAX as usize; + + pub struct GpuProfiler { + // `None` means the required GL extension is not supported. + pool: Option, + } + + struct QueryPool { + context: tracy_client::GpuContext, + pool: Vec, + // Index of first free and first pending queries in the pool. + head_tail: Cell<(usize, usize)>, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[repr(transparent)] + struct GpuQuery(ffi::types::GLuint); + + impl QueryPool { + fn new(context: tracy_client::GpuContext, gl: &ffi::Gles2) -> Self { + let mut pool = vec![GpuQuery(0); MAX_QUERIES]; + unsafe { + gl.GenQueriesEXT(pool.len() as ffi::types::GLsizei, pool.as_mut_ptr().cast()); + } + + Self { + context, + pool, + head_tail: Cell::new((0, 0)), + } + } + + fn next(&self, gl: &ffi::Gles2) -> GpuQuery { + let (head, tail) = self.head_tail.get(); + let query = self.pool[head]; + + let new_head = (head + 1) % MAX_QUERIES; + assert_ne!(new_head, tail, "ran out of queries"); + self.head_tail.set((new_head, tail)); + + unsafe { gl.QueryCounterEXT(query.0, ffi::TIMESTAMP_EXT) }; + query + } + + fn collect(&mut self, gl: &ffi::Gles2) { + let (head, tail) = self.head_tail.get_mut(); + if tail == head { + return; + } + profiling::scope!("QueryPool::collect"); + + while tail != head { + let query = self.pool[*tail]; + + let mut available = 0; + unsafe { + while gl.GetError() != ffi::NO_ERROR {} + gl.GetQueryObjectuivEXT(query.0, ffi::QUERY_RESULT_AVAILABLE, &mut available); + if gl.GetError() != ffi::NO_ERROR { + // Don't really have a good way out of this. + available = 1; + } + } + if available == 0 { + return; + } + + let mut timestamp = 0; + unsafe { gl.GetQueryObjecti64vEXT(query.0, ffi::QUERY_RESULT, &mut timestamp) }; + + self.context.upload_gpu_timestamp(query.0 as u16, timestamp); + + *tail = (*tail + 1) % MAX_QUERIES; + } + } + + /// Clean up the timestamp query pool by deleting them. + /// + /// If `gl` is `None` then all queries are leaked. Pass `None` only when failing to obtain + /// the GL context during drop. + fn cleanup(&mut self, gl: Option<&ffi::Gles2>) { + if self.pool.is_empty() { + return; + } + + if let Some(gl) = gl { + unsafe { + gl.DeleteQueriesEXT(self.pool.len() as ffi::types::GLsizei, self.pool.as_ptr().cast()); + } + } + + self.pool.clear(); + } + } + + impl Drop for QueryPool { + fn drop(&mut self) { + debug_assert!(self.pool.is_empty(), "QueryPool must be cleaned up before drop"); + } + } + + impl GpuProfiler { + pub fn new(gl: &ffi::Gles2, extensions: &[String]) -> Self { + let has_timer_query = extensions.iter().any(|ext| ext == "GL_EXT_disjoint_timer_query"); + if !has_timer_query { + warn!("GPU profiling enabled but GL_EXT_disjoint_timer_query is not supported"); + return Self { pool: None }; + } + + let gpu_name = unsafe { CStr::from_ptr(gl.GetString(ffi::RENDERER) as *const c_char).to_str() }; + let gpu_name = gpu_name.unwrap_or("GlesRenderer"); + + let client = tracy_client::Client::start(); + + let mut gpu_timestamp = 0; + unsafe { gl.GetInteger64v(ffi::TIMESTAMP_EXT, &mut gpu_timestamp) }; + + let context = client + .new_gpu_context( + Some(gpu_name), + tracy_client::GpuContextType::OpenGL, + gpu_timestamp, + 1.0, + ) + .unwrap(); + + let pool = QueryPool::new(context, gl); + + Self { pool: Some(pool) } + } + + pub fn enter(&self, span_location: SpanLocation, gl: &ffi::Gles2) -> GpuSpan { + let Some(pool) = &self.pool else { + return GpuSpan { active: false }; + }; + + if !tracy_client::Client::is_connected() { + return GpuSpan { active: false }; + } + + let query = pool.next(gl); + pool.context.begin_span(span_location.0, query.0 as u16); + + GpuSpan { active: true } + } + + pub fn exit(&self, gl: &ffi::Gles2, mut entered: GpuSpan) { + if !entered.active { + return; + } + entered.active = false; + + let Some(pool) = &self.pool else { + return; + }; + + let query = pool.next(gl); + pool.context.end_span(query.0 as u16); + } + + /// Collect completed timestamp queries and send them to the profiler. + /// + /// Must be called regularly to avoid filling up the query pool. A good place is right + /// before a batch of rendering operations. + pub fn collect(&mut self, gl: &ffi::Gles2) { + if let Some(pool) = &mut self.pool { + pool.collect(gl); + } + } + + /// Sync the GPU and CPU times by uploading the current GPU timestamp to the profiler. + /// + /// Necessary to avoid GPU timestamp drift. A good place to call this is right after + /// flushing a batch of rendering operations. + pub fn sync_gpu(&self, gl: &ffi::Gles2) { + let Some(pool) = &self.pool else { + return; + }; + + let mut gpu_timestamp = 0; + unsafe { gl.GetInteger64v(ffi::TIMESTAMP_EXT, &mut gpu_timestamp) }; + pool.context.sync_gpu_time(gpu_timestamp); + } + + /// Clean up the timestamp query pool by deleting them. + /// + /// If `gl` is `None` then all queries are leaked. Pass `None` only when failing to obtain + /// the GL context during drop. + pub fn cleanup(&mut self, gl: Option<&ffi::Gles2>) { + if let Some(pool) = &mut self.pool { + pool.cleanup(gl); + } + } + } +} + +#[cfg(not(feature = "tracy_gpu_profiling"))] +mod imp { + use super::{GpuSpan, SpanLocation, ffi}; + + pub struct GpuProfiler(()); + + impl GpuProfiler { + pub fn new(_gl: &ffi::Gles2, _extensions: &[String]) -> Self { + Self(()) + } + + pub fn enter(&self, _span_location: SpanLocation, _gl: &ffi::Gles2) -> GpuSpan { + GpuSpan { active: true } + } + + pub fn exit(&self, _gl: &ffi::Gles2, mut entered: GpuSpan) { + entered.active = false; + } + + pub fn collect(&mut self, _gl: &ffi::Gles2) {} + + pub fn sync_gpu(&self, _gl: &ffi::Gles2) {} + + pub fn cleanup(&mut self, _gl: Option<&ffi::Gles2>) {} + } +} + +pub(crate) use imp::*; + +impl GpuProfiler { + pub fn scope<'a, 'b>(&'a self, span_location: SpanLocation, gl: &'b ffi::Gles2) -> ScopedGpuSpan<'a, 'b> { + let span = self.enter(span_location, gl); + + ScopedGpuSpan { + span: Some(span), + gl, + profiler: self, + } + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/shaders/implicit/mod.rs b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/mod.rs new file mode 100644 index 0000000..2667306 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/mod.rs @@ -0,0 +1,125 @@ +/// OpenGL Shaders +use crate::backend::renderer::gles::*; + +pub(in super::super) const VERTEX_SHADER: &str = include_str!("./texture.vert"); +pub(in super::super) const FRAGMENT_SHADER: &str = include_str!("./texture.frag"); + +pub(in super::super) const VERTEX_SHADER_SOLID: &str = include_str!("./solid.vert"); +pub(in super::super) const FRAGMENT_SHADER_SOLID: &str = include_str!("./solid.frag"); + +#[derive(Debug)] +pub(in super::super) struct GlesTexProgramInternal { + pub(in super::super) program: ffi::types::GLuint, + pub(in super::super) uniform_tex: ffi::types::GLint, + pub(in super::super) uniform_tex_matrix: ffi::types::GLint, + pub(in super::super) uniform_matrix: ffi::types::GLint, + pub(in super::super) uniform_alpha: ffi::types::GLint, + pub(in super::super) attrib_vert: ffi::types::GLint, + pub(in super::super) attrib_vert_position: ffi::types::GLint, + pub(in super::super) additional_uniforms: HashMap, +} + +#[derive(Debug)] +pub(in super::super) struct GlesTexProgramVariant { + pub(in super::super) normal: GlesTexProgramInternal, + pub(in super::super) debug: GlesTexProgramInternal, + + // debug flags + pub(in super::super) uniform_tint: ffi::types::GLint, +} + +/// Gles texture shader +/// +/// The program can be used with the same [`GlesRenderer`] it was created with, or one using a +/// shared [`EGLContext`]. +#[derive(Debug, Clone)] +pub struct GlesTexProgram(pub(in super::super) Arc); + +#[derive(Debug)] +pub(in super::super) struct GlesTexProgramInner { + pub(in super::super) variants: [GlesTexProgramVariant; 3], + pub(super) destruction_callback_sender: Sender, +} + +impl GlesTexProgram { + pub(in super::super) fn variant_for_format( + &self, + format: Option, + has_alpha: bool, + ) -> &GlesTexProgramVariant { + match format { + Some(ffi::BGRA_EXT) | Some(ffi::RGBA) | Some(ffi::RGBA8) | Some(ffi::RGB10_A2) + | Some(ffi::RGBA16F) => { + if has_alpha { + &self.0.variants[0] + } else { + &self.0.variants[1] + } + } + None => &self.0.variants[2], + _ => panic!("Unknown texture type"), + } + } +} + +impl Drop for GlesTexProgramInner { + fn drop(&mut self) { + for variant in &self.variants { + let _ = self + .destruction_callback_sender + .send(CleanupResource::Program(variant.normal.program)); + let _ = self + .destruction_callback_sender + .send(CleanupResource::Program(variant.debug.program)); + } + } +} + +#[derive(Debug, Clone)] +pub(in super::super) struct GlesSolidProgram { + pub(in super::super) program: ffi::types::GLuint, + pub(in super::super) uniform_matrix: ffi::types::GLint, + pub(in super::super) uniform_color: ffi::types::GLint, + pub(in super::super) attrib_vert: ffi::types::GLint, + pub(in super::super) attrib_position: ffi::types::GLint, +} + +/// Gles pixel shader +/// +/// The program can be used with the same [`GlesRenderer`] it was created with, or one using a +/// shared [`EGLContext`]. +#[derive(Debug, Clone)] +pub struct GlesPixelProgram(pub(in super::super) Arc); + +#[derive(Debug)] +pub(in super::super) struct GlesPixelProgramInner { + pub(in super::super) normal: GlesPixelProgramInternal, + pub(in super::super) debug: GlesPixelProgramInternal, + pub(in super::super) destruction_callback_sender: Sender, + + // debug flags + pub(in super::super) uniform_tint: ffi::types::GLint, +} + +#[derive(Debug)] +pub(in super::super) struct GlesPixelProgramInternal { + pub(in super::super) program: ffi::types::GLuint, + pub(in super::super) uniform_matrix: ffi::types::GLint, + pub(in super::super) uniform_tex_matrix: ffi::types::GLint, + pub(in super::super) uniform_size: ffi::types::GLint, + pub(in super::super) uniform_alpha: ffi::types::GLint, + pub(in super::super) attrib_vert: ffi::types::GLint, + pub(in super::super) attrib_position: ffi::types::GLint, + pub(in super::super) additional_uniforms: HashMap, +} + +impl Drop for GlesPixelProgramInner { + fn drop(&mut self) { + let _ = self + .destruction_callback_sender + .send(CleanupResource::Program(self.normal.program)); + let _ = self + .destruction_callback_sender + .send(CleanupResource::Program(self.debug.program)); + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/shaders/implicit/solid.frag b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/solid.frag new file mode 100644 index 0000000..e14d2ad --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/solid.frag @@ -0,0 +1,8 @@ +#version 100 + +precision mediump float; +uniform vec4 color; + +void main() { + gl_FragColor = color; +} \ No newline at end of file diff --git a/vendor/smithay/src/backend/renderer/gles/shaders/implicit/solid.vert b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/solid.vert new file mode 100644 index 0000000..b4fde16 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/solid.vert @@ -0,0 +1,19 @@ +#version 100 + +uniform mat3 matrix; +attribute vec2 vert; +attribute vec4 position; + +mat2 scale(vec2 scale_vec){ + return mat2( + scale_vec.x, 0.0, + 0.0, scale_vec.y + ); +} + +void main() { + vec2 transform_translation = position.xy; + vec2 transform_scale = position.zw; + vec3 position = vec3(vert * scale(transform_scale) + transform_translation, 1.0); + gl_Position = vec4(matrix * position, 1.0); +} \ No newline at end of file diff --git a/vendor/smithay/src/backend/renderer/gles/shaders/implicit/texture.frag b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/texture.frag new file mode 100644 index 0000000..77fd618 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/texture.frag @@ -0,0 +1,38 @@ +#version 100 + +//_DEFINES_ + +#if defined(EXTERNAL) +#extension GL_OES_EGL_image_external : require +#endif + +precision highp float; +#if defined(EXTERNAL) +uniform samplerExternalOES tex; +#else +uniform sampler2D tex; +#endif + +uniform float alpha; +varying vec2 v_coords; + +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif + +void main() { + vec4 color = texture2D(tex, v_coords); + +#if defined(NO_ALPHA) + color = vec4(color.rgb, 1.0) * alpha; +#else + color = color * alpha; +#endif + +#if defined(DEBUG_FLAGS) + if (tint == 1.0) + color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; +#endif + + gl_FragColor = color; +} \ No newline at end of file diff --git a/vendor/smithay/src/backend/renderer/gles/shaders/implicit/texture.vert b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/texture.vert new file mode 100644 index 0000000..0bf5524 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/shaders/implicit/texture.vert @@ -0,0 +1,24 @@ +#version 100 + +uniform mat3 matrix; +uniform mat3 tex_matrix; + +attribute vec2 vert; +attribute vec4 vert_position; + +varying vec2 v_coords; + +mat2 scale(vec2 scale_vec){ + return mat2( + scale_vec.x, 0.0, + 0.0, scale_vec.y + ); +} + +void main() { + vec2 vert_transform_translation = vert_position.xy; + vec2 vert_transform_scale = vert_position.zw; + vec3 position = vec3(vert * scale(vert_transform_scale) + vert_transform_translation, 1.0); + v_coords = (tex_matrix * position).xy; + gl_Position = vec4(matrix * position, 1.0); +} \ No newline at end of file diff --git a/vendor/smithay/src/backend/renderer/gles/shaders/mod.rs b/vendor/smithay/src/backend/renderer/gles/shaders/mod.rs new file mode 100644 index 0000000..a1ba4a7 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/shaders/mod.rs @@ -0,0 +1,238 @@ +mod implicit; + +use std::fmt::Write; + +pub use implicit::*; + +// define constants +/// No alpha shader define +pub const NO_ALPHA: &str = "NO_ALPHA"; +/// External texture shader define +pub const EXTERNAL: &str = "EXTERNAL"; +/// Debug flags shader define +pub const DEBUG_FLAGS: &str = "DEBUG_FLAGS"; + +use super::*; + +/// Compiles a shader variant. +/// +/// # Safety +/// +/// You must call this only when it is safe to compile shaders with GL. +pub unsafe fn compile_shader( + gl: &ffi::Gles2, + variant: ffi::types::GLuint, + src: &str, +) -> Result { + let shader = gl.CreateShader(variant); + if shader == 0 { + return Err(GlesError::CreateShaderObject); + } + + gl.ShaderSource( + shader, + 1, + &src.as_ptr() as *const *const u8 as *const *const ffi::types::GLchar, + &(src.len() as i32) as *const _, + ); + gl.CompileShader(shader); + + let mut status = ffi::FALSE as i32; + gl.GetShaderiv(shader, ffi::COMPILE_STATUS, &mut status as *mut _); + if status == ffi::FALSE as i32 { + let mut max_len = 0; + gl.GetShaderiv(shader, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _); + + let mut error = Vec::with_capacity(max_len as usize); + let mut len = 0; + gl.GetShaderInfoLog( + shader, + max_len as _, + &mut len as *mut _, + error.as_mut_ptr() as *mut _, + ); + error.set_len(len as usize); + + error!( + "[GL] {}", + std::str::from_utf8(&error).unwrap_or("") + ); + + gl.DeleteShader(shader); + return Err(GlesError::ShaderCompileError); + } + + Ok(shader) +} + +/// Compiles and links a shader program. +/// +/// # Safety +/// +/// You must call this only when it is safe to compile and link shaders with GL. +pub unsafe fn link_program( + gl: &ffi::Gles2, + vert_src: &str, + frag_src: &str, +) -> Result { + let vert = compile_shader(gl, ffi::VERTEX_SHADER, vert_src)?; + let frag = compile_shader(gl, ffi::FRAGMENT_SHADER, frag_src)?; + let program = gl.CreateProgram(); + gl.AttachShader(program, vert); + gl.AttachShader(program, frag); + gl.LinkProgram(program); + gl.DetachShader(program, vert); + gl.DetachShader(program, frag); + gl.DeleteShader(vert); + gl.DeleteShader(frag); + + let mut status = ffi::FALSE as i32; + gl.GetProgramiv(program, ffi::LINK_STATUS, &mut status as *mut _); + if status == ffi::FALSE as i32 { + let mut max_len = 0; + gl.GetProgramiv(program, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _); + + let mut error = Vec::with_capacity(max_len as usize); + let mut len = 0; + gl.GetProgramInfoLog( + program, + max_len as _, + &mut len as *mut _, + error.as_mut_ptr() as *mut _, + ); + error.set_len(len as usize); + + error!( + "[GL] {}", + std::str::from_utf8(&error).unwrap_or("") + ); + + gl.DeleteProgram(program); + return Err(GlesError::ProgramLinkError); + } + + Ok(program) +} + +pub(super) unsafe fn texture_program( + gl: &ffi::Gles2, + src: &str, + additional_uniforms: &[UniformName<'_>], + destruction_callback_sender: Sender, +) -> Result { + let create_variant = |defines: &[&str]| -> Result { + let shader = src.replace( + "//_DEFINES_", + &defines.iter().fold(String::new(), |mut shader, define| { + let _ = writeln!(&mut shader, "#define {define}"); + shader + }), + ); + let debug_shader = src.replace( + "//_DEFINES_", + &defines + .iter() + .chain(&[shaders::DEBUG_FLAGS]) + .fold(String::new(), |mut shader, define| { + let _ = writeln!(shader, "#define {define}"); + shader + }), + ); + + let program = unsafe { link_program(gl, shaders::VERTEX_SHADER, &shader)? }; + let debug_program = unsafe { link_program(gl, shaders::VERTEX_SHADER, debug_shader.as_ref())? }; + + let vert = c"vert"; + let vert_position = c"vert_position"; + let tex = c"tex"; + let matrix = c"matrix"; + let tex_matrix = c"tex_matrix"; + let alpha = c"alpha"; + let tint = c"tint"; + + Ok(GlesTexProgramVariant { + normal: GlesTexProgramInternal { + program, + uniform_tex: gl.GetUniformLocation(program, tex.as_ptr() as *const ffi::types::GLchar), + uniform_matrix: gl.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar), + uniform_tex_matrix: gl + .GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar), + uniform_alpha: gl.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar), + attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar), + attrib_vert_position: gl + .GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar), + additional_uniforms: additional_uniforms + .iter() + .map(|uniform| { + let name = CString::new(uniform.name.as_bytes()).expect("Interior null in name"); + let location = + gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar); + ( + uniform.name.clone().into_owned(), + UniformDesc { + location, + type_: uniform.type_, + }, + ) + }) + .collect(), + }, + debug: GlesTexProgramInternal { + program: debug_program, + uniform_tex: gl.GetUniformLocation(debug_program, tex.as_ptr() as *const ffi::types::GLchar), + uniform_matrix: gl + .GetUniformLocation(debug_program, matrix.as_ptr() as *const ffi::types::GLchar), + uniform_tex_matrix: gl + .GetUniformLocation(debug_program, tex_matrix.as_ptr() as *const ffi::types::GLchar), + uniform_alpha: gl + .GetUniformLocation(debug_program, alpha.as_ptr() as *const ffi::types::GLchar), + attrib_vert: gl.GetAttribLocation(debug_program, vert.as_ptr() as *const ffi::types::GLchar), + attrib_vert_position: gl + .GetAttribLocation(debug_program, vert_position.as_ptr() as *const ffi::types::GLchar), + additional_uniforms: additional_uniforms + .iter() + .map(|uniform| { + let name = CString::new(uniform.name.as_bytes()).expect("Interior null in name"); + let location = + gl.GetUniformLocation(debug_program, name.as_ptr() as *const ffi::types::GLchar); + ( + uniform.name.clone().into_owned(), + UniformDesc { + location, + type_: uniform.type_, + }, + ) + }) + .collect(), + }, + // debug flags + uniform_tint: gl.GetUniformLocation(debug_program, tint.as_ptr() as *const ffi::types::GLchar), + }) + }; + + Ok(GlesTexProgram(Arc::new(GlesTexProgramInner { + variants: [ + create_variant(&[])?, + create_variant(&[shaders::NO_ALPHA])?, + create_variant(&[shaders::EXTERNAL])?, + ], + destruction_callback_sender, + }))) +} + +pub(super) unsafe fn solid_program(gl: &ffi::Gles2) -> Result { + let program = link_program(gl, shaders::VERTEX_SHADER_SOLID, shaders::FRAGMENT_SHADER_SOLID)?; + + let matrix = c"matrix"; + let color = c"color"; + let vert = c"vert"; + let position = c"position"; + + Ok(GlesSolidProgram { + program, + uniform_matrix: gl.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar), + uniform_color: gl.GetUniformLocation(program, color.as_ptr() as *const ffi::types::GLchar), + attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar), + attrib_position: gl.GetAttribLocation(program, position.as_ptr() as *const ffi::types::GLchar), + }) +} diff --git a/vendor/smithay/src/backend/renderer/gles/texture.rs b/vendor/smithay/src/backend/renderer/gles/texture.rs new file mode 100644 index 0000000..a97a94f --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/texture.rs @@ -0,0 +1,231 @@ +use ffi::Gles2; + +use super::*; +use std::sync::Arc; + +/// A handle to a GLES texture +/// +/// The texture can be used with the same [`GlesRenderer`] it was created with, or one using a +/// shared [`EGLContext`]. +#[derive(Debug, Clone)] +pub struct GlesTexture(pub(super) Arc); + +impl GlesTexture { + /// Create a GlesTexture from a raw gl texture id. + /// + /// It is expected to not be external or y_inverted. + /// + /// Ownership over the texture is taken by the renderer, you should not free the texture yourself. + /// + /// # Safety + /// + /// The renderer cannot make sure `tex` is a valid texture id. + pub unsafe fn from_raw( + renderer: &GlesRenderer, + internal_format: Option, + opaque: bool, + tex: ffi::types::GLuint, + size: Size, + ) -> GlesTexture { + GlesTexture(Arc::new(GlesTextureInternal { + texture: tex, + sync: RwLock::default(), + format: internal_format, + has_alpha: !opaque, + is_external: false, + y_inverted: false, + size, + egl_images: None, + destruction_callback_sender: renderer.gles_cleanup().sender.clone(), + })) + } + + /// OpenGL texture id of this texture + /// + /// This id will become invalid, when the GlesTexture is dropped and does not transfer ownership. + pub fn tex_id(&self) -> ffi::types::GLuint { + self.0.texture + } + + /// Whether the texture is upside down + pub fn is_y_inverted(&self) -> bool { + self.0.y_inverted + } + + /// Whether this is the only reference to this texture (strong or weak) + /// + /// Note that this tracks only references to this Smithay object (that you can get by cloning + /// it). If you make a reference via OpenGL directly somehow, you need to keep track of it on + /// your own. + pub fn is_unique_reference(&mut self) -> bool { + Arc::get_mut(&mut self.0).is_some() + } +} + +#[derive(Debug, Default)] +pub(super) struct TextureSync { + read_sync: Mutex>, + write_sync: Mutex>, +} + +unsafe fn wait_for_syncpoint(sync: &mut Option, gl: &Gles2) { + if let Some(sync_obj) = *sync { + match gl.ClientWaitSync(sync_obj, 0, 0) { + ffi::ALREADY_SIGNALED | ffi::CONDITION_SATISFIED => { + let _ = sync.take(); + gl.DeleteSync(sync_obj); + } + _ => { + gl.WaitSync(sync_obj, 0, ffi::TIMEOUT_IGNORED); + } + }; + } +} + +impl TextureSync { + pub(super) fn wait_for_upload(&self, gl: &Gles2) { + unsafe { + wait_for_syncpoint(&mut self.write_sync.lock().unwrap(), gl); + } + } + + pub(super) fn update_read(&self, gl: &Gles2) { + let mut read_sync = self.read_sync.lock().unwrap(); + if let Some(old) = read_sync.take() { + unsafe { + gl.WaitSync(old, 0, ffi::TIMEOUT_IGNORED); + gl.DeleteSync(old); + }; + } + *read_sync = Some(unsafe { gl.FenceSync(ffi::SYNC_GPU_COMMANDS_COMPLETE, 0) }); + } + + pub(super) fn wait_for_all(&mut self, gl: &Gles2) { + unsafe { + wait_for_syncpoint(self.read_sync.get_mut().unwrap(), gl); + wait_for_syncpoint(self.write_sync.get_mut().unwrap(), gl); + } + } + + pub(super) fn update_write(&mut self, gl: &Gles2) { + let write_sync = self.write_sync.get_mut().unwrap(); + if let Some(old) = write_sync.take() { + unsafe { + gl.WaitSync(old, 0, ffi::TIMEOUT_IGNORED); + gl.DeleteSync(old); + }; + } + + *write_sync = Some(unsafe { gl.FenceSync(ffi::SYNC_GPU_COMMANDS_COMPLETE, 0) }); + } +} + +#[derive(Debug)] +pub(super) struct GlesTextureInternal { + pub(super) texture: ffi::types::GLuint, + pub(super) sync: RwLock, + pub(super) format: Option, + pub(super) has_alpha: bool, + pub(super) is_external: bool, + pub(super) y_inverted: bool, + pub(super) size: Size, + pub(super) egl_images: Option>, + pub(super) destruction_callback_sender: Sender, +} +unsafe impl Send for GlesTextureInternal {} +unsafe impl Sync for GlesTextureInternal {} + +impl Drop for GlesTextureInternal { + fn drop(&mut self) { + let _ = self + .destruction_callback_sender + .send(CleanupResource::Texture(self.texture)); + let mut sync = self.sync.write().unwrap(); + if let Some(sync) = sync.read_sync.get_mut().unwrap().take() { + let _ = self + .destruction_callback_sender + .send(CleanupResource::Sync(sync as *const _)); + } + if let Some(sync) = sync.write_sync.get_mut().unwrap().take() { + let _ = self + .destruction_callback_sender + .send(CleanupResource::Sync(sync as *const _)); + } + if let Some(images) = self.egl_images.take() { + for image in images { + let _ = self + .destruction_callback_sender + .send(CleanupResource::EGLImage(image)); + } + } + } +} + +impl Texture for GlesTexture { + fn width(&self) -> u32 { + self.0.size.w as u32 + } + fn height(&self) -> u32 { + self.0.size.h as u32 + } + fn size(&self) -> Size { + self.0.size + } + fn format(&self) -> Option { + let fmt = gl_internal_format_to_fourcc(self.0.format?); + if self.0.has_alpha { + fmt + } else { + fmt.and_then(get_opaque) + } + } +} + +/// Texture mapping of a GLES2 texture +#[derive(Debug)] +pub struct GlesMapping { + pub(super) pbo: ffi::types::GLuint, + pub(super) format: ffi::types::GLenum, + pub(super) layout: ffi::types::GLenum, + pub(super) has_alpha: bool, + pub(super) size: Size, + pub(super) mapping: AtomicPtr, + pub(super) destruction_callback_sender: Sender, +} + +impl Texture for GlesMapping { + fn width(&self) -> u32 { + self.size.w as u32 + } + fn height(&self) -> u32 { + self.size.h as u32 + } + fn size(&self) -> Size { + self.size + } + fn format(&self) -> Option { + let fmt = gl_read_format_to_fourcc(self.format, self.layout); + if self.has_alpha { + fmt + } else { + fmt.and_then(get_opaque) + } + } +} +impl TextureMapping for GlesMapping { + fn flipped(&self) -> bool { + true + } + fn format(&self) -> Fourcc { + Texture::format(self).expect("Should never happen") + } +} + +impl Drop for GlesMapping { + fn drop(&mut self) { + let _ = self.destruction_callback_sender.send(CleanupResource::Mapping( + self.pbo, + self.mapping.load(Ordering::SeqCst), + )); + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/uniform.rs b/vendor/smithay/src/backend/renderer/gles/uniform.rs new file mode 100644 index 0000000..ae212f4 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/uniform.rs @@ -0,0 +1,497 @@ +use std::borrow::Cow; + +use super::GlesError; + +/// Different value types of a shader uniform variable for the [`GlesRenderer`](super::GlesRenderer). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UniformType { + /// A single float + _1f, + /// Two floats + _2f, + /// Three floats + _3f, + /// Four floats + _4f, + /// A single signed integer + _1i, + /// Two signed integers + _2i, + /// Three signed integers + _3i, + /// Four signed integers + _4i, + /// A single unsigned integer + _1ui, + /// Two unsigned integers + _2ui, + /// Three unsigned integers + _3ui, + /// Four unsigned integers + _4ui, + /// 2x2 matrices + Matrix2x2, + /// 2x3 matrices + Matrix2x3, + /// 2x4 matrices + Matrix2x4, + /// 3x2 matrices + Matrix3x2, + /// 3x3 matrices + Matrix3x3, + /// 3x4 matrices + Matrix3x4, + /// 4x2 matrices + Matrix4x2, + /// 4x3 matrices + Matrix4x3, + /// 4x4 matrices + Matrix4x4, +} + +/// GL location and type of a uniform shader variable +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UniformDesc { + /// GL location of the uniform + pub location: super::ffi::types::GLint, + /// type of the uniform + pub type_: UniformType, +} + +/// A shader uniform variable consisting out of a name and value +#[derive(Debug, Clone, PartialEq)] +pub struct Uniform<'a> { + /// name of the uniform + pub name: Cow<'a, str>, + /// value of the uniform + pub value: UniformValue, +} + +/// A description of a uniform shader variable consisting out of a name and type +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UniformName<'a> { + /// name of the uniform + pub name: Cow<'a, str>, + /// type of the uniform + pub type_: UniformType, +} + +impl<'a> Uniform<'a> { + /// Create a new uniform variable value + pub fn new(name: impl Into>, value: impl Into) -> Uniform<'a> { + Uniform { + name: name.into(), + value: value.into(), + } + } + + /// Convert the uniform to a static lifetime, cloning the contents + pub fn to_owned(&self) -> Uniform<'static> { + Uniform { + name: Cow::Owned(self.name.clone().into_owned()), + value: self.value.clone(), + } + } + + /// Convert the uniform to a static lifetime, cloning the name if necessary + pub fn into_owned(self) -> Uniform<'static> { + Uniform { + name: Cow::Owned(self.name.into_owned()), + value: self.value, + } + } +} + +impl<'a> UniformName<'a> { + /// Create a new uniform variable description + pub fn new(name: impl Into>, type_: UniformType) -> UniformName<'a> { + UniformName { + name: name.into(), + type_, + } + } + + /// Convert the uniform name to a static lifetime, cloning the contents + pub fn to_owned(&self) -> UniformName<'static> { + UniformName { + name: Cow::Owned(self.name.clone().into_owned()), + type_: self.type_, + } + } + + /// Convert the uniform name to a static lifetime, cloning the name if necessary + pub fn into_owned(self) -> UniformName<'static> { + UniformName { + name: Cow::Owned(self.name.into_owned()), + type_: self.type_, + } + } +} + +/// Value of a uniform variable +#[derive(Debug, Clone, PartialEq)] +pub enum UniformValue { + /// A single float + _1f(f32), + /// Two floats + _2f(f32, f32), + /// Three floats + _3f(f32, f32, f32), + /// Four floats + _4f(f32, f32, f32, f32), + /// A single signed integer + _1i(i32), + /// Two signed integers + _2i(i32, i32), + /// Three signed integers + _3i(i32, i32, i32), + /// Four signed integers + _4i(i32, i32, i32, i32), + /// A single unsigned integer + _1ui(u32), + /// Two unsigned integers + _2ui(u32, u32), + /// Three signed integers + _3ui(u32, u32, u32), + /// Four signed integers + _4ui(u32, u32, u32, u32), + /// 2x2 matrices + Matrix2x2 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 4]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 2x3 matrices + Matrix2x3 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 6]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 2x4 matrices + Matrix2x4 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 8]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 3x2 matrices + Matrix3x2 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 6]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 3x3 matrices + Matrix3x3 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 9]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 3x4 matrices + Matrix3x4 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 12]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 4x2 matrices + Matrix4x2 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 8]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 4x3 matrices + Matrix4x3 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 12]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, + /// 4x4 matrices + Matrix4x4 { + /// Matrices supplied as [f32]-arrays + matrices: Vec<[f32; 16]>, + /// If transpose is `false`, each matrix is assumed to be supplied in column major order. + /// If transpose is `true`, each matrix is assumed to be supplied in row major order. + transpose: bool, + }, +} + +impl UniformValue { + /// Checks if the contexts of this value match the provided uniform type + pub fn matches(&self, type_: &UniformType) -> bool { + *type_ == self.type_() + } + + /// Returns the type of this uniform value + pub fn type_(&self) -> UniformType { + match self { + UniformValue::_1f(_) => UniformType::_1f, + UniformValue::_2f(_, _) => UniformType::_2f, + UniformValue::_3f(_, _, _) => UniformType::_3f, + UniformValue::_4f(_, _, _, _) => UniformType::_4f, + UniformValue::_1i(_) => UniformType::_1i, + UniformValue::_2i(_, _) => UniformType::_2i, + UniformValue::_3i(_, _, _) => UniformType::_3i, + UniformValue::_4i(_, _, _, _) => UniformType::_4i, + UniformValue::_1ui(_) => UniformType::_1ui, + UniformValue::_2ui(_, _) => UniformType::_2ui, + UniformValue::_3ui(_, _, _) => UniformType::_3ui, + UniformValue::_4ui(_, _, _, _) => UniformType::_4ui, + UniformValue::Matrix2x2 { .. } => UniformType::Matrix2x2, + UniformValue::Matrix2x3 { .. } => UniformType::Matrix2x3, + UniformValue::Matrix2x4 { .. } => UniformType::Matrix2x4, + UniformValue::Matrix3x2 { .. } => UniformType::Matrix3x2, + UniformValue::Matrix3x3 { .. } => UniformType::Matrix3x3, + UniformValue::Matrix3x4 { .. } => UniformType::Matrix3x4, + UniformValue::Matrix4x2 { .. } => UniformType::Matrix4x2, + UniformValue::Matrix4x3 { .. } => UniformType::Matrix4x3, + UniformValue::Matrix4x4 { .. } => UniformType::Matrix4x4, + } + } + + /// Sets the `desc` uniform to this value. + /// + /// # Safety + /// + /// You have to make sure to pass a valid `UniformDesc`, and to only call this function when it + /// is otherwise safe to call `gl.Uniform()` series of methods. + pub unsafe fn set(&self, gl: &super::ffi::Gles2, desc: &UniformDesc) -> Result<(), GlesError> { + if !self.matches(&desc.type_) { + return Err(GlesError::UniformTypeMismatch { + provided: self.type_(), + declared: desc.type_, + }); + } + + match self { + UniformValue::_1f(v0) => unsafe { gl.Uniform1f(desc.location, *v0) }, + UniformValue::_2f(v0, v1) => unsafe { gl.Uniform2f(desc.location, *v0, *v1) }, + UniformValue::_3f(v0, v1, v2) => unsafe { gl.Uniform3f(desc.location, *v0, *v1, *v2) }, + UniformValue::_4f(v0, v1, v2, v3) => unsafe { gl.Uniform4f(desc.location, *v0, *v1, *v2, *v3) }, + UniformValue::_1i(v0) => unsafe { gl.Uniform1i(desc.location, *v0) }, + UniformValue::_2i(v0, v1) => unsafe { gl.Uniform2i(desc.location, *v0, *v1) }, + UniformValue::_3i(v0, v1, v2) => unsafe { gl.Uniform3i(desc.location, *v0, *v1, *v2) }, + UniformValue::_4i(v0, v1, v2, v3) => unsafe { gl.Uniform4i(desc.location, *v0, *v1, *v2, *v3) }, + UniformValue::_1ui(v0) => unsafe { gl.Uniform1ui(desc.location, *v0) }, + UniformValue::_2ui(v0, v1) => unsafe { gl.Uniform2ui(desc.location, *v0, *v1) }, + UniformValue::_3ui(v0, v1, v2) => unsafe { gl.Uniform3ui(desc.location, *v0, *v1, *v2) }, + UniformValue::_4ui(v0, v1, v2, v3) => unsafe { gl.Uniform4ui(desc.location, *v0, *v1, *v2, *v3) }, + UniformValue::Matrix2x2 { matrices, transpose } => unsafe { + gl.UniformMatrix2fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix2x3 { matrices, transpose } => unsafe { + gl.UniformMatrix2x3fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix2x4 { matrices, transpose } => unsafe { + gl.UniformMatrix2x4fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix3x2 { matrices, transpose } => unsafe { + gl.UniformMatrix3x2fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix3x3 { matrices, transpose } => unsafe { + gl.UniformMatrix3fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix3x4 { matrices, transpose } => unsafe { + gl.UniformMatrix3x4fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix4x2 { matrices, transpose } => unsafe { + gl.UniformMatrix4x2fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix4x3 { matrices, transpose } => unsafe { + gl.UniformMatrix4x3fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + UniformValue::Matrix4x4 { matrices, transpose } => unsafe { + gl.UniformMatrix4fv( + desc.location, + matrices.len() as i32, + *transpose as u8, + matrices.as_ptr() as *const _, + ) + }, + }; + + Ok(()) + } +} + +impl From for UniformValue { + #[inline] + fn from(v: f32) -> Self { + UniformValue::_1f(v) + } +} +impl From<(f32, f32)> for UniformValue { + #[inline] + fn from((v1, v2): (f32, f32)) -> Self { + UniformValue::_2f(v1, v2) + } +} +impl From<(f32, f32, f32)> for UniformValue { + #[inline] + fn from((v1, v2, v3): (f32, f32, f32)) -> Self { + UniformValue::_3f(v1, v2, v3) + } +} +impl From<(f32, f32, f32, f32)> for UniformValue { + #[inline] + fn from((v1, v2, v3, v4): (f32, f32, f32, f32)) -> Self { + UniformValue::_4f(v1, v2, v3, v4) + } +} +impl From<[f32; 2]> for UniformValue { + #[inline] + fn from(v: [f32; 2]) -> Self { + UniformValue::_2f(v[0], v[1]) + } +} +impl From<[f32; 3]> for UniformValue { + #[inline] + fn from(v: [f32; 3]) -> Self { + UniformValue::_3f(v[0], v[1], v[2]) + } +} +impl From<[f32; 4]> for UniformValue { + #[inline] + fn from(v: [f32; 4]) -> Self { + UniformValue::_4f(v[0], v[1], v[2], v[3]) + } +} + +impl From for UniformValue { + #[inline] + fn from(v: i32) -> Self { + UniformValue::_1i(v) + } +} +impl From<(i32, i32)> for UniformValue { + #[inline] + fn from((v1, v2): (i32, i32)) -> Self { + UniformValue::_2i(v1, v2) + } +} +impl From<(i32, i32, i32)> for UniformValue { + #[inline] + fn from((v1, v2, v3): (i32, i32, i32)) -> Self { + UniformValue::_3i(v1, v2, v3) + } +} +impl From<(i32, i32, i32, i32)> for UniformValue { + #[inline] + fn from((v1, v2, v3, v4): (i32, i32, i32, i32)) -> Self { + UniformValue::_4i(v1, v2, v3, v4) + } +} +impl From<[i32; 2]> for UniformValue { + #[inline] + fn from(v: [i32; 2]) -> Self { + UniformValue::_2i(v[0], v[1]) + } +} +impl From<[i32; 3]> for UniformValue { + #[inline] + fn from(v: [i32; 3]) -> Self { + UniformValue::_3i(v[0], v[1], v[2]) + } +} +impl From<[i32; 4]> for UniformValue { + #[inline] + fn from(v: [i32; 4]) -> Self { + UniformValue::_4i(v[0], v[1], v[2], v[3]) + } +} + +impl From for UniformValue { + #[inline] + fn from(v: u32) -> Self { + UniformValue::_1ui(v) + } +} +impl From<(u32, u32)> for UniformValue { + #[inline] + fn from((v1, v2): (u32, u32)) -> Self { + UniformValue::_2ui(v1, v2) + } +} +impl From<(u32, u32, u32)> for UniformValue { + #[inline] + fn from((v1, v2, v3): (u32, u32, u32)) -> Self { + UniformValue::_3ui(v1, v2, v3) + } +} +impl From<(u32, u32, u32, u32)> for UniformValue { + #[inline] + fn from((v1, v2, v3, v4): (u32, u32, u32, u32)) -> Self { + UniformValue::_4ui(v1, v2, v3, v4) + } +} +impl From<[u32; 2]> for UniformValue { + #[inline] + fn from(v: [u32; 2]) -> Self { + UniformValue::_2ui(v[0], v[1]) + } +} +impl From<[u32; 3]> for UniformValue { + #[inline] + fn from(v: [u32; 3]) -> Self { + UniformValue::_3ui(v[0], v[1], v[2]) + } +} +impl From<[u32; 4]> for UniformValue { + #[inline] + fn from(v: [u32; 4]) -> Self { + UniformValue::_4ui(v[0], v[1], v[2], v[3]) + } +} diff --git a/vendor/smithay/src/backend/renderer/gles/version.rs b/vendor/smithay/src/backend/renderer/gles/version.rs new file mode 100644 index 0000000..735a6e7 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/gles/version.rs @@ -0,0 +1,144 @@ +use std::{ + ffi::{CStr, CString}, + os::raw::c_char, +}; + +use super::ffi::{self, Gles2}; + +pub const GLES_3_0: GlVersion = GlVersion::new(3, 0); +pub const GLES_2_0: GlVersion = GlVersion::new(2, 0); + +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct GlVersion { + pub major: i32, + pub minor: i32, +} + +impl GlVersion { + pub const fn new(major: i32, minor: i32) -> Self { + GlVersion { major, minor } + } +} + +impl Eq for GlVersion {} + +impl Ord for GlVersion { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.major.cmp(&other.major) { + std::cmp::Ordering::Equal => self.minor.cmp(&other.minor), + ord => ord, + } + } +} + +impl PartialOrd for GlVersion { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Invalid version string {0:?}")] +pub struct GlVersionParseError(CString); + +impl TryFrom<&CStr> for GlVersion { + type Error = GlVersionParseError; + + fn try_from(value: &CStr) -> Result { + let mut bytes = value.to_bytes(); + + let prefix = b"OpenGL ES "; + if bytes.starts_with(prefix) { + // Strip the prefix + bytes = &bytes[prefix.len()..]; + } + + let ascii_to_int = |ch: u8| (ch - b'0') as u32; + + let mut iter = bytes.iter(); + + let mut major: Option = None; + let mut minor: Option = None; + + for v in &mut iter { + if v.is_ascii_digit() { + major = Some(major.unwrap_or(0) * 10 + ascii_to_int(*v)); + } else if *v == b'.' { + break; + } else { + // Neither digit nor '.', so let's assume invalid string + return Err(GlVersionParseError(value.to_owned())); + } + } + + for v in iter { + if v.is_ascii_digit() { + minor = Some(minor.unwrap_or(0) * 10 + ascii_to_int(*v)); + } else { + break; + } + } + + major + .zip(minor) + .map(|(major, minor)| GlVersion::new(major as i32, minor as i32)) + .ok_or_else(|| GlVersionParseError(value.to_owned())) + } +} + +impl TryFrom<&Gles2> for GlVersion { + type Error = GlVersionParseError; + + fn try_from(value: &Gles2) -> Result { + let version = unsafe { CStr::from_ptr(value.GetString(ffi::VERSION) as *const c_char) }; + GlVersion::try_from(version) + } +} + +#[cfg(test)] +mod tests { + use super::GlVersion; + use std::{convert::TryFrom, ffi::CStr, os::raw::c_char}; + + #[test] + fn test_parse_1234_4321() { + let gl_version = b"1234.4321 Mesa 20.3.5\0"; + let gl_version_str = unsafe { CStr::from_ptr(gl_version.as_ptr() as *const c_char) }; + assert_eq!( + GlVersion::try_from(gl_version_str).unwrap(), + GlVersion::new(1234, 4321) + ) + } + + #[test] + fn test_parse_mesa_3_2() { + let gl_version = b"OpenGL ES 3.2 Mesa 20.3.5\0"; + let gl_version_str = unsafe { CStr::from_ptr(gl_version.as_ptr() as *const c_char) }; + assert_eq!(GlVersion::try_from(gl_version_str).unwrap(), GlVersion::new(3, 2)) + } + + #[test] + fn test_3_2_greater_3_0() { + assert!(GlVersion::new(3, 2) > GlVersion::new(3, 0)) + } + + #[test] + fn test_3_0_greater_or_equal_3_0() { + assert!(GlVersion::new(3, 0) >= GlVersion::new(3, 0)) + } + + #[test] + fn test_3_0_less_or_equal_3_0() { + assert!(GlVersion::new(3, 0) <= GlVersion::new(3, 0)) + } + + #[test] + fn test_3_0_eq_3_0() { + assert!(GlVersion::new(3, 0) == GlVersion::new(3, 0)) + } + + #[test] + fn test_2_0_less_3_0() { + assert!(GlVersion::new(2, 0) < GlVersion::new(3, 0)) + } +} diff --git a/vendor/smithay/src/backend/renderer/glow.rs b/vendor/smithay/src/backend/renderer/glow.rs new file mode 100644 index 0000000..167ac4f --- /dev/null +++ b/vendor/smithay/src/backend/renderer/glow.rs @@ -0,0 +1,670 @@ +#![allow(unsafe_op_in_unsafe_fn)] + +//! Integration for using [`glow`] on top of smithays OpenGL ES 2 renderer +use tracing::warn; + +#[cfg(feature = "wayland_frontend")] +use crate::backend::renderer::{ImportDmaWl, ImportMemWl}; +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +use crate::backend::{egl::display::EGLBufferReader, renderer::ImportEgl}; +use crate::{ + backend::{ + allocator::{Format, Fourcc, dmabuf::Dmabuf, format::FormatSet}, + egl::EGLContext, + renderer::{ + Bind, Blit, BlitFrame, Color32F, DebugFlags, ExportMem, ImportDma, ImportMem, Offscreen, + Renderer, RendererSuper, TextureFilter, + element::UnderlyingStorage, + gles::{element::*, *}, + sync, + }, + }, + utils::{Buffer as BufferCoord, Physical, Rectangle, Size, Transform, user_data::UserDataMap}, +}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::{wl_buffer, wl_shm}; + +use glow::Context; +use std::{ + borrow::{Borrow, BorrowMut}, + sync::Arc, +}; + +use super::{ContextId, Frame, FrameContext, element::RenderElement}; + +#[derive(Debug)] +/// A renderer utilizing OpenGL ES 2 and [`glow`] on top for easier custom rendering. +pub struct GlowRenderer { + gl: GlesInternal, + glow: Arc, +} + +#[derive(Debug)] +enum GlesInternal { + Owned(Box), + Frame(GlesFrameGuard<'static, 'static, 'static>), +} + +impl AsRef for GlesInternal { + fn as_ref(&self) -> &GlesRenderer { + match self { + Self::Owned(r) => r, + Self::Frame(g) => g.as_ref(), + } + } +} + +impl AsMut for GlesInternal { + fn as_mut(&mut self) -> &mut GlesRenderer { + match self { + Self::Owned(r) => r, + Self::Frame(g) => g.as_mut(), + } + } +} + +#[derive(Debug)] +/// [`Frame`] implementation of a [`GlowRenderer`]. +/// +/// Leaking the frame will cause the same problems as leaking a [`GlesFrame`]. +pub struct GlowFrame<'frame, 'buffer> { + frame: Option>, + glow: Arc, +} + +impl GlowRenderer { + /// Get the supported [`Capabilities`](Capability) of the renderer + /// + /// # Safety + /// + /// This operation will cause undefined behavior if the given EGLContext is active in another thread. + pub unsafe fn supported_capabilities(context: &EGLContext) -> Result, GlesError> { + GlesRenderer::supported_capabilities(context) + } + + /// Creates a new OpenGL ES 2 + Glow renderer from a given [`EGLContext`] + /// with all [`supported capabilities`](Self::supported_capabilities). + /// + /// # Safety + /// + /// This operation will cause undefined behavior if the given EGLContext is active in another thread. + /// + /// See: [`with_capabilities`](Self::with_capabilities) for more information + pub unsafe fn new(context: EGLContext) -> Result { + let supported_capabilities = Self::supported_capabilities(&context)?; + Self::with_capabilities(context, supported_capabilities) + } + + /// Creates a new OpenGL ES 2 + Glow renderer from a given [`EGLContext`] + /// with the specified [`Capabilities`](Capability). If a requested [`Capability`] is not supported an + /// error will be returned. + /// + /// # Safety + /// + /// This operation will cause undefined behavior if the given EGLContext is active in another thread. + /// + /// # Implementation details + /// + /// - Texture handles created by the resulting renderer are valid for every rendered created with an + /// `EGLContext` shared with the given one (see `EGLContext::new_shared`) and can be used on + /// any of these renderers. + /// - This renderer has no default framebuffer, use `Bind::bind` before rendering. + /// - Binding a new target, while another one is already bound, will replace the current target. + /// - Shm buffers can be released after a successful import, without the texture handle becoming invalid. + /// - Texture filtering starts with Linear-downscaling and Linear-upscaling + pub unsafe fn with_capabilities( + context: EGLContext, + capabilities: impl IntoIterator, + ) -> Result { + let glow = { + context.make_current()?; + Context::from_loader_function(|s| crate::backend::egl::get_proc_address(s) as *const _) + }; + let gl = GlesRenderer::with_capabilities(context, capabilities)?; + + Ok(GlowRenderer { + gl: GlesInternal::Owned(Box::new(gl)), + glow: Arc::new(glow), + }) + } + + /// Get access to the underlying [`EGLContext`]. + /// + /// *Note*: Modifying the context state, might result in rendering issues. + /// The context state is considered an implementation detail + /// and no guarantee is made about what can or cannot be changed. + /// To make sure a certain modification does not interfere with + /// the renderer's behaviour, check the source. + pub fn egl_context(&self) -> &EGLContext { + self.gl.as_ref().egl_context() + } + + /// Run custom code in the GL context owned by this renderer. + /// + /// The OpenGL state of the renderer is considered an implementation detail + /// and no guarantee is made about what can or cannot be changed, + /// as such you should reset everything you change back to its previous value + /// or check the source code of the version of Smithay you are using to ensure + /// your changes don't interfere with the renderer's behavior. + /// Doing otherwise can lead to rendering errors while using other functions of this renderer. + pub fn with_context(&mut self, func: F) -> Result + where + F: FnOnce(&Arc) -> R, + { + unsafe { + self.gl.as_ref().egl_context().make_current()?; + } + Ok(func(&self.glow)) + } +} + +impl GlowFrame<'_, '_> { + /// Run custom code in the GL context owned by this renderer. + /// + /// The OpenGL state of the renderer is considered an implementation detail + /// and no guarantee is made about what can or cannot be changed, + /// as such you should reset everything you change back to its previous value + /// or check the source code of the version of Smithay you are using to ensure + /// your changes don't interfere with the renderer's behavior. + /// Doing otherwise can lead to rendering errors while using other functions of this renderer. + pub fn with_context(&mut self, func: F) -> Result + where + F: FnOnce(&Arc) -> R, + { + Ok(func(&self.glow)) + } +} + +// TODO: When GAT, use TryFrom and be generic over the Error, +// so `TryFrom for GlesRenderer` qualifies +// just as `TryFrom for GlowRenderer` +impl From for GlowRenderer { + #[inline] + fn from(renderer: GlesRenderer) -> GlowRenderer { + let glow = unsafe { + renderer.egl_context().make_current().unwrap(); + Context::from_loader_function(|s| crate::backend::egl::get_proc_address(s) as *const _) + }; + + GlowRenderer { + gl: GlesInternal::Owned(Box::new(renderer)), + glow: Arc::new(glow), + } + } +} + +impl Borrow for GlowRenderer { + #[inline] + fn borrow(&self) -> &GlesRenderer { + self.gl.as_ref() + } +} + +impl BorrowMut for GlowRenderer { + #[inline] + fn borrow_mut(&mut self) -> &mut GlesRenderer { + self.gl.as_mut() + } +} + +impl<'frame, 'buffer> Borrow> for GlowFrame<'frame, 'buffer> { + #[inline] + fn borrow(&self) -> &GlesFrame<'frame, 'buffer> { + self.frame.as_ref().unwrap() + } +} + +impl<'frame, 'buffer> BorrowMut> for GlowFrame<'frame, 'buffer> { + #[inline] + fn borrow_mut(&mut self) -> &mut GlesFrame<'frame, 'buffer> { + self.frame.as_mut().unwrap() + } +} + +impl RendererSuper for GlowRenderer { + type Error = GlesError; + type TextureId = GlesTexture; + type Framebuffer<'buffer> = GlesTarget<'buffer>; + type Frame<'frame, 'buffer> + = GlowFrame<'frame, 'buffer> + where + 'buffer: 'frame, + Self: 'frame; +} + +impl Renderer for GlowRenderer { + fn context_id(&self) -> ContextId { + self.gl.as_ref().context_id() + } + + fn downscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.gl.as_mut().downscale_filter(filter) + } + fn upscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.gl.as_mut().upscale_filter(filter) + } + + fn set_debug_flags(&mut self, flags: DebugFlags) { + self.gl.as_mut().set_debug_flags(flags) + } + fn debug_flags(&self) -> DebugFlags { + self.gl.as_ref().debug_flags() + } + + #[profiling::function] + fn render<'frame, 'buffer>( + &'frame mut self, + target: &'frame mut GlesTarget<'buffer>, + output_size: Size, + transform: Transform, + ) -> Result, Self::Error> + where + 'buffer: 'frame, + { + let glow = self.glow.clone(); + let frame = self.gl.as_mut().render(target, output_size, transform)?; + Ok(GlowFrame { + frame: Some(frame), + glow, + }) + } + + #[profiling::function] + fn wait(&mut self, sync: &sync::SyncPoint) -> Result<(), Self::Error> { + self.gl.as_mut().wait(sync) + } + + #[profiling::function] + fn cleanup_texture_cache(&mut self) -> Result<(), Self::Error> { + self.gl.as_mut().cleanup_texture_cache() + } + + #[profiling::function] + fn invalidate_caches(&mut self) -> Result<(), Self::Error> { + self.gl.as_mut().invalidate_caches() + } +} + +impl Frame for GlowFrame<'_, '_> { + type Error = GlesError; + type TextureId = GlesTexture; + + fn context_id(&self) -> ContextId { + self.frame.as_ref().unwrap().context_id() + } + + #[profiling::function] + fn clear(&mut self, color: Color32F, at: &[Rectangle]) -> Result<(), Self::Error> { + self.frame.as_mut().unwrap().clear(color, at) + } + + #[profiling::function] + fn draw_solid( + &mut self, + dst: Rectangle, + damage: &[Rectangle], + color: Color32F, + ) -> Result<(), Self::Error> { + self.frame.as_mut().unwrap().draw_solid(dst, damage, color) + } + + #[profiling::function] + fn render_texture_from_to( + &mut self, + texture: &Self::TextureId, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + src_transform: Transform, + alpha: f32, + ) -> Result<(), Self::Error> { + Frame::render_texture_from_to( + self.frame.as_mut().unwrap(), + texture, + src, + dst, + damage, + opaque_regions, + src_transform, + alpha, + ) + } + + fn transformation(&self) -> Transform { + self.frame.as_ref().unwrap().transformation() + } + fn output_size(&self) -> Size { + self.frame.as_ref().unwrap().output_size() + } + + #[profiling::function] + fn render_texture_at( + &mut self, + texture: &Self::TextureId, + pos: crate::utils::Point, + texture_scale: i32, + output_scale: impl Into>, + src_transform: Transform, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + alpha: f32, + ) -> Result<(), Self::Error> { + self.frame.as_mut().unwrap().render_texture_at( + texture, + pos, + texture_scale, + output_scale, + src_transform, + damage, + opaque_regions, + alpha, + ) + } + + #[profiling::function] + fn wait(&mut self, sync: &sync::SyncPoint) -> Result<(), Self::Error> { + self.frame.as_mut().unwrap().wait(sync) + } + + #[profiling::function] + fn finish(mut self) -> Result { + self.finish_internal() + } +} + +impl GlowFrame<'_, '_> { + #[profiling::function] + fn finish_internal(&mut self) -> Result { + if let Some(frame) = self.frame.take() { + frame.finish() + } else { + Ok(sync::SyncPoint::default()) + } + } +} + +impl Drop for GlowFrame<'_, '_> { + fn drop(&mut self) { + if let Err(err) = self.finish_internal() { + warn!("Ignored error finishing GlowFrame on drop: {}", err); + } + } +} + +/// Guard type wrapping the underlying [`GlowRenderer`] of a [`GlowFrame`]. +#[derive(Debug)] +pub struct GlowFrameGuard<'a, 'frame, 'buffer> { + renderer: GlowRenderer, + _phantom: std::marker::PhantomData<(&'a (), &'frame (), &'buffer ())>, +} + +impl AsRef for GlowFrameGuard<'_, '_, '_> { + fn as_ref(&self) -> &GlowRenderer { + &self.renderer + } +} + +impl AsMut for GlowFrameGuard<'_, '_, '_> { + fn as_mut(&mut self) -> &mut GlowRenderer { + &mut self.renderer + } +} + +impl<'a, 'frame, 'buffer> FrameContext<'a, 'frame, 'buffer, GlowRenderer> for GlowFrame<'frame, 'buffer> +where + 'frame: 'a, +{ + type Guard = GlowFrameGuard<'a, 'frame, 'buffer>; + + fn renderer(&'a mut self) -> Self::Guard { + let guard = self.frame.as_mut().unwrap().renderer(); + + GlowFrameGuard { + renderer: GlowRenderer { + gl: GlesInternal::Frame(unsafe { + std::mem::transmute::< + GlesFrameGuard<'a, 'frame, 'buffer>, + GlesFrameGuard<'static, 'static, 'static>, + >(guard) + }), + glow: self.glow.clone(), + }, + _phantom: std::marker::PhantomData, + } + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportMemWl for GlowRenderer { + #[profiling::function] + fn import_shm_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result { + self.gl.as_mut().import_shm_buffer(buffer, surface, damage) + } + + fn shm_formats(&self) -> Box> { + self.gl.as_ref().shm_formats() + } +} + +impl ImportMem for GlowRenderer { + #[profiling::function] + fn import_memory( + &mut self, + data: &[u8], + format: Fourcc, + size: Size, + flipped: bool, + ) -> Result { + self.gl.as_mut().import_memory(data, format, size, flipped) + } + + #[profiling::function] + fn update_memory( + &mut self, + texture: &Self::TextureId, + data: &[u8], + region: Rectangle, + ) -> Result<(), Self::Error> { + self.gl.as_mut().update_memory(texture, data, region) + } + + fn mem_formats(&self) -> Box> { + self.gl.as_ref().mem_formats() + } +} + +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +impl ImportEgl for GlowRenderer { + fn bind_wl_display( + &mut self, + display: &wayland_server::DisplayHandle, + ) -> Result<(), crate::backend::egl::Error> { + self.gl.as_mut().bind_wl_display(display) + } + + fn unbind_wl_display(&mut self) { + self.gl.as_mut().unbind_wl_display() + } + + fn egl_reader(&self) -> Option<&EGLBufferReader> { + self.gl.as_ref().egl_reader() + } + + #[profiling::function] + fn import_egl_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result { + self.gl.as_mut().import_egl_buffer(buffer, surface, damage) + } +} + +impl ImportDma for GlowRenderer { + #[profiling::function] + fn import_dmabuf( + &mut self, + buffer: &Dmabuf, + damage: Option<&[Rectangle]>, + ) -> Result { + self.gl.as_mut().import_dmabuf(buffer, damage) + } + fn dmabuf_formats(&self) -> FormatSet { + self.gl.as_ref().dmabuf_formats() + } + fn has_dmabuf_format(&self, format: Format) -> bool { + self.gl.as_ref().has_dmabuf_format(format) + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportDmaWl for GlowRenderer {} + +impl ExportMem for GlowRenderer { + type TextureMapping = GlesMapping; + + #[profiling::function] + fn copy_framebuffer( + &mut self, + from: &GlesTarget<'_>, + region: Rectangle, + format: Fourcc, + ) -> Result { + self.gl.as_mut().copy_framebuffer(from, region, format) + } + + #[profiling::function] + fn copy_texture( + &mut self, + texture: &Self::TextureId, + region: Rectangle, + format: Fourcc, + ) -> Result { + self.gl.as_mut().copy_texture(texture, region, format) + } + + fn can_read_texture(&mut self, texture: &Self::TextureId) -> Result { + self.gl.as_mut().can_read_texture(texture) + } + + #[profiling::function] + fn map_texture<'a>( + &mut self, + texture_mapping: &'a Self::TextureMapping, + ) -> Result<&'a [u8], Self::Error> { + self.gl.as_mut().map_texture(texture_mapping) + } +} + +impl Bind for GlowRenderer +where + GlesRenderer: Bind, +{ + #[profiling::function] + fn bind<'a>(&mut self, target: &'a mut T) -> Result, GlesError> { + self.gl.as_mut().bind(target) + } + fn supported_formats(&self) -> Option { + self.gl.as_ref().supported_formats() + } +} + +impl Offscreen for GlowRenderer +where + GlesRenderer: Offscreen, +{ + #[profiling::function] + fn create_buffer(&mut self, format: Fourcc, size: Size) -> Result { + self.gl.as_mut().create_buffer(format, size) + } +} + +impl<'buffer> BlitFrame> for GlowFrame<'_, 'buffer> { + fn blit_to( + &mut self, + to: &mut GlesTarget<'buffer>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + self.frame.as_mut().unwrap().blit_to(to, src, dst, filter) + } + + fn blit_from( + &mut self, + from: &GlesTarget<'buffer>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + self.frame.as_mut().unwrap().blit_from(from, src, dst, filter) + } +} + +impl Blit for GlowRenderer { + #[profiling::function] + fn blit( + &mut self, + from: &GlesTarget<'_>, + to: &mut GlesTarget<'_>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + self.gl.as_mut().blit(from, to, src, dst, filter) + } +} + +impl RenderElement for PixelShaderElement { + #[profiling::function] + fn draw( + &self, + frame: &mut GlowFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), GlesError> { + RenderElement::::draw(self, frame.borrow_mut(), src, dst, damage, opaque_regions, cache) + } + + fn underlying_storage(&self, renderer: &mut GlowRenderer) -> Option> { + RenderElement::::underlying_storage(self, renderer.borrow_mut()) + } +} + +impl RenderElement for TextureShaderElement { + #[profiling::function] + fn draw( + &self, + frame: &mut GlowFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), GlesError> { + RenderElement::::draw(self, frame.borrow_mut(), src, dst, damage, opaque_regions, cache) + } + + fn underlying_storage(&self, renderer: &mut GlowRenderer) -> Option> { + RenderElement::::underlying_storage(self, renderer.borrow_mut()) + } +} diff --git a/vendor/smithay/src/backend/renderer/mod.rs b/vendor/smithay/src/backend/renderer/mod.rs new file mode 100644 index 0000000..2efa08c --- /dev/null +++ b/vendor/smithay/src/backend/renderer/mod.rs @@ -0,0 +1,1043 @@ +//! Rendering functionality and abstractions +//! +//! Collection of common traits and implementations +//! to facilitate (possible hardware-accelerated) rendering. +//! +//! Supported rendering apis: +//! +//! - Raw OpenGL ES 2 + +use crate::utils::{Buffer as BufferCoord, Physical, Point, Rectangle, Scale, Size, Transform, ids::id_gen}; +use glam::Affine2; +use std::{ + any::TypeId, + cmp::Ordering, + error::Error, + fmt, + hash::{Hash, Hasher}, + marker::PhantomData, + sync::Arc, +}; + +#[cfg(feature = "wayland_frontend")] +use crate::wayland::{compositor::SurfaceData, shm::fourcc_to_shm_format}; +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::{wl_buffer, wl_shm}; + +#[cfg(feature = "renderer_gl")] +pub mod gles; + +#[cfg(feature = "renderer_glow")] +pub mod glow; + +#[cfg(feature = "renderer_pixman")] +pub mod pixman; + +mod color; +pub use color::Color32F; + +use crate::backend::allocator::{Format, Fourcc, dmabuf::Dmabuf}; +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +use crate::backend::egl::{ + Error as EglError, + display::{BUFFER_READER, EGLBufferReader}, +}; + +use super::allocator::format::FormatSet; + +#[cfg(feature = "renderer_multi")] +pub mod multigpu; + +pub mod utils; + +pub mod element; + +pub mod damage; + +pub mod sync; +use sync::SyncPoint; + +// Note: This doesn't fully work yet due to . +// Use `--features renderer_test` when running doc tests manually. +#[cfg(any(feature = "renderer_test", test, doctest))] +pub mod test; + +/// Identifies a renderer context for a specific texture type. +/// +/// Renderers with the same `ContextId` are assumed to be texture-compatible, +/// meaning textures created by one can be imported into another. +pub struct ContextId(Arc, PhantomData T>); + +/// A type-erased [`ContextId`] without the `Texture` generic. +/// +/// This allows representing and comparing renderer contexts across different texture types. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ErasedContextId(Arc, TypeId); + +impl ContextId { + /// Allocates a new `ContextId`. + pub fn new() -> Self { + ContextId(Arc::new(InnerContextId::new()), PhantomData) + } + + /// Maps this `ContextId` to one with another `Texture` type. + /// + /// This is typically used by wrapper or derivative renderers that define a new `TextureId` + /// while reusing the underlying context for rendering. + pub fn map(self) -> ContextId { + ContextId(self.0, PhantomData) + } + + /// Returns an [`ErasedContextId`] representing this context without the texture type. + /// + /// This is useful when storing or comparing contexts across different texture types. + pub fn erased(&self) -> ErasedContextId + where + T: 'static, + { + ErasedContextId(self.0.clone(), TypeId::of::()) + } +} + +impl Default for ContextId { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for ContextId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("ContextId") + .field(&self.0) + .field(&format_args!("_")) + .finish() + } +} + +impl Clone for ContextId { + fn clone(&self) -> Self { + ContextId(self.0.clone(), PhantomData) + } +} + +impl PartialEq for ContextId { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for ContextId {} + +impl PartialOrd for ContextId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ContextId { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } +} + +impl Hash for ContextId { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +id_gen!(context_id); + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct InnerContextId(usize); + +impl InnerContextId { + fn new() -> Self { + Self(context_id::next()) + } +} + +impl Drop for InnerContextId { + fn drop(&mut self) { + context_id::remove(self.0); + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +/// Texture filtering methods +pub enum TextureFilter { + /// Returns the value of the texture element that is nearest (in Manhattan distance) to the center of the pixel being textured. + Linear, + /// Returns the weighted average of the four texture elements that are closest to the center of the pixel being textured. + Nearest, +} + +impl Transform { + /// A projection matrix to apply this transformation + #[inline] + pub fn matrix(&self) -> Affine2 { + match self { + Transform::Normal => Affine2::IDENTITY, + Transform::_90 => Affine2::from_cols_array(&[0.0, -1.0, 1.0, 0.0, 0.0, 0.0]), + Transform::_180 => Affine2::from_cols_array(&[-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]), + Transform::_270 => Affine2::from_cols_array(&[0.0, 1.0, -1.0, 0.0, 0.0, 0.0]), + Transform::Flipped => Affine2::from_cols_array(&[-1.0, 0.0, 0.0, 1.0, 0.0, 0.0]), + Transform::Flipped90 => Affine2::from_cols_array(&[0.0, 1.0, 1.0, 0.0, 0.0, 0.0]), + Transform::Flipped180 => Affine2::from_cols_array(&[1.0, 0.0, 0.0, -1.0, 0.0, 0.0]), + Transform::Flipped270 => Affine2::from_cols_array(&[0.0, -1.0, -1.0, 0.0, 0.0, 0.0]), + } + } +} + +#[cfg(feature = "wayland_frontend")] +impl From for Transform { + #[inline] + fn from(transform: wayland_server::protocol::wl_output::Transform) -> Transform { + use wayland_server::protocol::wl_output::Transform as WlTransform; + match transform { + WlTransform::Normal => Transform::Normal, + WlTransform::_90 => Transform::_90, + WlTransform::_180 => Transform::_180, + WlTransform::_270 => Transform::_270, + WlTransform::Flipped => Transform::Flipped, + WlTransform::Flipped90 => Transform::Flipped90, + WlTransform::Flipped180 => Transform::Flipped180, + WlTransform::Flipped270 => Transform::Flipped270, + _ => Transform::Normal, + } + } +} + +/// Abstraction for Renderers, that can render into different targets +pub trait Bind: Renderer { + /// Initialize a framebuffer with a given rendering target. + /// + /// This function *may* error, if: + /// - The specific given target handle is incompatible with the underlying rendering api + /// + /// **Note**: Some renderers might only be able to determine if a handle is compatible + /// during a `Renderer::render` call with the resulting `Framebuffer`. + fn bind<'a>(&mut self, target: &'a mut Target) -> Result, Self::Error>; + + /// Supported pixel formats for given targets, if applicable. + fn supported_formats(&self) -> Option { + None + } +} + +/// A two dimensional texture +pub trait Texture: fmt::Debug { + /// Size of the texture plane + fn size(&self) -> Size { + Size::from((self.width() as i32, self.height() as i32)) + } + + /// Width of the texture plane + fn width(&self) -> u32; + /// Height of the texture plane + fn height(&self) -> u32; + + /// Format of the texture, if available. + /// + /// In case the format is hidden by the implementation, + /// it should be assumed, that the pixel representation cannot be read. + /// + /// Thus [`ExportMem::copy_texture`], if implemented, will not succeed for this texture. + /// Note that this does **not** mean every texture with a format is guaranteed to be copyable. + fn format(&self) -> Option; +} + +/// A downloaded texture buffer +pub trait TextureMapping: Texture { + /// Returns if the mapped buffer is flipped on the y-axis + /// (compared to the lower left being (0, 0)) + fn flipped(&self) -> bool; + + /// Format of the texture + fn format(&self) -> Fourcc { + Texture::format(self).expect("Texture Mappings need to have a format") + } +} + +/// Helper trait for [`Renderer`], which defines a rendering api for a currently in-progress frame during [`Renderer::render`]. +/// +/// Dropping the [`Frame`] or explicitly calling [`Frame::finish`] will free any unused resources. If you need explicit control +/// over resource clean-up take a look at [`Renderer::cleanup_texture_cache`]. +pub trait Frame { + /// Error type returned by the rendering operations of this renderer. + type Error: Error; + /// Texture Handle type used by this renderer. + type TextureId: Texture; + + /// Returns the [`ContextId`] of the associated renderer. + fn context_id(&self) -> ContextId; + + /// Clear the complete current target with a single given color. + /// + /// The `at` parameter specifies a set of rectangles to clear in the current target. This allows partially + /// clearing the target which may be useful for damaged rendering. + /// + /// This operation is only valid in between a `begin` and `finish`-call. + /// If called outside this operation may error-out, do nothing or modify future rendering results in any way. + fn clear(&mut self, color: Color32F, at: &[Rectangle]) -> Result<(), Self::Error>; + + /// Draw a solid color to the current target at the specified destination with the specified color. + fn draw_solid( + &mut self, + dst: Rectangle, + damage: &[Rectangle], + color: Color32F, + ) -> Result<(), Self::Error>; + + /// Render a texture to the current target as a flat 2d-plane at a given + /// position and applying the given transformation with the given alpha value. + /// (Meaning `src_transform` should match the orientation of surface being rendered). + #[allow(clippy::too_many_arguments)] + fn render_texture_at( + &mut self, + texture: &Self::TextureId, + pos: Point, + texture_scale: i32, + output_scale: impl Into>, + src_transform: Transform, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + alpha: f32, + ) -> Result<(), Self::Error> { + self.render_texture_from_to( + texture, + Rectangle::from_size(texture.size()).to_f64(), + Rectangle::new( + pos, + texture + .size() + .to_logical(texture_scale, src_transform) + .to_physical_precise_round(output_scale), + ), + damage, + opaque_regions, + src_transform, + alpha, + ) + } + + /// Render part of a texture as given by src to the current target into the rectangle described by dst + /// as a flat 2d-plane after applying the inverse of the given transformation. + /// (Meaning `src_transform` should match the orientation of surface being rendered). + #[allow(clippy::too_many_arguments)] + fn render_texture_from_to( + &mut self, + texture: &Self::TextureId, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + src_transform: Transform, + alpha: f32, + ) -> Result<(), Self::Error>; + + /// Output transformation that is applied to this frame + fn transformation(&self) -> Transform; + + /// Output size this frame was initialized for. + fn output_size(&self) -> Size; + + /// Wait for a [`SyncPoint`] to be signaled + fn wait(&mut self, sync: &sync::SyncPoint) -> Result<(), Self::Error>; + + /// Finish this [`Frame`] returning any error that may happen during any cleanup. + /// + /// Dropping the frame instead may result in any of the following and is implementation dependent: + /// - All actions done to the frame vanish and are never executed + /// - A partial renderer with undefined framebuffer contents occurs + /// - All actions are performed as normal without errors being returned. + /// + /// Leaking the frame instead will leak resources and can cause any of the previous effects. + /// Leaking might make the renderer return Errors and force it's recreation. + /// Leaking may not cause otherwise undefined behavior and program execution will always continue normally. + fn finish(self) -> Result; +} + +/// Helper trait for [`Frame`]s, that allow referencing the underlying renderer +/// to create additional frames for offscreen targets. +pub trait FrameContext<'a, 'frame, 'buffer, R: Renderer>: Frame { + /// Type returned by [`FrameContext::renderer`] which derefs into the underlying [`Renderer`]. + type Guard: fmt::Debug + AsRef + AsMut + 'a; + + /// Receive the underlying [`Renderer`] + fn renderer(&'a mut self) -> Self::Guard; +} + +bitflags::bitflags! { + /// Debug flags that can be enabled at runtime + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct DebugFlags: u32 { + /// Tint all rendered textures + const TINT = 0b00000001; + } +} + +/// Workaround for , please look at [`Renderer`] instead. +pub trait RendererSuper: fmt::Debug { + /// Error type returned by the rendering operations of this renderer. + type Error: Error; + /// Texture Handle type used by this renderer. + type TextureId: Texture; + /// Framebuffer to draw onto + type Framebuffer<'buffer>: Texture; + /// Type representing a currently in-progress frame during the [`Renderer::render`]-call + type Frame<'frame, 'buffer>: Frame + where + 'buffer: 'frame, + Self: 'frame; +} + +/// Abstraction of commonly used rendering operations for compositors. +/// +/// *Note*: Associated types are defined in [`RendererSuper`]. +pub trait Renderer: RendererSuper { + /// Returns the [`ContextId`] of this renderer + /// + /// See [`ContextId`] for more details. + fn context_id(&self) -> ContextId; + + /// Set the filter method to be used when rendering a texture into a smaller area than its size + fn downscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error>; + /// Set the filter method to be used when rendering a texture into a larger area than its size + fn upscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error>; + + /// Set the enabled [`DebugFlags`] + fn set_debug_flags(&mut self, flags: DebugFlags); + /// Returns the current enabled [`DebugFlags`] + fn debug_flags(&self) -> DebugFlags; + + /// Initialize a rendering context on the provided framebuffer with given dimensions and transformation. + /// + /// The `output_size` specifies the dimensions of the display **before** the `dst_transform` is + /// applied. + /// + /// This function *may* error, if: + /// - The given dimensions are unsupported (too large) for this renderer + /// - The given Transformation is not supported by the renderer (`Transform::Normal` is always supported). + /// - The underlying object of the given framebuffer is incompatible with this particular render instance. + fn render<'frame, 'buffer>( + &'frame mut self, + framebuffer: &'frame mut Self::Framebuffer<'buffer>, + output_size: Size, + dst_transform: Transform, + ) -> Result, Self::Error> + where + 'buffer: 'frame; + + /// Wait for a [`SyncPoint`] to be signaled + fn wait(&mut self, sync: &sync::SyncPoint) -> Result<(), Self::Error>; + + /// Forcibly clean up the renderer internal texture cache + /// + /// Note: Resources used by the renderer will be implicitly cleaned-up after finishing + /// a [`Frame`] by either dropping the [`Frame`] or explicitly calling [`Frame::finish`]. + /// This call can be used to clean-up resources in cases where either no [`Frame`] is used + /// at all to prevent resource pile-up or in case of only infrequent access to lower + /// system resource usage. + fn cleanup_texture_cache(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + /// Drop all internal caches, freeing their resources unconditionally + /// + /// Unlike [`Renderer::cleanup_texture_cache`], this also discards cached imports and bind + /// targets whose source buffers are still alive. + /// + /// Note: Caches held outside the renderer, like per-surface textures in surface user-data, + /// are unaffected. + /// + /// The default implementation falls back to [`Renderer::cleanup_texture_cache`]. + fn invalidate_caches(&mut self) -> Result<(), Self::Error> { + self.cleanup_texture_cache() + } +} + +/// Trait for renderers that support creating offscreen framebuffers to render into. +/// +/// Usually also implement [`ExportMem`] to receive the framebuffers contents. +pub trait Offscreen: Renderer + Bind { + /// Create a new instance of a framebuffer. + /// + /// This call *may* fail, if (but not limited to): + /// - The maximum amount of framebuffers for this renderer would be exceeded + /// - The format is not supported to be rendered into + /// - The size is too large for a framebuffer + fn create_buffer(&mut self, format: Fourcc, size: Size) -> Result; +} + +/// Trait for Renderers supporting importing wl_buffers using shared memory. +#[cfg(feature = "wayland_frontend")] +pub trait ImportMemWl: ImportMem { + /// Import a given shm-based buffer into the renderer (see [`buffer_type`]). + /// + /// Returns a texture_id, which can be used with [`Frame::render_texture_from_to`] (or [`Frame::render_texture_at`]) + /// or implementation-specific functions. + /// + /// If not otherwise defined by the implementation, this texture id is only valid for the renderer, that created it. + /// This operation needs no bound or default rendering target. + /// + /// The implementation defines, if the id keeps being valid, if the buffer is released, + /// to avoid relying on implementation details, keep the buffer alive, until you destroyed this texture again. + /// + /// If provided the `SurfaceAttributes` can be used to do caching of rendering resources and is generally recommended. + /// + /// The `damage` argument provides a list of rectangle locating parts of the buffer that need to be updated. When provided + /// with an empty list `&[]`, the renderer is allowed to not update the texture at all. + fn import_shm_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result; + + /// Returns supported formats for shared memory buffers. + /// + /// Will always contain At least `Argb8888` and `Xrgb8888`. + fn shm_formats(&self) -> Box> { + Box::new(self.mem_formats().flat_map(fourcc_to_shm_format)) + } +} + +/// Trait for Renderers supporting importing bitmaps from memory. +pub trait ImportMem: Renderer { + /// Import a given chunk of memory into the renderer. + /// + /// Returns a texture_id, which can be used with [`Frame::render_texture_from_to`] (or [`Frame::render_texture_at`]) + /// or implementation-specific functions. + /// + /// If not otherwise defined by the implementation, this texture id is only valid for the renderer, that created it. + /// This operation needs no bound or default rendering target. + /// + /// Settings flipped to true will cause the buffer to be interpreted like the y-axis is flipped + /// (opposed to the lower left begin (0, 0)). + /// This is a texture specific property, so future uploads to the same texture via [`ImportMem::update_memory`] + /// will also be interpreted as flipped. + /// + /// The provided data slice needs to be in a format supported as indicated by [`ImportMem::mem_formats`]. + /// Its length should thus be `size.w * size.h * bits_per_pixel`. + /// Anything beyond will be truncated, if the buffer is too small an error will be returned. + fn import_memory( + &mut self, + data: &[u8], + format: Fourcc, + size: Size, + flipped: bool, + ) -> Result; + + /// Update a portion of a given chunk of memory into an existing texture. + /// + /// This operation needs no bound or default rendering target. + /// + /// The provided data slice needs to be in the same format used to create the texture and the same size of the texture. + /// Its length should this be `texture.size().w * texture.size().h * bits_per_pixel`. + /// Anything beyond will be ignored, if the buffer is too small an error will be returned. + /// + /// This function *may* error, if (but not limited to): + /// - The texture was not created using either [`ImportMemWl::import_shm_buffer`] or [`ImportMem::import_memory`]. + /// External textures imported by other means (e.g. via ImportDma) may not be writable. This property is defined + /// by the implementation. + /// - The region is out of bounds of the initial size the texture was created with. Implementations are not required + /// to support resizing the original texture. + fn update_memory( + &mut self, + texture: &Self::TextureId, + data: &[u8], + region: Rectangle, + ) -> Result<(), Self::Error>; + + /// Returns supported formats for memory imports. + fn mem_formats(&self) -> Box>; +} + +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +/// Trait for Renderers supporting importing wl_drm-based buffers. +pub trait ImportEgl: Renderer { + /// Binds the underlying EGL display to the given Wayland display. + /// + /// This will allow clients to utilize EGL to create hardware-accelerated + /// surfaces. This renderer will thus be able to handle wl_drm-based buffers. + /// + /// ## Errors + /// + /// This might return [`EglExtensionNotSupported`](super::egl::Error::EglExtensionNotSupported) + /// if binding is not supported by the EGL implementation. + /// + /// This might return [`OtherEGLDisplayAlreadyBound`](super::egl::Error::OtherEGLDisplayAlreadyBound) + /// if called for the same [`Display`](wayland_server::Display) multiple times, as only one egl + /// display may be bound at any given time. + fn bind_wl_display(&mut self, display: &wayland_server::DisplayHandle) -> Result<(), EglError>; + + /// Unbinds a previously bound egl display, if existing. + /// + /// *Note*: As a result any previously created egl-based WlBuffers will not be readable anymore. + /// Your compositor will have to deal with existing buffers of *unknown* type. + fn unbind_wl_display(&mut self); + + /// Returns the underlying [`EGLBufferReader`]. + /// + /// The primary use for this is calling [`buffer_dimensions`] or [`buffer_type`]. + /// + /// Returns `None` if no [`Display`](wayland_server::Display) was previously bound to the underlying + /// [`EGLDisplay`](super::egl::EGLDisplay) (see [`ImportEgl::bind_wl_display`]). + fn egl_reader(&self) -> Option<&EGLBufferReader>; + + /// Import a given wl_drm-based buffer into the renderer (see [`buffer_type`]). + /// + /// Returns a texture_id, which can be used with [`Frame::render_texture_from_to`] (or [`Frame::render_texture_at`]) + /// or implementation-specific functions. + /// + /// If not otherwise defined by the implementation, this texture id is only valid for the renderer, that created it. + /// + /// This operation needs no bound or default rendering target. + /// + /// The implementation defines, if the id keeps being valid, if the buffer is released, + /// to avoid relying on implementation details, keep the buffer alive, until you destroyed this texture again. + fn import_egl_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result; +} + +#[cfg(feature = "wayland_frontend")] +/// Trait for Renderers supporting importing dmabuf-based wl_buffers +pub trait ImportDmaWl: ImportDma { + /// Import a given dmabuf-based buffer into the renderer (see [`buffer_type`]). + /// + /// Returns a texture_id, which can be used with [`Frame::render_texture_from_to`] (or [`Frame::render_texture_at`]) + /// or implementation-specific functions. + /// + /// If not otherwise defined by the implementation, this texture id is only valid for the renderer, that created it. + /// + /// This operation needs no bound or default rendering target. + /// + /// The implementation defines, if the id keeps being valid, if the buffer is released, + /// to avoid relying on implementation details, keep the buffer alive, until you destroyed this texture again. + fn import_dma_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + _surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result { + let dmabuf = crate::wayland::dmabuf::get_dmabuf(buffer) + .expect("import_dma_buffer without checking buffer type?"); + self.import_dmabuf(dmabuf, Some(damage)) + } +} + +/// Trait for Renderers supporting importing dmabufs. +pub trait ImportDma: Renderer { + /// Returns supported formats for dmabufs. + fn dmabuf_formats(&self) -> FormatSet { + FormatSet::default() + } + + /// Test if a specific dmabuf [`Format`] is supported + fn has_dmabuf_format(&self, format: Format) -> bool { + self.dmabuf_formats().contains(&format) + } + + /// Import a given raw dmabuf into the renderer. + /// + /// Returns a texture_id, which can be used with [`Frame::render_texture_from_to`] (or [`Frame::render_texture_at`]) + /// or implementation-specific functions. + /// + /// If not otherwise defined by the implementation, this texture id is only valid for the renderer, that created it. + /// + /// This operation needs no bound or default rendering target. + /// + /// The implementation defines, if the id keeps being valid, if the buffer is released, + /// to avoid relying on implementation details, keep the buffer alive, until you destroyed this texture again. + fn import_dmabuf( + &mut self, + dmabuf: &Dmabuf, + damage: Option<&[Rectangle]>, + ) -> Result; +} + +// TODO: Replace this with a trait_alias, once that is stabilized. +// pub type ImportAll = Renderer + ImportShm + ImportEgl; + +/// Common trait for renderers of any wayland buffer type +#[cfg(feature = "wayland_frontend")] +pub trait ImportAll: Renderer { + /// Import a given buffer into the renderer. + /// + /// Returns a texture_id, which can be used with [`Frame::render_texture_from_to`] (or [`Frame::render_texture_at`]) + /// or implementation-specific functions. + /// + /// If not otherwise defined by the implementation, this texture id is only valid for the renderer, that created it. + /// + /// This operation needs no bound or default rendering target. + /// + /// The implementation defines, if the id keeps being valid, if the buffer is released, + /// to avoid relying on implementation details, keep the buffer alive, until you destroyed this texture again. + /// + /// If provided the `SurfaceAttributes` can be used to do caching of rendering resources and is generally recommended. + /// + /// The `damage` argument provides a list of rectangle locating parts of the buffer that need to be updated. When provided + /// with an empty list `&[]`, the renderer is allowed to not update the texture at all. + /// + /// Returns `None`, if the buffer type cannot be determined or does not correspond to a texture (e.g.: single pixel buffer). + fn import_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Option>; +} + +// TODO: Do this with specialization, when possible and do default implementations +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +impl ImportAll for R { + #[profiling::function] + fn import_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&SurfaceData>, + damage: &[Rectangle], + ) -> Option> { + match buffer_type(buffer) { + Some(BufferType::Shm) => Some(self.import_shm_buffer(buffer, surface, damage)), + Some(BufferType::Egl) => Some(self.import_egl_buffer(buffer, surface, damage)), + Some(BufferType::Dma) => Some(self.import_dma_buffer(buffer, surface, damage)), + _ => None, + } + } +} + +#[cfg(all( + feature = "wayland_frontend", + not(all(feature = "backend_egl", feature = "use_system_lib")) +))] +impl ImportAll for R { + fn import_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&SurfaceData>, + damage: &[Rectangle], + ) -> Option> { + match buffer_type(buffer) { + Some(BufferType::Shm) => Some(self.import_shm_buffer(buffer, surface, damage)), + Some(BufferType::Dma) => Some(self.import_dma_buffer(buffer, surface, damage)), + _ => None, + } + } +} + +/// Trait for renderers supporting exporting contents of framebuffers or textures into memory. +pub trait ExportMem: Renderer { + /// Texture type representing a downloaded pixel buffer. + type TextureMapping: TextureMapping; + + /// Copies the contents of the provided target. + /// + /// This operation is not destructive, the contents of the framebuffer keep being valid. + /// + /// This function *may* fail, if (but not limited to): + /// - The framebuffer is not readable + /// - The region is out of bounds of the framebuffer + /// - There is not enough space to create the mapping + /// - It is not possible to convert the framebuffer into the provided format. + fn copy_framebuffer( + &mut self, + target: &Self::Framebuffer<'_>, + region: Rectangle, + format: Fourcc, + ) -> Result; + + /// Copies the contents of the passed texture. + /// *Note*: This function may change or invalidate the current bind. + /// + /// Renderers are not required to support any format other than what was returned by `Texture::format`. + /// This operation is not destructive, the contents of the texture keep being valid. + /// + /// This function *may* fail, if: + /// - There is not enough space to create the mapping + /// - The texture does no allow copying for implementation-specific reasons + /// - It is not possible to convert the texture into the provided format. + fn copy_texture( + &mut self, + texture: &Self::TextureId, + region: Rectangle, + format: Fourcc, + ) -> Result; + + /// Returns whether the renderer should be able to read-back from the given texture. + /// + /// No actual copying shall be performed by this function nor is a format specified, + /// so it is still legal for [`ExportMem::copy_texture`] to return an error, if this + /// method returns `true`. + /// + /// This function *may* fail, if: + /// - A readability test did successfully complete (not that it returned `unreadable`!) + /// - Any of the state of the renderer is irrevesibly changed + fn can_read_texture(&mut self, texture: &Self::TextureId) -> Result; + + /// Returns a read-only pointer to a previously created texture mapping. + /// + /// The format of the returned slice is given by [`Texture::format`] of the texture mapping. + /// + /// This function *may* fail, if (but not limited to): + /// - There is not enough space in memory + fn map_texture<'a>(&mut self, texture_mapping: &'a Self::TextureMapping) + -> Result<&'a [u8], Self::Error>; +} + +/// Trait for renderers supporting blitting contents from one framebuffer to another. +// We would like to require the following. But we can't because of . +// for<'frame, 'buffer> Self::Frame<'frame, 'buffer>: BlitFrame>, +pub trait Blit +where + Self: Renderer, +{ + /// Copies the contents of `src` from one provided target to `dst` in the other provided target, + /// applying `filter` if necessary. + /// + /// This operation is non destructive, the contents of the source framebuffer + /// are kept intact as is any region not in `dst` for the target framebuffer. + /// + /// This function *may* fail, if (but not limited to): + /// - The source framebuffer is not readable + /// - The destination framebuffer is not writable + /// - `src` is out of bounds for the source framebuffer + /// - `dst` is out of bounds for the destination framebuffer + /// - `src` and `dst` sizes are different and interpolation is not supported by this renderer. + /// - source and target framebuffer are the same + fn blit( + &mut self, + from: &Self::Framebuffer<'_>, + to: &mut Self::Framebuffer<'_>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result; +} + +/// Trait for frames supporting blitting contents from/to the current framebuffer to/from another. +pub trait BlitFrame +where + Self: Frame, +{ + /// Copies the contents of the bound framebuffer to `dst` in the provided framebuffer, + /// applying `filter` if necessary. + /// + /// This operation is non destructive, the contents of the current framebuffer + /// are kept intact as is any region not in `dst` for the target framebuffer. + /// + /// This function *may* fail, if (but not limited to): + /// - The bound framebuffer is not readable + /// - The destination framebuffer is not writable + /// - `src` is out of bounds for the bound framebuffer + /// - `dst` is out of bounds for the destination framebuffer + /// - `src` and `dst` sizes are different and interpolation is not supported by this renderer. + /// - bound and target framebuffer are the same + fn blit_to( + &mut self, + to: &mut Framebuffer, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result; + + /// Copies the contents of the provided framebuffer to `dst` in the bound framebuffer, + /// applying `filter` if necessary. + /// + /// This operation is non destructive, the contents of the source framebuffer + /// are kept intact as is any region not in `dst` for the bound framebuffer. + /// + /// This function *may* fail, if (but not limited to): + /// - The source framebuffer is not readable + /// - The bound framebuffer is not writable + /// - `src` is out of bounds for the source framebuffer + /// - `dst` is out of bounds for the bound framebuffer + /// - `src` and `dst` sizes are different and interpolation is not supported by this renderer. + /// - source and bound framebuffer are the same + fn blit_from( + &mut self, + from: &Framebuffer, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result; +} + +#[cfg(feature = "wayland_frontend")] +#[non_exhaustive] +/// Buffer type of a given wl_buffer, if managed by smithay +#[derive(Debug)] +pub enum BufferType { + /// Buffer is managed by the [`crate::wayland::shm`] global + Shm, + #[cfg(all(feature = "backend_egl", feature = "use_system_lib"))] + /// Buffer is managed by a currently initialized [`crate::backend::egl::display::EGLBufferReader`] + Egl, + /// Buffer is managed by the [`crate::wayland::dmabuf`] global + Dma, + /// Buffer represents a singe pixel + SinglePixel, +} + +/// Returns the *type* of a wl_buffer +/// +/// Returns `None` if the type is not known to smithay +/// or otherwise not supported (e.g. not initialized using one of smithays [`crate::wayland`]-handlers). +#[cfg(feature = "wayland_frontend")] +pub fn buffer_type(buffer: &wl_buffer::WlBuffer) -> Option { + use crate::wayland::shm::BufferAccessError; + + if crate::wayland::dmabuf::get_dmabuf(buffer).is_ok() { + return Some(BufferType::Dma); + } + + if !matches!( + crate::wayland::shm::with_buffer_contents(buffer, |_, _, _| ()), + Err(BufferAccessError::NotManaged) + ) { + return Some(BufferType::Shm); + } + + if crate::wayland::single_pixel_buffer::get_single_pixel_buffer(buffer).is_ok() { + return Some(BufferType::SinglePixel); + } + + // Not managed, check if this is an EGLBuffer + #[cfg(all(feature = "backend_egl", feature = "use_system_lib"))] + if BUFFER_READER + .lock() + .unwrap() + .as_ref() + .and_then(|x| x.upgrade()) + .and_then(|x| x.egl_buffer_dimensions(buffer)) + .is_some() + { + return Some(BufferType::Egl); + } + + None +} + +/// Returns if the buffer has an alpha channel +/// +/// Returns `None` if the type is not known to smithay +/// or otherwise not supported (e.g. not initialized using one of smithays [`crate::wayland`]-handlers). +/// +/// Note: This is on a best-effort, but will never return false for a buffer +/// with a format that supports alpha. +#[cfg(feature = "wayland_frontend")] +pub fn buffer_has_alpha(buffer: &wl_buffer::WlBuffer) -> Option { + use super::allocator::format::has_alpha; + use crate::wayland::shm::shm_format_to_fourcc; + + if let Ok(dmabuf) = crate::wayland::dmabuf::get_dmabuf(buffer) { + return Some(crate::backend::allocator::format::has_alpha(dmabuf.0.format)); + } + + if let Ok(has_alpha) = crate::wayland::shm::with_buffer_contents(buffer, |_, _, data| { + shm_format_to_fourcc(data.format).is_some_and(has_alpha) + }) { + return Some(has_alpha); + } + + if let Ok(spb) = crate::wayland::single_pixel_buffer::get_single_pixel_buffer(buffer) { + return Some(spb.has_alpha()); + } + + // Not managed, check if this is an EGLBuffer + #[cfg(all(feature = "backend_egl", feature = "use_system_lib"))] + if let Some(format) = BUFFER_READER + .lock() + .unwrap() + .as_ref() + .and_then(|x| x.upgrade()) + .and_then(|x| x.egl_buffer_contents(buffer).ok()) + .map(|b| b.format) + { + return Some(crate::backend::egl::display::EGLBufferReader::egl_buffer_has_alpha(format)); + } + + None +} + +/// Returns the dimensions of a wl_buffer +/// +/// *Note*: This will only return dimensions for buffer types known to smithay (see [`buffer_type`]) +#[cfg(feature = "wayland_frontend")] +pub fn buffer_dimensions(buffer: &wl_buffer::WlBuffer) -> Option> { + use crate::{ + backend::allocator::Buffer, + wayland::shm::{self, BufferAccessError}, + }; + + if let Ok(buf) = crate::wayland::dmabuf::get_dmabuf(buffer) { + return Some((buf.width() as i32, buf.height() as i32).into()); + } + + if crate::wayland::single_pixel_buffer::get_single_pixel_buffer(buffer).is_ok() { + return Some(Size::from((1, 1))); + } + + match shm::with_buffer_contents(buffer, |_, _, data| (data.width, data.height).into()) { + Ok(data) => Some(data), + + Err(BufferAccessError::NotManaged) => { + // Not managed, check if this is an EGLBuffer + #[cfg(all(feature = "backend_egl", feature = "use_system_lib"))] + if let Some(dim) = BUFFER_READER + .lock() + .unwrap() + .as_ref() + .and_then(|x| x.upgrade()) + .and_then(|x| x.egl_buffer_dimensions(buffer)) + { + return Some(dim); + } + + None + } + + Err(_) => None, + } +} + +/// Returns if the underlying buffer is y-inverted +/// +/// *Note*: This will only return y-inverted for buffer types known to smithay (see [`buffer_type`]) +#[cfg(feature = "wayland_frontend")] +#[profiling::function] +pub fn buffer_y_inverted(buffer: &wl_buffer::WlBuffer) -> Option { + if let Ok(dmabuf) = crate::wayland::dmabuf::get_dmabuf(buffer) { + return Some(dmabuf.y_inverted()); + } + + #[cfg(all(feature = "backend_egl", feature = "use_system_lib"))] + if let Some(Ok(egl_buffer)) = BUFFER_READER + .lock() + .unwrap() + .as_ref() + .and_then(|x| x.upgrade()) + .map(|x| x.egl_buffer_contents(buffer)) + { + return Some(egl_buffer.y_inverted); + } + + None +} diff --git a/vendor/smithay/src/backend/renderer/multigpu/gbm.rs b/vendor/smithay/src/backend/renderer/multigpu/gbm.rs new file mode 100644 index 0000000..2d99897 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/multigpu/gbm.rs @@ -0,0 +1,401 @@ +//! Implementation of the multi-gpu [`GraphicsApi`] using +//! user provided GBM devices and OpenGL ES for rendering. + +use tracing::warn; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use wayland_server::protocol::wl_buffer; + +use crate::backend::{ + SwapBuffersError, + allocator::{ + Allocator, + dmabuf::{AnyError, Dmabuf, DmabufAllocator}, + gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, + }, + drm::{CreateDrmNodeError, DrmNode}, + egl::{EGLContext, EGLDisplay, Error as EGLError, context::ContextPriority}, + renderer::{ + Renderer, RendererSuper, + gles::{GlesError, GlesRenderer}, + multigpu::{ApiDevice, Error as MultiError, GraphicsApi}, + }, +}; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use crate::{ + backend::{ + allocator::Buffer as BufferTrait, + egl::display::EGLBufferReader, + renderer::{ + Bind, ExportMem, ImportDma, ImportEgl, ImportMem, + multigpu::{Error as MultigpuError, MultiRenderer, MultiTexture, import_dmabuf_internal}, + }, + }, + utils::{Buffer as BufferCoords, Rectangle}, +}; +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +use std::borrow::BorrowMut; +use std::{ + borrow::Borrow, + collections::HashMap, + fmt, + os::unix::prelude::AsFd, + sync::atomic::{AtomicBool, Ordering}, +}; + +/// Errors raised by the [`GbmGlesBackend`] +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// EGL api error + #[error(transparent)] + Egl(#[from] EGLError), + /// OpenGL error + #[error(transparent)] + Gl(#[from] GlesError), + /// Error creating a drm node + #[error(transparent)] + DrmNode(#[from] CreateDrmNodeError), +} + +impl From for SwapBuffersError { + #[inline] + fn from(err: Error) -> SwapBuffersError { + match err { + x @ Error::DrmNode(_) | x @ Error::Egl(_) => SwapBuffersError::ContextLost(Box::new(x)), + Error::Gl(x) => x.into(), + } + } +} + +type Factory = Box Result>; + +/// A [`GraphicsApi`] utilizing user-provided GBM Devices and OpenGL ES for rendering. +pub struct GbmGlesBackend { + devices: HashMap)>, + factory: Option, + context_priority: Option, + allocator_flags: GbmBufferFlags, + needs_enumeration: AtomicBool, + _renderer: std::marker::PhantomData, +} + +impl fmt::Debug for GbmGlesBackend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GbmGlesBackend") + .field("devices", &self.devices) + .finish() + } +} + +impl Default for GbmGlesBackend { + #[inline] + fn default() -> Self { + GbmGlesBackend { + devices: HashMap::new(), + factory: None, + context_priority: None, + allocator_flags: GbmBufferFlags::RENDERING, + needs_enumeration: AtomicBool::new(true), + _renderer: std::marker::PhantomData, + } + } +} + +impl GbmGlesBackend { + /// Initialize a new [`GbmGlesBackend`] with a factory for instantiating [`GlesRenderer`]s + pub fn with_factory(factory: F) -> Self + where + F: Fn(&EGLDisplay) -> Result + 'static, + { + Self { + factory: Some(Box::new(factory)), + ..Default::default() + } + } + + /// Initialize a new [`GbmGlesBackend`] with a [`ContextPriority`] for instantiating [`GlesRenderer`]s + /// + /// Note: This is mutually exclusive with [`GbmGlesBackend::with_factory`](GbmGlesBackend::with_factory), + /// but you can create an [`EGLContext`] with a specific [`ContextPriority`] within your factory. + /// See [`EGLContext::new_with_priority`] for more information. + pub fn with_context_priority(priority: ContextPriority) -> Self { + Self { + context_priority: Some(priority), + ..Default::default() + } + } + + /// Sets the default flags to use for allocating buffers via the [`GbmAllocator`] + /// provided by these backends devices. + /// + /// Only affects nodes added via [`add_node`][Self::add_node] *after* calling this method. + pub fn set_allocator_flags(&mut self, flags: GbmBufferFlags) { + self.allocator_flags = flags; + } + + /// Add a new GBM device for a given node to the api + pub fn add_node(&mut self, node: DrmNode, gbm: GbmDevice) -> Result<(), EGLError> { + if self.devices.contains_key(&node) { + return Ok(()); + } + + let allocator = GbmAllocator::new(gbm.clone(), self.allocator_flags); + self.devices + .insert(node, (unsafe { EGLDisplay::new(gbm)? }, allocator)); + self.needs_enumeration.store(true, Ordering::SeqCst); + Ok(()) + } + + /// Remove a given node from the api + pub fn remove_node(&mut self, node: &DrmNode) { + if self.devices.remove(node).is_some() { + self.needs_enumeration.store(true, Ordering::SeqCst); + } + } +} + +impl + Renderer + Borrow, A: AsFd + Clone + 'static> + GraphicsApi for GbmGlesBackend +{ + type Device = GbmGlesDevice; + type Error = Error; + + fn enumerate(&self, list: &mut Vec) -> Result<(), Self::Error> { + self.needs_enumeration.store(false, Ordering::SeqCst); + + // remove old stuff + list.retain(|renderer| { + self.devices + .keys() + .any(|node| renderer.node.dev_id() == node.dev_id()) + }); + + // add new stuff + let new_renderers = self + .devices + .iter() + .filter(|(node, _)| { + !list + .iter() + .any(|renderer| renderer.node.dev_id() == node.dev_id()) + }) + .map(|(node, (display, gbm))| { + let renderer = if let Some(factory) = self.factory.as_ref() { + factory(display)?.into() + } else { + let context = + EGLContext::new_with_priority(display, self.context_priority.unwrap_or_default()) + .map_err(Error::Egl)?; + unsafe { GlesRenderer::new(context).map_err(Error::Gl)? }.into() + }; + + Ok(GbmGlesDevice { + node: *node, + _display: display.clone(), + renderer, + allocator: Box::new(DmabufAllocator(gbm.clone())), + }) + }) + .flat_map(|x: Result, Error>| match x { + Ok(x) => Some(x), + Err(x) => { + warn!("Skipping GbmDevice: {}", x); + None + } + }) + .collect::>>(); + list.extend(new_renderers); + + // but don't replace already initialized renderers + + Ok(()) + } + + fn needs_enumeration(&self) -> bool { + self.needs_enumeration.load(Ordering::Acquire) + } + + fn identifier() -> &'static str { + "gbm_gles" + } +} + +// TODO: Replace with specialization impl in multigpu/mod once possible +impl< + T: GraphicsApi, + R: From + Renderer + Borrow, + A: AsFd + Clone + 'static, +> std::convert::From for MultiError, T> +where + T::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[inline] + fn from(err: GlesError) -> MultiError, T> { + MultiError::Render(err) + } +} + +/// [`ApiDevice`] of the [`GbmGlesBackend`] +pub struct GbmGlesDevice { + node: DrmNode, + renderer: R, + allocator: Box>, + _display: EGLDisplay, +} + +impl fmt::Debug for GbmGlesDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GbmGlesDevice") + .field("node", &self.node) + .field("renderer", &self.renderer) + .finish_non_exhaustive() + } +} + +impl> ApiDevice for GbmGlesDevice { + type Renderer = R; + + fn renderer(&self) -> &Self::Renderer { + &self.renderer + } + fn renderer_mut(&mut self) -> &mut Self::Renderer { + &mut self.renderer + } + fn allocator(&mut self) -> &mut dyn Allocator { + self.allocator.as_mut() + } + fn node(&self) -> &DrmNode { + &self.node + } + fn can_do_cross_device_imports(&self) -> bool { + !self.renderer.borrow().is_software() + } +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +impl ImportEgl for MultiRenderer<'_, '_, GbmGlesBackend, GbmGlesBackend> +where + A: AsFd + Clone + 'static, + R: From + + BorrowMut + + Renderer + + Bind + + ImportDma + + ImportMem + + ImportEgl + + ExportMem + + 'static, + R::TextureId: Clone + Send, +{ + fn bind_wl_display(&mut self, display: &wayland_server::DisplayHandle) -> Result<(), EGLError> { + self.render.renderer_mut().bind_wl_display(display) + } + fn unbind_wl_display(&mut self) { + self.render.renderer_mut().unbind_wl_display() + } + fn egl_reader(&self) -> Option<&EGLBufferReader> { + self.render.renderer().egl_reader() + } + + #[profiling::function] + fn import_egl_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result { + if let Some(dmabuf) = Self::try_import_egl(self.render.renderer_mut(), buffer) + .ok() + .or_else(|| { + self.target + .as_mut() + .and_then(|renderer| Self::try_import_egl(renderer.device.renderer_mut(), buffer).ok()) + }) + .or_else(|| { + self.other_renderers + .iter_mut() + .find_map(|renderer| Self::try_import_egl(renderer.renderer_mut(), buffer).ok()) + }) + { + let texture = MultiTexture::from_surface(surface, dmabuf.size(), dmabuf.format()); + let texture_ref = texture.0.clone(); + let res = import_dmabuf_internal( + self.render, + self.target.as_mut().map(|target| &mut *target.device), + &mut self.other_renderers, + &dmabuf, + texture, + Some(damage), + ); + if res.is_ok() { + if let Some(surface) = surface { + surface.data_map.insert_if_missing_threadsafe(|| texture_ref); + } + } + return res; + } + + Err(MultigpuError::DeviceMissing) + } +} + +#[cfg(all(feature = "wayland_frontend", feature = "use_system_lib"))] +impl MultiRenderer<'_, '_, GbmGlesBackend, GbmGlesBackend> +where + A: AsFd + Clone + 'static, + R: From + + BorrowMut + + Renderer + + ImportDma + + ImportMem + + ImportEgl + + ExportMem + + 'static, +{ + #[profiling::function] + fn try_import_egl( + renderer: &mut R, + buffer: &wl_buffer::WlBuffer, + ) -> Result, GbmGlesBackend>> { + if !renderer + .borrow_mut() + .extensions + .iter() + .any(|ext| ext == "GL_OES_EGL_image") + { + return Err(MultigpuError::Render(GlesError::GLExtensionNotSupported(&[ + "GL_OES_EGL_image", + ]))); + } + + if renderer.egl_reader().is_none() { + return Err(MultigpuError::Render(GlesError::EGLBufferAccessError( + crate::backend::egl::BufferAccessError::NotManaged(crate::backend::egl::EGLError::BadDisplay), + ))); + } + + unsafe { + renderer + .borrow_mut() + .egl_context() + .make_current() + .map_err(GlesError::from) + .map_err(MultigpuError::Render)? + }; + + let egl = renderer + .egl_reader() + .as_ref() + .unwrap() + .egl_buffer_contents(buffer) + .map_err(GlesError::EGLBufferAccessError) + .map_err(MultigpuError::Render)?; + renderer + .borrow_mut() + .egl_context() + .display() + .create_dmabuf_from_image(egl.image(0).unwrap(), egl.size, egl.y_inverted) + .map_err(GlesError::BindBufferEGLError) + .map_err(MultigpuError::Render) + } +} diff --git a/vendor/smithay/src/backend/renderer/multigpu/mod.rs b/vendor/smithay/src/backend/renderer/multigpu/mod.rs new file mode 100644 index 0000000..8395437 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/multigpu/mod.rs @@ -0,0 +1,3928 @@ +//! +//! This module aims to make multi-gpu setups easier to handle for compositors. +//! +//! Its main entry point is the [`GpuManager`]. Initializing this with a +//! [`GraphicsApi`] implementation will allow you to create [`MultiRenderer`]s. +//! +//! smithay provides the following graphics apis: +//! - [`gbm::GbmGlesBackend`] +//! +//! A [`MultiRenderer`] gets created using two [`DrmNode`]s to identify gpus. +//! One gpu will be referred to as the render-gpu, the other as the target-gpu. +//! +//! Note: The render- and target-gpu may be identically to allow the multigpu +//! module to be used on single-gpu systems as well avoiding supporting multiple code-paths. +//! Doing so will not result in worse performance compared to rendering without the multi-gpu module. +//! +//! A [`MultiRenderer`] will support the [`Renderer`]-trait as well +//! as the other corresponding traits of the [`renderer`](crate::backend::renderer)-module, +//! if the [`GraphicsApi`] allows it. +//! +//! Any rendering operations will take place on the render-gpu transparently. +//! Output will be redirected the target gpu, as such any [`Bind`]- and [`Offscreen`]-implementations +//! will be allocated on the target-gpu. +//! +//! The `ImportMem`-implementation will upload the texture on the render-gpu, failing *if* +//! the render-gpu does not support the format as normal. Be careful when mixing gpu's +//! with different memory-formats supported, what formats you announce. +//! +//! In contrast the `ImportDma`-implementations will transparently create copies +//! of client buffers, if necessary, given it is unclear if a dmabuf can be imported on any given gpu. +//! The implementation strives for the best possible performance for a given setup, +//! when choosing a copy-path. +//! +//! Any `ExportMem`-implementations will originate from the render-gpu, which again +//! needs to support the requested format directly. No paths across other gpus are tested. +//! +//! *Note*: This module will not keep you from selecting sub-optimal configurations. +//! Any heuristics for which render-gpu to use for a given set of client buffers +//! and desired target-gpu are up to be implemented by the compositor. The module only +//! reduces the amount of necessary setup operations. +//! +use aliasable::boxed::AliasableBox; +use std::{ + any::Any, + collections::HashMap, + fmt, + sync::{Arc, Mutex}, +}; + +use super::{ + Bind, Blit, BlitFrame, Color32F, ContextId, DebugFlags, ErasedContextId, ExportMem, Frame, ImportDma, + ImportMem, Offscreen, Renderer, RendererSuper, Texture, TextureFilter, TextureMapping, + sync::{self, SyncPoint}, +}; +#[cfg(feature = "wayland_frontend")] +use super::{ImportDmaWl, ImportMemWl}; + +#[cfg(feature = "wayland_frontend")] +use crate::{ + backend::renderer::{BufferType, buffer_type}, + wayland::{compositor::SurfaceData, dmabuf::get_dmabuf, shm}, +}; +use crate::{ + backend::{ + SwapBuffersError, + allocator::{ + Allocator, Buffer as BufferTrait, Format, Fourcc, Modifier, + dmabuf::{AnyError, Dmabuf}, + format::{FormatSet, get_bpp}, + }, + drm::DrmNode, + renderer::FrameContext, + }, + utils::{Buffer as BufferCoords, Physical, Rectangle, Size, Transform}, +}; +use tracing::{debug, info, info_span, instrument, trace, trace_span, warn}; +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::{wl_buffer, wl_shm, wl_surface::WlSurface}; + +#[cfg(all(feature = "backend_gbm", feature = "backend_egl", feature = "renderer_gl"))] +pub mod gbm; + +/// Tracks available gpus from a given [`GraphicsApi`] +#[derive(Debug)] +pub struct GpuManager { + api: A, + devices: Vec, + dmabuf_cache: HashMap<(DrmNode, DrmNode), Option<(bool, Dmabuf)>>, + span: tracing::Span, +} + +/// Errors generated by [`GpuManager`] and [`MultiRenderer`]. +#[derive(thiserror::Error)] +pub enum Error +where + R::Error: 'static, + T::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + /// The graphics api errored on device enumeration + #[error("The render graphics api failed enumerating devices {0:?}")] + RenderApiError(#[source] R::Error), + /// The graphics api errored on device enumeration + #[error("The target graphics api failed enumerating devices {0:?}")] + TargetApiError(#[source] T::Error), + /// The graphics api has found no node matching the drm node + #[error("The graphics api has found no node matching {0:?}")] + NoDevice(DrmNode), + /// The device requested did not match the expected + #[error("The devices requested {0:?} did not match the expected")] + MismatchedDevice(DrmNode), + /// The device has gone missing + #[error("The device has gone missing")] + DeviceMissing, + /// Error on the rendering device + #[error("Error on the rendering device: {0:}")] + Render(#[source] <::Renderer as RendererSuper>::Error), + /// Error on the target device + #[error("Error on the target device: {0:}")] + Target(#[source] <::Renderer as RendererSuper>::Error), + /// Failed to import buffer using the api on any device + #[error("Failed to import buffer")] + ImportFailed, + /// Buffer allocation failed + #[error("Failed to allocate buffer")] + AllocatorError(AnyError), +} + +impl Error { + fn transpose(self) -> Error { + match self { + Error::Render(err) => Error::Target(err), + Error::Target(err) => Error::Render(err), + Error::RenderApiError(err) => Error::TargetApiError(err), + Error::TargetApiError(err) => Error::RenderApiError(err), + Error::NoDevice(d) => Error::NoDevice(d), + Error::MismatchedDevice(d) => Error::MismatchedDevice(d), + Error::DeviceMissing => Error::DeviceMissing, + Error::ImportFailed => Error::ImportFailed, + Error::AllocatorError(a) => Error::AllocatorError(a), + } + } +} + +impl Error { + fn generalize(self) -> Error { + match self { + Error::Render(err) => Error::Render(err), + Error::Target(err) => Error::Render(err), + Error::RenderApiError(err) => Error::RenderApiError(err), + Error::TargetApiError(err) => Error::RenderApiError(err), + Error::NoDevice(d) => Error::NoDevice(d), + Error::MismatchedDevice(d) => Error::MismatchedDevice(d), + Error::DeviceMissing => Error::DeviceMissing, + Error::ImportFailed => Error::ImportFailed, + Error::AllocatorError(a) => Error::AllocatorError(a), + } + } +} + +impl fmt::Debug for Error +where + R::Error: 'static, + T::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::RenderApiError(err) => write!(f, "Error::RenderApiError({err:?})"), + Error::TargetApiError(err) => write!(f, "Error::TargetApiError({err:?})"), + Error::NoDevice(dev) => write!(f, "Error::NoDevice({dev:?})"), + Error::MismatchedDevice(dev) => write!(f, "Error::MismatchedDevice({dev:?})"), + Error::DeviceMissing => write!(f, "Error::DeviceMissing"), + Error::Render(err) => write!(f, "Error::Render({err:?})"), + Error::Target(err) => write!(f, "Error::Target({err:?})"), + Error::ImportFailed => write!(f, "Error::ImportFailed"), + Error::AllocatorError(err) => write!(f, "Error::AllocationError({err})"), + } + } +} + +impl From> for SwapBuffersError +where + R::Error: Into + Send + Sync, + T::Error: Into + Send + Sync, + <::Renderer as RendererSuper>::Error: Into + Send + Sync, + <::Renderer as RendererSuper>::Error: Into + Send + Sync, +{ + #[inline] + fn from(err: Error) -> SwapBuffersError { + match err { + x @ Error::NoDevice(_) | x @ Error::DeviceMissing | x @ Error::AllocatorError(_) => { + SwapBuffersError::ContextLost(Box::new(x)) + } + x @ Error::MismatchedDevice(_) | x @ Error::ImportFailed => { + SwapBuffersError::TemporaryFailure(Box::new(x)) + } + Error::RenderApiError(x) => x.into(), + Error::TargetApiError(x) => x.into(), + Error::Render(x) => x.into(), + Error::Target(x) => x.into(), + } + } +} + +impl AsRef for GpuManager { + fn as_ref(&self) -> &A { + &self.api + } +} + +impl AsMut for GpuManager { + fn as_mut(&mut self) -> &mut A { + &mut self.api + } +} + +impl GpuManager { + /// Create a new [`GpuManager`] for a given [`GraphicsApi`]. + pub fn new(api: A) -> Result, Error> { + let span = info_span!("renderer_multi", backend = A::identifier()); + + let mut devices = Vec::new(); + api.enumerate(&mut devices).map_err(Error::RenderApiError)?; + + Ok(GpuManager { + api, + devices, + dmabuf_cache: HashMap::new(), + span, + }) + } + + /// Get all devices enumerated by the API. + pub fn devices(&mut self) -> Result, A::Error> { + if self.api.needs_enumeration() { + self.api.enumerate(&mut self.devices)?; + } + Ok(self.devices.iter()) + } + + /// Get all devices enumerated by the API. + pub fn devices_mut(&mut self) -> Result, A::Error> { + if self.api.needs_enumeration() { + self.api.enumerate(&mut self.devices)?; + } + Ok(self.devices.iter_mut()) + } + + /// Clean up the texture caches of every device enumerated by the API. + /// + /// See [`Renderer::cleanup_texture_cache`]. Cleanup is best-effort: every device is attempted + /// even if one of them fails, every failure is logged, and the first error is returned. + #[profiling::function] + pub fn cleanup_texture_cache(&mut self) -> Result<(), Error> { + let mut result = Ok(()); + for device in self.devices_mut().map_err(Error::RenderApiError)? { + result = result.and(cleanup_device_texture_cache(device).map_err(Error::Render)); + } + result + } + + /// Drop all caches held by this manager and by every device enumerated by the API. + /// + /// See [`Renderer::invalidate_caches`]. Beyond the per-device caches this also drops the + /// buffers cached for copying between a render- and a target-node. + /// + /// Invalidation is best-effort: every device is attempted even if one of them fails, every + /// failure is logged, and the first error is returned. + #[profiling::function] + pub fn invalidate_caches(&mut self) -> Result<(), Error> { + self.dmabuf_cache.clear(); + + let mut result = Ok(()); + for device in self.devices_mut().map_err(Error::RenderApiError)? { + result = result.and(invalidate_device_caches(device).map_err(Error::Render)); + } + result + } + + /// Create a [`MultiRenderer`] from a single device. + /// + /// This a convenience function to deal with the same types even, if you only need one device. + /// Because no copies are necessary in these cases, all extra arguments can be omitted. + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn single_renderer<'api>( + &'api mut self, + device: &DrmNode, + ) -> Result, Error> { + if !self.devices.iter().any(|dev| dev.node() == device) || self.api.needs_enumeration() { + self.api + .enumerate(&mut self.devices) + .map_err(Error::RenderApiError)?; + } + + if !self.devices.iter().any(|dev| dev.node() == device) { + return Err(Error::NoDevice(*device)); + } + + let (mut render, others) = self + .devices + .iter_mut() + .partition::, _>(|dev| dev.node() == device); + + Ok(MultiRenderer { + render: render.remove(0), + target: None, + other_renderers: others, + span: tracing::Span::current(), + }) + } + + /// Create a [`MultiRenderer`]. + /// + /// - `render_device` should refer to the gpu node rendering operations will take place upon. + /// - `target_device` should refer to the gpu node the composited buffer will end up upon + /// - `allocator` should refer to an `Allocator`, that works guaranteed with the `render_device` + /// to do offscreen composition on. Dma copies will be used, if buffers returned by the allocator + /// also work on the `target_device`. + /// - `copy_format` denotes the format buffers will be allocated in for offscreen rendering. + /// + /// It is valid to pass the same devices for both, but you *should* use [`GpuManager::single_renderer`] in those cases. + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn renderer<'api>( + &'api mut self, + render_device: &DrmNode, + target_device: &DrmNode, + copy_format: Fourcc, + ) -> Result, Error> + where + ::Renderer: Bind, + { + if !self.devices.iter().any(|device| device.node() == render_device) + || !self.devices.iter().any(|device| device.node() == target_device) + || self.api.needs_enumeration() + { + self.api + .enumerate(&mut self.devices) + .map_err(Error::RenderApiError)?; + } + + if !self.devices.iter().any(|device| device.node() == render_device) { + return Err(Error::NoDevice(*render_device)); + } + if !self.devices.iter().any(|device| device.node() == target_device) { + return Err(Error::NoDevice(*target_device)); + } + + let (mut render, others) = self + .devices + .iter_mut() + .partition::, _>(|device| device.node() == render_device); + if target_device != render_device { + let (mut target, others) = others + .into_iter() + .partition::, _>(|device| device.node() == target_device); + + Ok(MultiRenderer { + render: render.remove(0), + target: Some(TargetData { + device: target.remove(0), + cached_buffer: self + .dmabuf_cache + .entry((*render_device, *target_device)) + .or_default(), + format: copy_format, + }), + other_renderers: others, + span: tracing::Span::current(), + }) + } else { + Ok(MultiRenderer { + render: render.remove(0), + target: None, + other_renderers: others, + span: tracing::Span::current(), + }) + } + } + + /// Create a [`MultiRenderer`] from two different [`GraphicsApi`]s. + /// + /// - `render_api` should be the [`GpuManager`] used for the `render_device`. + /// - `target_api` should be the [`GpuManager`] used for the `target_device`. + /// - `render_device` should refer to the gpu node rendering operations will take place upon. + /// - `target_device` should refer to the gpu node the composited buffer will end up upon + /// - `copy_format` denotes the format buffers will be allocated in for offscreen rendering. + #[instrument(level = "trace", skip(render_api, target_api), follows_from = [&render_api.span, &target_api.span])] + #[profiling::function] + pub fn cross_renderer<'render, 'target, B: GraphicsApi>( + render_api: &'render mut Self, + target_api: &'target mut GpuManager, + render_device: &DrmNode, + target_device: &DrmNode, + copy_format: Fourcc, + ) -> Result, Error> + where + ::Renderer: Bind, + ::Renderer: ImportDma, + { + if !render_api + .devices + .iter() + .any(|device| device.node() == render_device) + || render_api.api.needs_enumeration() + { + render_api + .api + .enumerate(&mut render_api.devices) + .map_err(Error::RenderApiError)?; + } + + if !target_api + .devices + .iter() + .any(|device| device.node() == target_device) + || target_api.api.needs_enumeration() + { + target_api + .api + .enumerate(&mut target_api.devices) + .map_err(Error::TargetApiError)?; + } + + if !render_api + .devices + .iter() + .any(|device| device.node() == render_device) + { + return Err(Error::NoDevice(*render_device)); + } + if !target_api + .devices + .iter() + .any(|device| device.node() == target_device) + { + return Err(Error::NoDevice(*target_device)); + } + + let (mut render, others) = render_api + .devices + .iter_mut() + .partition::, _>(|device| device.node() == render_device); + if target_device != render_device { + let target = target_api + .devices + .iter_mut() + .find(|device| device.node() == target_device) + .unwrap(); + + Ok(MultiRenderer { + render: render.remove(0), + target: Some(TargetData { + device: target, + cached_buffer: target_api + .dmabuf_cache + .entry((*render_device, *target_device)) + .or_default(), + format: copy_format, + }), + other_renderers: others, + span: tracing::Span::current(), + }) + } else { + Ok(MultiRenderer { + render: render.remove(0), + target: None, + other_renderers: others, + span: tracing::Span::current(), + }) + } + } + + /// Function for optimizing buffer imports across multiple gpus. + /// + /// If you are using [`MultiRenderer`]s do rendering of your client buffers, + /// you can call `early_import` on commit to start necessary copy processes early. + /// + /// - `target` refers to the gpu node, that the buffer needs to be accessible on later. + /// *Note*: Usually this will be **render**ing gpu of a [`MultiRenderer`] + /// - `surface` is the wayland surface, whose buffer and subsurfaces buffers shall be imported + /// + /// Note: This will do nothing, if you are not using + /// [`crate::backend::renderer::utils::on_commit_buffer_handler`] + /// to let smithay handle buffer management. + #[cfg(feature = "wayland_frontend")] + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn early_import(&mut self, target: DrmNode, surface: &WlSurface) -> Result<(), Error> + where + A: 'static, + ::Renderer: ImportMemWl + ImportDmaWl + ExportMem, + <::Renderer as ExportMem>::TextureMapping: 'static, + { + use crate::{ + backend::renderer::utils::RendererSurfaceStateUserData, + wayland::compositor::{TraversalAction, with_surface_tree_upward}, + }; + + if self.devices.is_empty() { + return Err(Error::DeviceMissing); + } + + let mut result = Ok(()); + with_surface_tree_upward( + surface, + (), + |_surface, states, _| { + if let Some(data) = states.data_map.get::() { + let mut data_ref = data.lock().unwrap(); + let data = &mut *data_ref; + if data.textures.is_empty() { + // Import a new buffer if available + if let Some(buffer) = data.buffer.as_ref() { + // We do an optimistic optimization here, so contrary to many much more defensive damage-tracking algorithms, + // we only import the most recent set of damage here. + // If we need more on rendering - which we cannot know at this point - we will call import_missing later + // to receive the rest. + // FIXME: We should be able to get rid of this allocation here + let buffer_damage = data.damage().raw().take(1).flatten().cloned().fold( + Vec::>::new(), + |damage, mut rect| { + // replace with drain_filter, when that becomes stable to reuse the original Vec's memory + let (overlapping, mut new_damage): (Vec<_>, Vec<_>) = damage + .into_iter() + .partition(|other| other.overlaps_or_touches(rect)); + + for overlap in overlapping { + rect = rect.merge(overlap); + } + new_damage.push(rect); + new_damage + }, + ); + + if let Err(err) = self.early_import_buffer(target, buffer, states, &buffer_damage) + { + result = Err(err); + } + } + // Now, was the import successful? + if result.is_ok() { + TraversalAction::DoChildren(()) + } else { + // we are not displayed, so our children are neither + TraversalAction::SkipChildren + } + } else { + TraversalAction::SkipChildren + } + } else { + TraversalAction::SkipChildren + } + }, + |_, _, _| {}, + |_, _, _| true, + ); + + result + } + + #[cfg(feature = "wayland_frontend")] + #[profiling::function] + fn early_import_buffer( + &mut self, + target_node: DrmNode, + buffer: &wl_buffer::WlBuffer, + surface: &SurfaceData, + damage: &[Rectangle], + ) -> Result<(), Error> + where + A: 'static, + ::Renderer: ImportMemWl + ImportDmaWl + ExportMem, + <::Renderer as ExportMem>::TextureMapping: 'static, + { + match buffer_type(buffer) { + Some(BufferType::Dma) => { + let dmabuf = get_dmabuf(buffer).unwrap(); + let mut texture = MultiTexture::from_surface(Some(surface), dmabuf.size(), dmabuf.format()); + + if !self.devices.iter().any(|device| target_node == *device.node()) { + return Err(Error::DeviceMissing); + } + + let mut devices = self.devices.iter_mut(); + let first = devices.next().unwrap(); + + let src_node = import_on_src_node(dmabuf, Some(damage), &mut texture, first, None, devices)?; + if src_node != target_node { + let target_id = self + .devices + .iter() + .find_map(|dev| (*dev.node() == target_node).then(|| dev.renderer().context_id())) + .unwrap(); + let src_id = self + .devices + .iter() + .find_map(|dev| { + (*dev.node() == src_node).then(|| dev.renderer().context_id().erased()) + }) + .unwrap(); + + let texture_internal = texture.0.lock().unwrap(); + { + let target_texture = texture_internal.textures.get(&target_id.erased()); + if !matches!( + target_texture, + Some(GpuSingleTexture::Mem { + external_shadow: None, + .. + }) + ) { + // if we don't have a mem texture, either + // - we don't have any import, in that case lets figure this out during first import + // - we have a dma shadow copy, in that case we can't copy without invalidating the shadow buffer + // if the damage doesn't match up with what we be requested during the next import. + // - this also applies if it is an external copy, so we additionally need a shadow buffer before reading from memory. + return Ok(()); + } + } + + // if we do need to do a memory copy, we start with the export early + + let src_texture = match texture_internal.textures.get(&src_id).unwrap() { + GpuSingleTexture::Direct(tex) => tex + .downcast_ref::<<::Renderer as RendererSuper>::TextureId>( + ) + .unwrap(), + _ => unreachable!(), + }; + + let format = src_texture + .format() + // Check the target device supports the format, if not use 8-bit format + .filter(|format| { + self.devices + .iter() + .find(|device| target_node == *device.node()) + .unwrap() + .renderer() + .mem_formats() + .any(|fmt| fmt == *format) + }) + // The GL spec *always* supports this format. + // TODO: Re-evaluate this, once we support vulkan + .unwrap_or(Fourcc::Abgr8888); + + let src_device = self + .devices + .iter_mut() + .find(|device| src_node == *device.node()) + .unwrap(); + + let mappings = { + let damage = damage + .iter() + .flat_map(|rect| rect.intersection(Rectangle::from_size(src_texture.size()))) + .fold(Vec::>::new(), |damage, mut rect| { + // replace with drain_filter, when that becomes stable to reuse the original Vec's memory + let (overlapping, mut new_damage): (Vec<_>, Vec<_>) = damage + .into_iter() + .partition(|other| other.overlaps_or_touches(rect)); + + for overlap in overlapping { + rect = rect.merge(overlap); + } + new_damage.push(rect); + new_damage + }); + damage + .iter() + .copied() + .map(|damage| { + let mapping = src_device + .renderer_mut() + .copy_texture(src_texture, damage, format) + .map_err(Error::Target)?; + Ok((damage, mapping)) + }) + .collect::, Error>>()? + }; + + std::mem::drop(texture_internal); + texture.insert_mapping::( + src_node, + &target_id, + texture.size(), + mappings.into_iter(), + ); + surface.data_map.insert_if_missing_threadsafe(|| texture.0); + } + + Ok(()) + } + #[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" + ))] + Some(BufferType::Egl) => { + // we need specialization for requiring ImportEGL + // or require ImportAll, which will block this function for all + // renderers that cannot import egl buffers, so we just don't + // and sadly go the slow path + Ok(()) + } + Some(BufferType::Shm) => { + // we just need to upload in import_shm_buffer + Ok(()) + } + Some(BufferType::SinglePixel) => { + // no need to do anything + Ok(()) + } + None => { + // whelp, nothing we can do + Ok(()) + } + } + } +} + +/// A graphics api, that supports enumerating graphics devices +pub trait GraphicsApi { + /// Devices this api produces + type Device: ApiDevice; + /// Errors this api returns + type Error: std::error::Error; + + /// Enumerate available devices by: + /// - removing gone devices from list + /// - adding new devices to list + /// + /// Existing devices are guaranteed to be not recreated + fn enumerate(&self, list: &mut Vec) -> Result<(), Self::Error>; + /// Method to force a re-enumeration, e.g. to free resources + fn needs_enumeration(&self) -> bool { + false + } + /// Unique name for representing the api type in log messages + fn identifier() -> &'static str; +} + +/// A device produced by a [`GraphicsApi`]. +pub trait ApiDevice: fmt::Debug { + /// The [`Renderer`] this devices contains + type Renderer: Renderer; + + /// Returns a reference to the underlying renderer + fn renderer(&self) -> &Self::Renderer; + /// Returns a mutable reference to the underlying renderer + fn renderer_mut(&mut self) -> &mut Self::Renderer; + /// Returns an allocator for buffers renderers of this device can sample from and render to + fn allocator(&mut self) -> &mut dyn Allocator; + + /// Returns a [`DrmNode`] representing the graphics device + fn node(&self) -> &DrmNode; + + /// Returns whether the underlying renderer can in principle do cross-device imports. + /// (With no guarantee on being able to import a specific buffer.) + fn can_do_cross_device_imports(&self) -> bool; + + /// Returns whether the [`MultiRenderer`] should attempt exporting buffers from this + /// device to other devices. By default this always returns `true`, but can be used + /// to implement quirks for buggy hardware. + fn should_do_cross_device_exports(&self) -> bool { + true + } +} + +/// Renderer, that transparently copies rendering results to another gpu, +/// as well as transparently importing client buffers residing on different gpus. +pub struct MultiRenderer<'render, 'target, R: GraphicsApi, T: GraphicsApi> { + render: &'render mut R::Device, + target: Option>, + other_renderers: Vec<&'render mut R::Device>, + span: tracing::Span, +} + +impl fmt::Debug for MultiRenderer<'_, '_, R, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MultiRenderer") + .field("render", &self.render) + .field("target", &self.target) + .field("other_renderers", &self.other_renderers) + .finish() + } +} + +impl AsRef<::Renderer> + for MultiRenderer<'_, '_, R, T> +{ + fn as_ref(&self) -> &::Renderer { + self.render.renderer() + } +} + +impl AsMut<::Renderer> + for MultiRenderer<'_, '_, R, T> +{ + fn as_mut(&mut self) -> &mut ::Renderer { + self.render.renderer_mut() + } +} + +impl<'render, 'target, R: GraphicsApi, T: GraphicsApi> MultiRenderer<'render, 'target, R, T> { + /// Converts this renderer into a reference of the target-device's renderer, + /// if it diverges from the render-device. + pub fn target_as_ref(&self) -> Option<&::Renderer> { + self.target.as_ref().map(|data| data.device.renderer()) + } + + /// Converts this renderer into a mutable reference of the target-device's renderer, + /// if it diverges from the render-device. + pub fn target_as_mut(&mut self) -> Option<&mut ::Renderer> { + self.target.as_mut().map(|data| data.device.renderer_mut()) + } + + /// The devices of the render-api, starting with the render-device. + fn render_devices(&mut self) -> impl Iterator { + std::iter::once(&mut *self.render).chain(self.other_renderers.iter_mut().map(|dev| &mut **dev)) + } + + /// Converts this `MultiRenderer` into a `single_renderer` for the provided target device. + /// + /// Will return the current renderer, if it is already a single-device render. + pub fn into_target(self) -> MultiRenderer<'target, 'target, T, T> { + if let Some(target) = self.target { + MultiRenderer { + render: target.device, + target: None, + other_renderers: Vec::new(), + span: self.span, + } + } else { + // Safety: We know R == T and thus 'render == 'target, if target is `None`. + unsafe { + std::mem::transmute::< + MultiRenderer<'render, 'target, R, T>, + MultiRenderer<'target, 'target, T, T>, + >(self) + } + } + } +} + +/// A Framebuffer of a [`MultiRenderer`]. +pub struct MultiFramebuffer<'buffer, T: GraphicsApi>(MultiFramebufferInternal<'buffer, T>); +enum MultiFramebufferInternal<'buffer, T: GraphicsApi> { + Render(<::Renderer as RendererSuper>::Framebuffer<'buffer>), + Target(<::Renderer as RendererSuper>::Framebuffer<'buffer>), +} + +impl<'buffer, T: GraphicsApi> fmt::Debug for MultiFramebuffer<'buffer, T> +where + <::Renderer as RendererSuper>::Framebuffer<'buffer>: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + MultiFramebufferInternal::Render(framebuffer) => framebuffer.fmt(f), + MultiFramebufferInternal::Target(framebuffer) => framebuffer.fmt(f), + } + } +} + +impl Texture for MultiFramebuffer<'_, T> { + fn size(&self) -> Size { + match &self.0 { + MultiFramebufferInternal::Render(framebuffer) => framebuffer.size(), + MultiFramebufferInternal::Target(framebuffer) => framebuffer.size(), + } + } + + fn width(&self) -> u32 { + match &self.0 { + MultiFramebufferInternal::Render(framebuffer) => framebuffer.width(), + MultiFramebufferInternal::Target(framebuffer) => framebuffer.width(), + } + } + + fn height(&self) -> u32 { + match &self.0 { + MultiFramebufferInternal::Render(framebuffer) => framebuffer.height(), + MultiFramebufferInternal::Target(framebuffer) => framebuffer.height(), + } + } + + fn format(&self) -> Option { + match &self.0 { + MultiFramebufferInternal::Render(framebuffer) => framebuffer.format(), + MultiFramebufferInternal::Target(framebuffer) => framebuffer.format(), + } + } +} + +impl<'a, T: GraphicsApi> AsRef<<::Renderer as RendererSuper>::Framebuffer<'a>> + for MultiFramebuffer<'a, T> +{ + fn as_ref(&self) -> &<::Renderer as RendererSuper>::Framebuffer<'a> { + match &self.0 { + MultiFramebufferInternal::Render(fb) => fb, + MultiFramebufferInternal::Target(fb) => fb, + } + } +} + +impl<'a, T: GraphicsApi> AsMut<<::Renderer as RendererSuper>::Framebuffer<'a>> + for MultiFramebuffer<'a, T> +{ + fn as_mut(&mut self) -> &mut <::Renderer as RendererSuper>::Framebuffer<'a> { + match &mut self.0 { + MultiFramebufferInternal::Render(fb) => fb, + MultiFramebufferInternal::Target(fb) => fb, + } + } +} + +/// [`Frame`] implementation of a [`MultiRenderer`]. +/// +/// Leaking the frame will potentially keep it from doing necessary copies +/// of the internal framebuffer for some multi-gpu configurations. The result would +/// be no updated framebuffer contents. +/// Additionally, all problems related to the Frame-implementation of the underlying +/// [`GraphicsApi`] will be present. +pub struct MultiFrame<'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> +where + 'buffer: 'frame, + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + node: DrmNode, + + frame: Option<<::Renderer as RendererSuper>::Frame<'frame, 'buffer>>, + framebuffer: + Option::Renderer as RendererSuper>::Framebuffer<'frame>>>, + target: Option>, + render: *mut &'render mut R::Device, + other_renderers: &'frame mut [&'render mut R::Device], + + dst_transform: Transform, + size: Size, + damage: Vec>, + span: tracing::span::EnteredSpan, +} + +struct TargetData<'target, T: GraphicsApi> { + device: &'target mut T::Device, + cached_buffer: &'target mut Option<(bool, Dmabuf)>, + format: Fourcc, +} + +struct TargetFrameData<'target, 'frame, 'buffer, T: GraphicsApi> { + device: &'frame mut &'target mut T::Device, + framebuffer: &'frame mut <::Renderer as RendererSuper>::Framebuffer<'buffer>, + texture: Option<<::Renderer as RendererSuper>::TextureId>, + format: Fourcc, +} + +impl<'frame, 'buffer, R: GraphicsApi + 'frame, T: GraphicsApi> fmt::Debug + for MultiFrame<'_, '_, 'frame, 'buffer, R, T> +where + 'buffer: 'frame, + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::TextureId: fmt::Debug, + R::Device: fmt::Debug, + T::Device: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MultiFrame") + .field("node", &self.node) + .field("render", unsafe { &*self.render }) + .field("target", &self.target) + .field("dst_transform", &self.dst_transform) + .field("size", &self.size) + .field("damage", &self.damage) + .finish() + } +} + +impl fmt::Debug for TargetData<'_, T> +where + T::Device: fmt::Debug, + <::Renderer as RendererSuper>::TextureId: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TargetData") + .field("device", self.device) + .field("cached_buffer", &self.cached_buffer) + .field("format", &self.format) + .finish() + } +} + +impl fmt::Debug for TargetFrameData<'_, '_, '_, T> +where + T::Device: fmt::Debug, + <::Renderer as RendererSuper>::TextureId: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TargetFrameData") + .field("device", self.device) + .field("format", &self.format) + .finish_non_exhaustive() + } +} + +// These casts are ok, because the frame cannot outlive the MultiFrame, +// see MultiRenderer::render for how this hack works and why it is necessary. + +impl<'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> + AsRef<<::Renderer as RendererSuper>::Frame<'frame, 'buffer>> + for MultiFrame<'_, '_, 'frame, 'buffer, R, T> +where + 'buffer: 'frame, + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + fn as_ref(&self) -> &<::Renderer as RendererSuper>::Frame<'frame, 'buffer> { + self.frame.as_ref().unwrap() + } +} + +impl<'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> + AsMut<<::Renderer as RendererSuper>::Frame<'frame, 'buffer>> + for MultiFrame<'_, '_, 'frame, 'buffer, R, T> +where + 'buffer: 'frame, + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + fn as_mut( + &mut self, + ) -> &mut <::Renderer as RendererSuper>::Frame<'frame, 'buffer> { + self.frame.as_mut().unwrap() + } +} + +impl Offscreen for MultiRenderer<'_, '_, R, T> +where + ::Renderer: Offscreen, + // We need these because the Bind-impl does and Offscreen requires Bind + ::Renderer: Bind, + // We need these because the Renderer-impl does and Offscreen requires Bind, which requires Unbind, which requires Renderer + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn create_buffer( + &mut self, + format: Fourcc, + size: Size, + ) -> Result::Error> { + if let Some(target) = self.target.as_mut() { + target + .device + .renderer_mut() + .create_buffer(format, size) + .map_err(Error::Target) + } else { + // SAFETY: We know this is safe, because `self.target` can only be `None` if R == T. + let renderer = unsafe { + std::mem::transmute::< + &mut ::Renderer, + &mut ::Renderer, + >(self.render.renderer_mut()) + }; + renderer.create_buffer(format, size).map_err(Error::Target) + } + } +} + +impl Bind for MultiRenderer<'_, '_, R, T> +where + ::Renderer: Bind, + // We need this because the Renderer-impl does and Bind requires Unbind, which requires Renderer + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[instrument(level = "trace", parent = &self.span, skip(self, bind))] + #[profiling::function] + fn bind<'a>( + &mut self, + bind: &'a mut Target, + ) -> Result<::Framebuffer<'a>, ::Error> { + if let Some(target) = self.target.as_mut() { + target + .device + .renderer_mut() + .bind(bind) + .map(MultiFramebufferInternal::Target) + .map(MultiFramebuffer) + .map_err(Error::Target) + } else { + // SAFETY: We know this is safe, because `self.target` can only be `None` if R == T. + let renderer = unsafe { + std::mem::transmute::< + &mut ::Renderer, + &mut ::Renderer, + >(self.render.renderer_mut()) + }; + renderer + .bind(bind) + .map(MultiFramebufferInternal::Render) + .map(MultiFramebuffer) + .map_err(Error::Target) + } + } + + fn supported_formats(&self) -> Option { + if let Some(target) = self.target.as_ref() { + Bind::::supported_formats(target.device.renderer()) + } else { + // SAFETY: We know this is safe, because `self.target` can only be `None` if R == T. + let renderer = unsafe { + std::mem::transmute::<&::Renderer, &::Renderer>( + self.render.renderer(), + ) + }; + Bind::::supported_formats(renderer) + } + } +} + +const MAX_CPU_COPIES: usize = 3; // TODO, benchmark this + +impl<'render, 'target, R: GraphicsApi, T: GraphicsApi> RendererSuper for MultiRenderer<'render, 'target, R, T> +where + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + type Error = Error; + type TextureId = MultiTexture; + type Framebuffer<'buffer> = MultiFramebuffer<'buffer, T>; + type Frame<'frame, 'buffer> + = MultiFrame<'render, 'target, 'frame, 'buffer, R, T> + where + 'buffer: 'frame, + Self: 'frame; +} + +impl<'render, 'target, R: GraphicsApi, T: GraphicsApi> Renderer for MultiRenderer<'render, 'target, R, T> +where + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + fn context_id(&self) -> ContextId { + self.render.renderer().context_id().map() + } + + fn downscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.render + .renderer_mut() + .downscale_filter(filter) + .map_err(Error::Render) + } + fn upscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.render + .renderer_mut() + .upscale_filter(filter) + .map_err(Error::Render) + } + + fn set_debug_flags(&mut self, flags: DebugFlags) { + self.render.renderer_mut().set_debug_flags(flags) + } + fn debug_flags(&self) -> DebugFlags { + self.render.renderer().debug_flags() + } + + #[instrument(level = "trace", parent = &self.span, skip(self, framebuffer))] + #[profiling::function] + fn render<'frame, 'buffer>( + &'frame mut self, + framebuffer: &'frame mut Self::Framebuffer<'buffer>, + size: Size, + dst_transform: Transform, + ) -> Result, Self::Error> + where + 'buffer: 'frame, + { + let target_state = if let Some(target) = self.target.as_mut() { + let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal); + + if let Some((_, dmabuf)) = &target.cached_buffer { + if dmabuf.size() != buffer_size || BufferTrait::format(dmabuf).code != target.format { + *target.cached_buffer = None; + } + }; + + if target.cached_buffer.is_none() { + match create_shared_dma_framebuffer::(buffer_size, self.render, target) { + Ok(dmabuf) => { + *target.cached_buffer = Some((true, dmabuf)); + } + Err(err) => { + warn!( + "Error importing dmabuf from {} to {}: {}", + *self.render.node(), + *target.device.node(), + err + ); + info!("Falling back to cpu-copy."); + let modifiers = Bind::::supported_formats(self.render.renderer()) + .unwrap_or_default() + .into_iter() + .filter(|format| format.code == target.format) + .map(|f| f.modifier) + .collect::>(); + let mut dmabuf = self + .render + .allocator() + .create_buffer( + buffer_size.w as u32, + buffer_size.h as u32, + target.format, + &modifiers, + ) + .map_err(Error::AllocatorError)?; + + { + // make sure we mark this as a framebuffer on render first (some GL drivers don't like us to do this later). + let mut render_framebuffer = self + .render + .renderer_mut() + .bind(&mut dmabuf) + .map_err(Error::Render)?; + self.render + .renderer_mut() + .render(&mut render_framebuffer, size, dst_transform) + .map_err(Error::Render)?; + // drop everything + } + + *target.cached_buffer = Some((false, dmabuf)); + } + } + }; + + // try to import on target node + let (direct, dmabuf) = target.cached_buffer.as_mut().unwrap(); + // TODO: We could cache that texture all the way back to the GpuManager in a HashMap. + let texture = (*direct) + .then(|| { + target + .device + .renderer_mut() + .import_dmabuf(dmabuf, Some(&[Rectangle::from_size(buffer_size)])) + .map_err(Error::Target) + }) + .transpose()?; + let framebuffer = self.render.renderer_mut().bind(dmabuf).map_err(Error::Render)?; + + Some((&mut target.device, framebuffer, texture, target.format)) + } else { + None + }; + + let node = *self.render.node(); + let ptr = &mut self.render as *mut _; + + let mut target = None; + let mut new_framebuffer = None; + let frame = match &mut framebuffer.0 { + MultiFramebufferInternal::Render(framebuffer) => { + // SAFETY: We know this is fine, because target can only be `None` (and thus this framebuffer be of variant `Render`), if R == T. + let framebuffer = unsafe { + std::mem::transmute::< + &mut <::Renderer as RendererSuper>::Framebuffer<'buffer>, + &mut <::Renderer as RendererSuper>::Framebuffer<'buffer>, + >(framebuffer) + }; + self.render + .renderer_mut() + .render(framebuffer, size, dst_transform) + .map_err(Error::Render)? + } + MultiFramebufferInternal::Target(target_framebuffer) => { + let (target_device, render_framebuffer, texture, format) = target_state.unwrap(); + target = Some(TargetFrameData { + device: target_device, + framebuffer: target_framebuffer, + texture, + format, + }); + let mut render_framebuffer = AliasableBox::from_unique(Box::new(render_framebuffer)); + + // We extend the lifetime to 'frame, because this is self-referential. + // SAFETY: + // - We drop the framebuffer before `target`, which contains the referenced dmabuf + // - We drop the frame before the framebuffer as we store both in `MultiFrame` + // - `Frame` can't store an invalid pointer into the framebuffer, as the framebuffer is moved + // to the heap and won't be moved by the compiler thanks to `AliasableBox`. + let frame = unsafe { + std::mem::transmute::< + <::Renderer as RendererSuper>::Frame<'_, '_>, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>, + >( + self.render + .renderer_mut() + .render(&mut *render_framebuffer, size, dst_transform) + .map_err(Error::Render)?, + ) + }; + new_framebuffer = Some(render_framebuffer); + frame + } + }; + + let span = trace_span!( + parent: &self.span, + "renderer_multi_frame", + direct = target.as_ref().is_some_and(|t| t.texture.is_some()), + ) + .entered(); + + Ok(MultiFrame { + node, + frame: Some(frame), + framebuffer: new_framebuffer, + render: ptr, // this is fine, as long as we have the frame, this ptr is valid + target, + other_renderers: &mut self.other_renderers, + dst_transform, + size, + damage: Vec::new(), + span, + }) + } + + #[profiling::function] + fn wait(&mut self, sync: &sync::SyncPoint) -> Result<(), Self::Error> { + self.render.renderer_mut().wait(sync).map_err(Error::Render) + } + + #[profiling::function] + fn cleanup_texture_cache(&mut self) -> Result<(), Self::Error> { + let mut result = Ok(()); + for device in self.render_devices() { + result = result.and(cleanup_device_texture_cache(device).map_err(Error::Render)); + } + if let Some(target) = self.target.as_mut() { + result = result.and(cleanup_device_texture_cache(&mut *target.device).map_err(Error::Target)); + } + result + } + + #[profiling::function] + fn invalidate_caches(&mut self) -> Result<(), Self::Error> { + let mut result = Ok(()); + for device in self.render_devices() { + result = result.and(invalidate_device_caches(device).map_err(Error::Render)); + } + if let Some(target) = self.target.as_mut() { + *target.cached_buffer = None; + result = result.and(invalidate_device_caches(&mut *target.device).map_err(Error::Target)); + } + result + } +} + +/// Invalidate a single device's caches, logging a failure against the node it happened on. +/// +/// Callers keep only the first error, so the log is what identifies any device failing after it. +fn invalidate_device_caches( + device: &mut D, +) -> Result<(), ::Error> { + let node = *device.node(); + device + .renderer_mut() + .invalidate_caches() + .inspect_err(|err| warn!("Error invalidating caches of {}: {}", node, err)) +} + +/// Clean up a single device's texture cache, logging a failure against the node it happened on. +/// +/// See [`invalidate_device_caches`] for why the error is logged as well as returned. +fn cleanup_device_texture_cache( + device: &mut D, +) -> Result<(), ::Error> { + let node = *device.node(); + device + .renderer_mut() + .cleanup_texture_cache() + .inspect_err(|err| warn!("Error cleaning up texture cache of {}: {}", node, err)) +} + +fn create_shared_dma_framebuffer( + buffer_size: Size, + src: &mut R::Device, + target: &mut TargetData<'_, T>, +) -> Result> +where + R: GraphicsApi + 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + if !target.device.can_do_cross_device_imports() || !src.should_do_cross_device_exports() { + return Err(Error::ImportFailed); + } + + let target_formats = ImportDma::dmabuf_formats(target.device.renderer()) + .iter() + .filter(|format| format.code == target.format) + .copied() + .collect::(); + let render_formats = Bind::::supported_formats(src.renderer()).unwrap_or_default(); + let formats = target_formats.intersection(&render_formats); + let target_modifiers = formats + .map(|format| format.modifier) + .filter(|modifier| *modifier != Modifier::Invalid) + .collect::>(); + + if target_modifiers.is_empty() { + return Err(Error::ImportFailed); + } + + info!( + "Found dma-copy set for {:?} <-> {:?}: {:?}@{:?}", + src.node(), + target.device.node(), + target.format, + target_modifiers, + ); + + let mut dmabuf = src + .allocator() + .create_buffer( + buffer_size.w as u32, + buffer_size.h as u32, + target.format, + &target_modifiers, + ) + .map_err(Error::AllocatorError)?; + + // verify we can bind on src and import on target + + src.renderer_mut().bind(&mut dmabuf).map_err(Error::Render)?; + + target + .device + .renderer_mut() + .import_dmabuf(&dmabuf, Some(&[Rectangle::from_size(buffer_size)])) + .map_err(Error::Target)?; + + Ok(dmabuf) +} + +impl<'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> MultiFrame<'_, '_, 'frame, 'buffer, R, T> +where + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + fn flush_frame(&mut self) -> Result<(), Error> { + if self.target.is_some() { + let _ = self.finish_internal()?; + // now the frame is gone, lets use our unholy ptr till the end of this call: + // SAFETY: + // - The renderer will never be invalid because the lifetime of the frame must be shorter than the renderer. + // - The pointer can't be aliased because of the following: + // - Creating a frame requires an `&mut` reference to the renderer, making the mutable borrow safe. + // - The mutable reference is used in a function which mutably borrows the frame, that either being `.finish()` + // (which takes ownership of the frame) or dropping the frame. + let render = unsafe { &mut *self.render }; + + // We extend the lifetime to 'frame, because this is self-referential. + // SAFETY: + // - We drop the framebuffer before `target`, which contains the referenced dmabuf + // - We drop the frame before the framebuffer as we store both in `MultiFrame` + // - `Frame` can't store an invalid pointer into the framebuffer, as the framebuffer is moved + // to the heap and won't be moved by the compiler thanks to `AliasableBox`. + let frame = unsafe { + std::mem::transmute::< + <::Renderer as RendererSuper>::Frame<'_, '_>, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>, + >( + render + .renderer_mut() + .render(self.framebuffer.as_mut().unwrap(), self.size, self.dst_transform) + .map_err(Error::Render)?, + ) + }; + self.frame = Some(frame); + } + Ok(()) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn finish_internal(&mut self) -> Result> { + if let Some(frame) = self.frame.take() { + let sync = frame.finish().map_err(Error::Render)?; + + // now the frame is gone, lets use our unholy ptr till the end of this call: + // SAFETY: + // - The renderer will never be invalid because the lifetime of the frame must be shorter than the renderer. + // - The pointer can't be aliased because of the following: + // - Creating a frame requires an `&mut` reference to the renderer, making the mutable borrow safe. + // - The mutable reference is used in a function which mutably borrows the frame, that either being `.finish()` + // (which takes ownership of the frame) or dropping the frame. + let render = unsafe { &mut *self.render }; + + let damage_area = self.dst_transform.transform_size(self.size.to_logical(1)); + let mut damage = std::mem::take(&mut self.damage) + .into_iter() + .map(|rect| rect.to_logical(1).to_buffer(1, self.dst_transform, &damage_area)) + .collect::>(); + + let buffer_size = self.size.to_logical(1).to_buffer(1, Transform::Normal); + if let Some(target) = self.target.as_mut() { + if let Some(texture) = target.texture.as_ref() { + // try gpu copy + let damage = damage + .iter() + .map(|rect| rect.to_logical(1, Transform::Normal, &buffer_size).to_physical(1)) + .collect::>(); + let mut frame = target + .device + .renderer_mut() + .render(target.framebuffer, self.size, Transform::Normal) + .map_err(Error::Target)?; + frame.wait(&sync).map_err(Error::Target)?; + frame + .clear(Color32F::TRANSPARENT, &damage) + .map_err(Error::Target)?; + frame + .render_texture_from_to( + texture, + Rectangle::from_size(buffer_size).to_f64(), + Rectangle::from_size(self.size), + &damage, + &[Rectangle::from_size(self.size)], + Transform::Normal, + 1.0, + ) + .map_err(Error::Target)?; + let sync = frame.finish().map_err(Error::Target)?; + render + .renderer_mut() + .cleanup_texture_cache() + .map_err(Error::Render)?; + + return Ok(sync); + } + + let format = if target + .device + .renderer() + .mem_formats() + .any(|fmt| fmt == target.format) + { + target.format + } else { + // GL mandates this to be supported for downloading, this will truncate 10-bit data + Fourcc::Abgr8888 + }; + + // cpu copy + damage.dedup(); + damage.retain(|rect| rect.overlaps_or_touches(Rectangle::from_size(buffer_size))); + damage.retain(|rect| rect.size.h > 0 && rect.size.w > 0); + + let mut copy_rects = // merge overlapping rectangles + damage.iter().cloned().fold(Vec::new(), |new_damage, mut rect| { + // replace with drain_filter, when that becomes stable to reuse the original Vec's memory + let (overlapping, mut new_damage): (Vec<_>, Vec<_>) = new_damage + .into_iter() + .partition(|other: &Rectangle| other.overlaps_or_touches(rect)); + + for overlap in overlapping { + rect = rect.merge(overlap); + } + new_damage.push(rect); + new_damage + }); + if copy_rects.len() > MAX_CPU_COPIES { + copy_rects = Vec::from([Rectangle::from_size(buffer_size)]); + } + + let mut mappings = Vec::new(); + for rect in copy_rects { + let mapping = ( + ExportMem::copy_framebuffer( + render.renderer_mut(), + self.framebuffer.as_ref().unwrap(), + rect, + format, + ) + .map_err(Error::Render)?, + rect, + ); + mappings.push(mapping); + } + + if mappings.is_empty() { + render + .renderer_mut() + .cleanup_texture_cache() + .map_err(Error::Render)?; + return Ok(sync::SyncPoint::signaled()); + } + + let textures = mappings + .into_iter() + .map(|(mapping, rect)| { + let slice = ExportMem::map_texture(render.renderer_mut(), &mapping) + .map_err(Error::Render::)?; + let texture = target + .device + .renderer_mut() + .import_memory(slice, TextureMapping::format(&mapping), rect.size, false) + .map_err(Error::Target)?; + Ok((texture, rect)) + }) + .collect::, _>>()?; + + let mut frame = target + .device + .renderer_mut() + .render(target.framebuffer, self.size, Transform::Normal) + .map_err(Error::Target)?; + for (texture, rect) in textures { + for damage_rect in damage.iter().filter_map(|dmg_rect| dmg_rect.intersection(rect)) { + let dst = damage_rect + .to_logical(1, Transform::Normal, &buffer_size) + .to_physical(1); + let src = Rectangle::new(damage_rect.loc - rect.loc, damage_rect.size).to_f64(); + let damage = &[Rectangle::from_size(dst.size)]; + frame + .clear(Color32F::TRANSPARENT, &[dst]) + .map_err(Error::Target)?; + frame + .render_texture_from_to( + &texture, + src, + dst, + damage, + &[Rectangle::from_size(self.size)], + Transform::Normal, + 1.0, + ) + .map_err(Error::Target)?; + } + } + let sync = frame.finish().map_err(Error::Target)?; + render + .renderer_mut() + .cleanup_texture_cache() + .map_err(Error::Render)?; + return Ok(sync); + } + + render + .renderer_mut() + .cleanup_texture_cache() + .map_err(Error::Render)?; + return Ok(sync); + } + + Ok(sync::SyncPoint::signaled()) + } +} + +impl Drop for MultiFrame<'_, '_, '_, '_, R, T> +where + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + fn drop(&mut self) { + if let Err(err) = self.finish_internal() { + warn!("Ignored error finishing MultiFrame on drop: {}", err); + } + } +} + +/// [`Texture`]s produced by a [`MultiRenderer`]. +#[derive(Debug, Clone)] +pub struct MultiTexture(Arc>); +#[derive(Debug)] +struct MultiTextureInternal { + textures: HashMap, + size: Size, + format: Option, + #[allow(dead_code)] + buffer_format: Format, +} +// SAFETY: We require `Send` for textures of renderers suitable for the MultiRenderer. +// Type erasure just forces us to do this instead. +unsafe impl Send for MultiTextureInternal {} + +#[cfg(feature = "wayland_frontend")] +pub(crate) fn clear_surface_textures(states: &crate::wayland::compositor::SurfaceData) { + if let Some(texture) = states.data_map.get::>>() { + texture.lock().unwrap().textures.clear(); + } +} + +type DamageAnyTextureMappings = Vec<(Rectangle, Box)>; + +#[derive(Debug)] +enum GpuSingleTexture { + Direct(Box), + Dma { + texture: Box, + dmabuf: Dmabuf, + sync: Option, + }, + Mem { + external_shadow: Option<(Dmabuf, Box)>, + texture: Option>, + mappings: Option<(DrmNode, DamageAnyTextureMappings)>, + }, +} + +impl MultiTexture { + #[cfg(feature = "wayland_frontend")] + #[profiling::function] + fn from_surface( + surface: Option<&crate::wayland::compositor::SurfaceData>, + size: Size, + buffer_format: Format, + ) -> MultiTexture { + let internal = surface + .and_then(|surface| { + surface + .data_map + .get::>>() + .cloned() + }) + .unwrap_or_else(|| { + Arc::new(Mutex::new(MultiTextureInternal { + textures: HashMap::new(), + size, + format: None, + buffer_format, + })) + }); + { + let mut internal = internal.lock().unwrap(); + if internal.size != size || internal.buffer_format != buffer_format { + internal.textures.clear(); + internal.format = None; + internal.size = size; + internal.buffer_format = buffer_format; + } + } + MultiTexture(internal) + } + + fn new(size: Size, buffer_format: Format) -> MultiTexture { + MultiTexture(Arc::new(Mutex::new(MultiTextureInternal { + textures: HashMap::new(), + size, + format: None, + buffer_format, + }))) + } + + /// Create a `MultiTexture` from a renderer `A`-specific texture type. + /// + /// The resulting texture contains only and entry for `A` and can thus + /// only be successfully rendered from `MultiFrame`s where `R` equals `A`. + /// + /// Prefer using `ImportDma` or `ImportMem` for function which handle + /// returning and caching textures for the current renderer automatically. + pub fn from_native_texture( + render_id: &ContextId<<::Renderer as RendererSuper>::TextureId>, + texture: <::Renderer as RendererSuper>::TextureId, + ) -> Option { + let mut multi = Self::new( + texture.size(), + Format { + code: texture.format()?, + modifier: Modifier::Invalid, + }, + ); + multi.0.lock().unwrap().format = texture.format(); + multi.insert_texture::(render_id, texture); + Some(multi) + } + + /// Attempt to get a texture of type `T: Renderer::TextureId` given the renderer type `A` for the given `DrmNode`. + /// + /// Will return `None` if either: + /// + /// - No textures are available for the Renderer type `A`` + /// - No texture of type `T` is available for the given `DrmNode` + pub fn get( + &self, + render_id: &ContextId<<::Renderer as RendererSuper>::TextureId>, + ) -> Option<<::Renderer as RendererSuper>::TextureId> + where + <::Renderer as RendererSuper>::TextureId: Clone + Send + 'static, + { + let tex = self.0.lock().unwrap(); + tex.textures + .get(&render_id.erased()) + .and_then(|texture| match texture { + GpuSingleTexture::Direct(texture) => Some(texture), + GpuSingleTexture::Dma { texture, .. } => Some(texture), + GpuSingleTexture::Mem { texture, .. } => texture.as_ref(), + }) + .and_then(|texture| { + ::downcast_ref::<<::Renderer as RendererSuper>::TextureId>( + &**texture, + ) + }) + .cloned() + } + + fn needs_synchronization( + &self, + render_id: &ContextId<<::Renderer as RendererSuper>::TextureId>, + ) -> Option { + let mut tex = self.0.lock().unwrap(); + tex.textures + .get_mut(&render_id.erased()) + .and_then(|texture| match texture { + GpuSingleTexture::Direct(_) => None, + GpuSingleTexture::Dma { sync, .. } => sync.take(), + GpuSingleTexture::Mem { .. } => None, + }) + } + + fn reimport( + &self, + renderer: &mut ::Renderer, + ) -> Result<(), <::Renderer as RendererSuper>::Error> + where + ::Renderer: ImportDma, + { + let mut tex = self.0.lock().unwrap(); + if let Some(GpuSingleTexture::Dma { texture, dmabuf, .. }) = + tex.textures.get_mut(&renderer.context_id().erased()) + { + *texture = Box::new(renderer.import_dmabuf(dmabuf, None)?) as Box<_>; + } + Ok(()) + } + + fn insert_texture( + &mut self, + render_id: &ContextId<<::Renderer as RendererSuper>::TextureId>, + texture: <::Renderer as RendererSuper>::TextureId, + ) where + <::Renderer as RendererSuper>::TextureId: 'static, + { + let mut tex = self.0.lock().unwrap(); + let format = texture.format(); + if format != tex.format && !tex.textures.is_empty() { + warn!(has = ?tex.format, got = ?format, "Multi-SubTexture with wrong format!"); + return; + } + tex.format = format; + + let render_id = render_id.erased(); + trace!( + "Inserting into: {:p} for {:?}: {:?}", + Arc::as_ptr(&self.0), + render_id, + tex + ); + tex.textures + .insert(render_id, GpuSingleTexture::Direct(Box::new(texture) as Box<_>)); + } + + #[cfg(feature = "wayland_frontend")] + fn insert_mapping< + R: GraphicsApi + 'static, + T: GraphicsApi + 'static, + I: Iterator< + Item = ( + Rectangle, + <::Renderer as ExportMem>::TextureMapping, + ), + >, + >( + &mut self, + source: DrmNode, + render_id: &ContextId<<::Renderer as RendererSuper>::TextureId>, + size: Size, + new_mappings: I, + ) where + ::Renderer: ExportMem, + <::Renderer as ExportMem>::TextureMapping: 'static, + { + let mut tex_ref = self.0.lock().unwrap(); + let tex = &mut *tex_ref; + + let render_id = render_id.erased(); + let (old_texture, old_mapping, external_shadow) = tex + .textures + .remove(&render_id) + .map(|single| match single { + GpuSingleTexture::Mem { + texture, + mappings, + external_shadow, + } => (texture, mappings, external_shadow), + _ => (None, None, None), + }) + .unwrap_or((None, None, None)); + let old_texture = old_texture.filter(|tex| { + ::downcast_ref::<<::Renderer as RendererSuper>::TextureId>(tex) + .map(|tex| tex.size()) + == Some(size) + }); + + let mut mappings = old_mapping + .filter(|(old_src, _)| *old_src == source) + .map(|(_, mappings)| mappings) + .unwrap_or_default(); + + // don't keep old mappings that are superseded by new ones + let format = tex.format; + let new_mappings = new_mappings + .filter(|(_, mapping)| { + let mapping_fmt = TextureMapping::format(mapping); + if old_texture.is_some() && Some(mapping_fmt) != format { + warn!(has = ?format, got = ?mapping_fmt, "Multi-SubTexture Mapping with wrong format!"); + false + } else { + tex.format = Some(mapping_fmt); + true + } + }) + .map(|(r, m)| (r, Box::new(m) as Box)) + .collect::>(); + mappings.retain(|(region, _)| { + !new_mappings + .iter() + .any(|(new_region, _)| new_region.contains_rect(*region)) + }); + mappings.extend(new_mappings); + + tex.textures.insert( + render_id, + GpuSingleTexture::Mem { + mappings: Some((source, mappings)), + texture: old_texture, + external_shadow, + }, + ); + } +} + +impl Texture for MultiTexture { + fn size(&self) -> Size { + self.0.lock().unwrap().size + } + fn width(&self) -> u32 { + self.0.lock().unwrap().size.w as u32 + } + fn height(&self) -> u32 { + self.0.lock().unwrap().size.h as u32 + } + fn format(&self) -> Option { + self.0.lock().unwrap().format + } +} + +impl Frame for MultiFrame<'_, '_, '_, '_, R, T> +where + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + type Error = Error; + type TextureId = MultiTexture; + + fn context_id(&self) -> ContextId { + self.frame.as_ref().unwrap().context_id().map() + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn clear(&mut self, color: Color32F, at: &[Rectangle]) -> Result<(), Error> { + self.damage.extend(at); + self.frame + .as_mut() + .unwrap() + .clear(color, at) + .map_err(Error::Render) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn draw_solid( + &mut self, + dst: Rectangle, + damage: &[Rectangle], + color: Color32F, + ) -> Result<(), Self::Error> { + self.damage.extend(damage.iter().copied().map(|mut rect| { + rect.loc += dst.loc; + rect + })); + self.frame + .as_mut() + .unwrap() + .draw_solid(dst, damage, color) + .map_err(Error::Render) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn render_texture_from_to( + &mut self, + texture: &MultiTexture, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + src_transform: Transform, + alpha: f32, + ) -> Result<(), Error> { + let render_id = self.frame.as_mut().unwrap().context_id(); + let sync = texture.needs_synchronization::(&render_id); + if let Some(sync) = sync { + if let Err(err) = self.frame.as_mut().unwrap().wait(&sync) { + trace!(?err, "Failed to import sync point, blocking"); + let _ = sync.wait(); + } + } + texture + .reimport::(unsafe { (*self.render).renderer_mut() }) + .map_err(Error::Render)?; + + if let Some(texture) = texture.get::(&render_id) { + self.damage.extend(damage.iter().copied().map(|mut rect| { + rect.loc += dst.loc; + rect + })); + self.frame + .as_mut() + .unwrap() + .render_texture_from_to(&texture, src, dst, damage, opaque_regions, src_transform, alpha) + .map_err(Error::Render) + } else { + warn!( + "Failed to render texture {:?}, import for wrong devices {:?}? {:?}", + Arc::as_ptr(&texture.0), + self.node, + texture.0.lock().unwrap(), + ); + Ok(()) + } + } + + fn transformation(&self) -> Transform { + self.frame.as_ref().unwrap().transformation() + } + fn output_size(&self) -> Size { + self.frame.as_ref().unwrap().output_size() + } + + #[profiling::function] + fn wait(&mut self, sync: &sync::SyncPoint) -> Result<(), Self::Error> { + self.frame.as_mut().unwrap().wait(sync).map_err(Error::Render) + } + + #[profiling::function] + fn finish(mut self) -> Result { + self.finish_internal() + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportMemWl for MultiRenderer<'_, '_, R, T> +where + ::Renderer: ImportMemWl, + // We need this because the Renderer-impl does and ImportMem requires Renderer + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_shm_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result<::TextureId, ::Error> { + let shm_texture = self + .render + .renderer_mut() + .import_shm_buffer(buffer, surface, damage) + .map_err(Error::Render)?; + let (dimensions, format) = shm::with_buffer_contents(buffer, |_, _, data| { + Ok(( + (data.width, data.height).into(), + shm::shm_format_to_fourcc(data.format) + .map(|code| Format { + code, + modifier: Modifier::Linear, + }) + .ok_or(Error::ImportFailed)?, + )) + }) + .map_err(|_| Error::ImportFailed)??; + let mut texture = MultiTexture::from_surface(surface, dimensions, format); + texture.insert_texture::(&self.render.renderer().context_id(), shm_texture); + Ok(texture) + } + + fn shm_formats(&self) -> Box> { + ImportMemWl::shm_formats(self.render.renderer()) + } +} + +impl ImportMem for MultiRenderer<'_, '_, R, T> +where + ::Renderer: ImportMem, + // We need this because the Renderer-impl does and ImportMem requires Renderer + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_memory( + &mut self, + data: &[u8], + format: Fourcc, + size: Size, + flipped: bool, + ) -> Result<::TextureId, ::Error> { + let mem_texture = self + .render + .renderer_mut() + .import_memory(data, format, size, flipped) + .map_err(Error::Render)?; + let mut texture = MultiTexture::new( + size, + Format { + code: format, + modifier: Modifier::Linear, + }, + ); + texture.insert_texture::(&self.render.renderer().context_id(), mem_texture); + Ok(texture) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn update_memory( + &mut self, + texture: &::TextureId, + data: &[u8], + region: Rectangle, + ) -> Result<(), ::Error> { + let mem_texture = texture + .get::(&self.render.renderer().context_id()) + .ok_or_else(|| Error::MismatchedDevice(*self.render.node()))?; + self.render + .renderer_mut() + .update_memory(&mem_texture, data, region) + .map_err(Error::Render) + } + + fn mem_formats(&self) -> Box> { + ImportMem::mem_formats(self.render.renderer()) + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportDmaWl for MultiRenderer<'_, '_, R, T> +where + ::Renderer: ImportDmaWl + ImportMem + ExportMem, + ::Renderer: Bind + ExportMem, + <::Renderer as ExportMem>::TextureMapping: 'static, + <::Renderer as ExportMem>::TextureMapping: 'static, + T: 'static, + // We need this because the Renderer-impl does and ImportDma requires Renderer + R: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: Bind + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_dma_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&SurfaceData>, + damage: &[Rectangle], + ) -> Result<::TextureId, ::Error> { + let dmabuf = get_dmabuf(buffer).expect("import_dma_buffer without checking buffer type?"); + let texture = MultiTexture::from_surface(surface, dmabuf.size(), dmabuf.format()); + let texture_ref = texture.0.clone(); + let res = import_dmabuf_internal( + self.render, + self.target.as_mut().map(|target| &mut *target.device), + &mut self.other_renderers, + dmabuf, + texture, + Some(damage), + ); + if res.is_ok() { + if let Some(surface) = surface { + surface.data_map.insert_if_missing_threadsafe(|| texture_ref); + } + } + res + } +} + +impl ImportDma for MultiRenderer<'_, '_, R, T> +where + ::Renderer: ImportDma + ImportMem, + ::Renderer: Bind + ExportMem, + <::Renderer as ExportMem>::TextureMapping: 'static, + <::Renderer as ExportMem>::TextureMapping: 'static, + T: 'static, + // We need this because the Renderer-impl does and ImportDma requires Renderer + R: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + fn dmabuf_formats(&self) -> FormatSet { + ImportDma::dmabuf_formats(self.render.renderer()) + } + + fn has_dmabuf_format(&self, format: Format) -> bool { + ImportDma::has_dmabuf_format(self.render.renderer(), format) + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn import_dmabuf( + &mut self, + dmabuf: &Dmabuf, + damage: Option<&[Rectangle]>, + ) -> Result<::TextureId, ::Error> { + let texture = MultiTexture::new(dmabuf.size(), dmabuf.format()); + import_dmabuf_internal( + self.render, + self.target.as_mut().map(|target| &mut *target.device), + &mut self.other_renderers, + dmabuf, + texture, + damage, + ) + } +} + +fn import_on_src_node<'a, R, T>( + dmabuf: &Dmabuf, + damage: Option<&[Rectangle]>, + texture: &mut MultiTexture, + render: &mut R::Device, + mut target: Option<&mut &mut T::Device>, + mut others: impl Iterator, +) -> Result> +where + R: GraphicsApi + 'static, + ::Device: 'a, + <::Device as ApiDevice>::Renderer: Renderer + ImportDma, + <<::Device as ApiDevice>::Renderer as RendererSuper>::TextureId: 'static, + T: GraphicsApi + 'static, + <::Device as ApiDevice>::Renderer: Renderer + ImportDma, + <<::Device as ApiDevice>::Renderer as RendererSuper>::TextureId: 'static, +{ + match dmabuf.node() { + Some(node) => { + if node == *render.node() { + let renderer = render.renderer_mut(); + let imported = renderer.import_dmabuf(dmabuf, damage).map_err(Error::Render)?; + texture.insert_texture::(&renderer.context_id(), imported); + } else if target.as_ref().is_some_and(|target| node == *target.node()) { + let renderer = target.unwrap().renderer_mut(); + let imported = renderer.import_dmabuf(dmabuf, damage).map_err(Error::Target)?; + texture.insert_texture::(&renderer.context_id(), imported); + } else if let Some(other) = others.find(|other| node == *(other.node())) { + let renderer = other.renderer_mut(); + let imported = renderer.import_dmabuf(dmabuf, damage).map_err(Error::Render)?; + texture.insert_texture::(&renderer.context_id(), imported); + } else { + return Err(Error::DeviceMissing); + }; + + Ok(node) + } + None => { + // try them all + let node = if let Some(imported) = render + .can_do_cross_device_imports() + .then(|| { + render + .renderer_mut() + .import_dmabuf(dmabuf, damage) + .inspect_err(|err| { + debug!(?err, "failed to import dmabuf on render node {0}", render.node()) + }) + .ok() + }) + .flatten() + { + texture.insert_texture::(&render.renderer().context_id(), imported); + *render.node() + } else if let Some(imported) = target.as_mut().and_then(|target| { + target + .can_do_cross_device_imports() + .then(|| { + target + .renderer_mut() + .import_dmabuf(dmabuf, damage) + .inspect_err(|err| { + debug!(?err, "failed to import dmabuf on target node {0}", target.node()) + }) + .ok() + }) + .flatten() + }) { + let target = target.as_ref().unwrap(); + texture.insert_texture::(&target.renderer().context_id(), imported); + *target.node() + } else if let Some((other, imported)) = others.find_map(|other| { + other + .can_do_cross_device_imports() + .then(|| { + other + .renderer_mut() + .import_dmabuf(dmabuf, damage) + .inspect_err(|err| { + debug!(?err, "failed to import dmabuf on other node {0}", other.node()) + }) + .ok() + .map(|imported| (other, imported)) + }) + .flatten() + }) { + texture.insert_texture::(&other.renderer().context_id(), imported); + *other.node() + } else { + return Err(Error::DeviceMissing); + }; + dmabuf.set_node(node); + + Ok(node) + } + } +} + +fn dma_shadow_copy( + src_texture: &<::Renderer as RendererSuper>::TextureId, + damage: Option<&[Rectangle]>, + slot: &mut Option<(Dmabuf, Box, Option)>, + src: &mut S::Device, + mut target: Option<&mut T::Device>, + transfer_format: Option, +) -> Result<(), Error> +where + S: GraphicsApi, + T: GraphicsApi, + ::Renderer: Renderer + ImportDma + Bind, + ::Renderer: Renderer + ImportDma, + <::Renderer as RendererSuper>::TextureId: 'static, + <::Renderer as RendererSuper>::TextureId: 'static, +{ + if target + .as_ref() + .is_some_and(|target| !target.can_do_cross_device_imports() || !src.should_do_cross_device_exports()) + { + return Err(Error::ImportFailed); + } + + let format = src_texture.format().unwrap_or(Fourcc::Abgr8888); + + let ((shadow_buffer, _, existing_sync_point), is_new_buffer) = if slot.is_some() { + (slot.as_mut().unwrap(), false) + } else { + let read_formats = if let Some(target) = target.as_ref() { + ImportDma::dmabuf_formats(target.renderer()) + } else { + ImportDma::dmabuf_formats(src.renderer()) + }; + let write_formats = Bind::::supported_formats(src.renderer()).ok_or(Error::ImportFailed)?; + let candidates = read_formats + .intersection(&write_formats) + .filter(|f| f.modifier != Modifier::Invalid) + .copied() + .collect::(); + + if candidates.indexset().is_empty() { + return Err(Error::ImportFailed); + } + + let transfer_format = match transfer_format { + Some(fmt) if candidates.iter().any(|f| f.code == fmt) => fmt, + Some(_) => return Err(Error::ImportFailed), + None => { + if candidates.iter().any(|f| f.code == format) { + format + } else { + let bpp = get_bpp(format).unwrap_or(8); + if let Some(f) = candidates + .iter() + .find(|f| get_bpp(f.code).is_some_and(|val| val == bpp)) + { + f.code + } else { + candidates + .iter() + .find(|f| get_bpp(f.code).is_some_and(|val| val == 8)) + .map(|f| f.code) + .ok_or(Error::ImportFailed)? + } + } + } + }; + + let modifiers = candidates + .into_iter() + .filter(|f| f.code == transfer_format) + .map(|f| f.modifier) + .collect::>(); + + if modifiers.is_empty() { + return Err(Error::ImportFailed); + } + + let shadow_buffer = src + .allocator() + .create_buffer( + src_texture.width(), + src_texture.height(), + transfer_format, + &modifiers, + ) + .map_err(Error::AllocatorError)?; + + let target_texture = if let Some(target) = target.as_mut() { + Box::<<::Renderer as RendererSuper>::TextureId>::new( + target + .renderer_mut() + .import_dmabuf(&shadow_buffer, None) + .map_err(Error::Target)?, + ) as Box + } else { + Box::<<::Renderer as RendererSuper>::TextureId>::new( + src.renderer_mut() + .import_dmabuf(&shadow_buffer, None) + .map_err(Error::Render)?, + ) as Box + }; + (slot.insert((shadow_buffer, target_texture, None)), true) + }; + + let src_renderer = src.renderer_mut(); + if let Some(sync) = existing_sync_point.take() { + if let Err(err) = src_renderer.wait(&sync) { + debug!(?err, "Unable to wait for existing sync_point, blocking.."); + let _ = sync.wait(); + } + } + let mut framebuffer = src_renderer.bind(shadow_buffer).map_err(Error::Render)?; + let shadow_size = Size::from((src_texture.width() as i32, src_texture.height() as i32)); + let mut frame = src_renderer + .render(&mut framebuffer, shadow_size, Transform::Normal) + .map_err(Error::Render)?; + + let damage_slice = [Rectangle::from_size(shadow_size)]; + let damage = unsafe { + std::mem::transmute::]>, Option<&[Rectangle]>>( + damage, + ) + } // TODO: use bytemuck for stuff like this? + .filter(|_| !is_new_buffer) + .unwrap_or(&damage_slice); + + frame + .clear(Color32F::TRANSPARENT, damage) + .map_err(Error::Render)?; + frame + .render_texture_from_to( + src_texture, + Rectangle::from_size(src_texture.size()).to_f64(), + Rectangle::from_size(shadow_size), + damage, + &[], + Transform::Normal, + 1.0, + ) + .map_err(Error::Render)?; + *existing_sync_point = Some(frame.finish().map_err(Error::Render)?); + + // shadow buffer contains our copy and is readable by target and the original buffer was never migrated + Ok(()) +} + +type BoxedTextureMappingAndDamage = ( + Box<<<::Device as ApiDevice>::Renderer as ExportMem>::TextureMapping>, + Rectangle, +); + +type MemTexture = ( + Option>>, + Option::Device as ApiDevice>::Renderer as RendererSuper>::TextureId>>, +); + +fn mem_copy( + src_texture: &<::Renderer as RendererSuper>::TextureId, + damage: Option<&[Rectangle]>, + slot: &mut Option>, + src: &mut S::Device, + target: &mut T::Device, +) -> Result<(), Error> +where + S: GraphicsApi, + T: GraphicsApi, + ::Renderer: Renderer + ExportMem + Bind, + ::Renderer: Renderer + ImportMem, +{ + let format = src_texture + .format() + // Check the target device supports the format, if not use 8-bit format + .filter(|format| target.renderer().mem_formats().any(|fmt| fmt == *format)) + // The GL spec *always* supports this format. + // TODO: Re-evaluate this, once we support vulkan + .unwrap_or(Fourcc::Abgr8888); + + let texture_rect = Rectangle::from_size((src_texture.width() as i32, src_texture.height() as i32).into()); + let damage = damage.map(|damage| { + damage + .iter() + .flat_map(|rect| rect.intersection(texture_rect)) + .fold(Vec::>::new(), |damage, mut rect| { + // replace with drain_filter, when that becomes stable to reuse the original Vec's memory + let (overlapping, mut new_damage): (Vec<_>, Vec<_>) = damage + .into_iter() + .partition(|other| other.overlaps_or_touches(rect)); + + for overlap in overlapping { + rect = rect.merge(overlap); + } + new_damage.push(rect); + new_damage + }) + }); + + if slot.is_some() { + let (mapping, texture) = slot.as_mut().unwrap(); + let mappings = match mapping.take() { + Some(mut mappings) => { + mappings.retain(|(mapping, _)| TextureMapping::format(&**mapping) == format); + + let damage_slice = [texture_rect]; + let new_damage = damage + .as_deref() + .unwrap_or(&damage_slice) + .iter() + .filter(|rect| !mappings.iter().any(|(_, region)| region.contains_rect(**rect))) + .copied() + .collect::>(); + + if texture.is_none() + && (mappings.len() != 1 + || ::size(&*mappings[0].0) != texture_rect.size + || !new_damage.is_empty()) + { + let mapping = src + .renderer_mut() + .copy_texture(src_texture, texture_rect, format) + .map_err(Error::Render)?; + trace!("Creating mapping for: {:?}", damage); + mappings = vec![(Box::new(mapping), texture_rect)]; + } else { + mappings.extend( + new_damage + .into_iter() + .map(|damage| { + let mapping = src + .renderer_mut() + .copy_texture(src_texture, damage, format) + .map_err(Error::Render)?; + trace!("Creating mapping for: {:?}", damage); + Ok((Box::new(mapping), damage)) + }) + .collect::, Error>>()?, + ); + } + + mappings + } + None => { + let mapping = src + .renderer_mut() + .copy_texture(src_texture, texture_rect, format) + .map_err(Error::Render)?; + trace!("Creating mapping for: {:?}", damage); + vec![(Box::new(mapping), texture_rect)] + } + }; + + for (mapping, damage) in mappings { + let data = src.renderer_mut().map_texture(&mapping).map_err(Error::Render)?; + if let Some(texture) = texture.as_mut() { + trace!( + "Updating texture {:?} with mapping at {:?}", + texture.size(), + damage, + ); + target + .renderer_mut() + .update_memory(texture, data, damage) + .map_err(Error::Target)?; + } else { + trace!("Importing mapping as full buffer {:?}", mapping.size()); + let target_texture = target + .renderer_mut() + .import_memory(data, format, texture_rect.size, false) + .map_err(Error::Target)?; + *texture = Some(Box::new(target_texture)); + } + } + } else { + let mapping = src + .renderer_mut() + .copy_texture(src_texture, texture_rect, format) + .map_err(Error::Render)?; + trace!("Importing mapping as full buffer {:?}", mapping.size()); + let data = src.renderer_mut().map_texture(&mapping).map_err(Error::Render)?; + + let target_texture = target + .renderer_mut() + .import_memory(data, format, texture_rect.size, false) + .map_err(Error::Target)?; + *slot = Some((None, Some(Box::new(target_texture)))); + }; + + Ok(()) +} + +fn texture_copy( + src: &mut S::Device, + target: &mut T::Device, + src_texture: &<::Renderer as RendererSuper>::TextureId, + target_texture: &mut Option, + damage: Option<&[Rectangle]>, +) -> Result<(), Error> +where + S: GraphicsApi, + T: GraphicsApi, + ::Renderer: Renderer + ImportDma + ExportMem + Bind, + <::Renderer as ExportMem>::TextureMapping: 'static, + ::Renderer: Renderer + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: 'static, + <::Renderer as RendererSuper>::TextureId: 'static, +{ + // we need to reset in a few cases + if matches!(&target_texture, Some(GpuSingleTexture::Direct(_))) + || matches!( + &target_texture, + Some(GpuSingleTexture::Mem { mappings: Some((node, _)), .. }) if node != src.node() + ) + { + *target_texture = None; + } + + match target_texture.take() { + Some(GpuSingleTexture::Direct(_)) => unreachable!(), + Some(GpuSingleTexture::Mem { + mut external_shadow, + texture, + mappings, + }) => { + if let Some((dmabuf, texture)) = external_shadow.take() { + let mut slot = Some((dmabuf, texture, None)); + dma_shadow_copy::(src_texture, damage, &mut slot, src, None, None) + .map_err(Error::generalize::)?; + external_shadow = slot.map(|(dmabuf, texture, sync_point)| { + if let Some(sync) = sync_point { + // ignore interrupt errors + src.renderer_mut().wait(&sync).unwrap_or_else(|_| { + let _ = sync.wait(); + }); + } + (dmabuf, texture) + }); + } + + let mut slot = Some(( + mappings.map(|(_, mappings)| mappings.into_iter().map(|(damage, mapping)| + (mapping.downcast::<<::Renderer as ExportMem>::TextureMapping>().unwrap(), damage) + ).collect::>()), + texture.map(|texture| texture.downcast::<<::Renderer as RendererSuper>::TextureId>().unwrap()) + )); + + let src_texture = external_shadow + .as_ref() + .map(|(_, texture)| { + texture + .downcast_ref::<<::Renderer as RendererSuper>::TextureId>() + .unwrap() + }) + .unwrap_or(src_texture); + let res = mem_copy::(src_texture, damage, &mut slot, src, target); + *target_texture = slot.map(|(mappings, texture)| GpuSingleTexture::Mem { + external_shadow, + texture: texture.map(|texture| texture as Box), + mappings: mappings.map(|mappings| { + ( + *src.node(), + mappings + .into_iter() + .map(|(mapping, damage)| (damage, mapping as Box)) + .collect(), + ) + }), + }); + res + } + Some(GpuSingleTexture::Dma { + texture, + dmabuf, + sync, + }) => { + let mut slot = Some((dmabuf, texture, sync)); + let res = dma_shadow_copy::(src_texture, damage, &mut slot, src, Some(target), None); + *target_texture = slot.map(|(dmabuf, texture, sync)| GpuSingleTexture::Dma { + texture, + dmabuf, + sync, + }); + res + } + None => { + let mut slot = None; + match dma_shadow_copy::(src_texture, damage, &mut slot, src, Some(target), None) { + Ok(()) => { + *target_texture = slot.map(|(dmabuf, texture, sync)| GpuSingleTexture::Dma { + texture: texture as Box, + dmabuf, + sync, + }); + Ok(()) + } + Err(err) => { + trace!(?err, "Dma shadow copy failed, falling back to cpu"); + + let mut external_shadow = None; + if !ExportMem::can_read_texture(src.renderer_mut(), src_texture).map_err(Error::Render)? { + let mut slot = None; + dma_shadow_copy::(src_texture, damage, &mut slot, src, None, None) + .map_err(Error::generalize::)?; + external_shadow = slot.map(|(dmabuf, texture, sync_point)| { + if let Some(sync) = sync_point { + // ignore interrupt errors + src.renderer_mut().wait(&sync).unwrap_or_else(|_| { + let _ = sync.wait(); + }); + } + (dmabuf, texture as Box) + }); + }; + + let mut slot = None; + let mem_src_texture = external_shadow + .as_ref() + .map(|(_, texture)| { + texture + .downcast_ref::<<::Renderer as RendererSuper>::TextureId>() + .unwrap() + }) + .unwrap_or(src_texture); + + let res = match mem_copy::(mem_src_texture, damage, &mut slot, src, target) { + Ok(res) => Ok(res), + Err(_) => { + let mut dma_slot = None; + dma_shadow_copy::( + src_texture, + damage, + &mut dma_slot, + src, + None, + Some(Fourcc::Abgr8888), + ) + .map_err(Error::generalize::)?; + external_shadow = dma_slot.map(|(dmabuf, texture, sync_point)| { + if let Some(sync) = sync_point { + // ignore interrupt errors + src.renderer_mut().wait(&sync).unwrap_or_else(|_| { + let _ = sync.wait(); + }); + } + (dmabuf, texture as Box) + }); + let src_texture = external_shadow + .as_ref() + .map(|(_, texture)| { + texture + .downcast_ref::<<::Renderer as RendererSuper>::TextureId>() + .unwrap() + }) + .unwrap_or(src_texture); + mem_copy::(src_texture, damage, &mut slot, src, target) + } + }; + + *target_texture = slot.map(|(mappings, texture)| GpuSingleTexture::Mem { + texture: texture.map(|texture| texture as Box), + mappings: mappings.map(|mappings| { + ( + *src.node(), + mappings + .into_iter() + .map(|(mapping, damage)| (damage, mapping as Box)) + .collect(), + ) + }), + external_shadow, + }); + res + } + } + } + } +} + +fn import_dmabuf_internal( + render: &mut R::Device, + mut target: Option<&mut T::Device>, + other_renderers: &mut [&mut R::Device], + dmabuf: &Dmabuf, + mut texture: MultiTexture, + damage: Option<&[Rectangle]>, +) -> Result> +where + R: GraphicsApi + 'static, + <::Device as ApiDevice>::Renderer: + Renderer + ImportDma + ExportMem + ImportMem + Bind, + <::Renderer as ExportMem>::TextureMapping: 'static, + <<::Device as ApiDevice>::Renderer as RendererSuper>::TextureId: 'static, + T: GraphicsApi + 'static, + <::Device as ApiDevice>::Renderer: Renderer + ImportDma + ExportMem + Bind, + <<::Device as ApiDevice>::Renderer as RendererSuper>::TextureId: 'static, +{ + let src_node = import_on_src_node::( + dmabuf, + damage, + &mut texture, + render, + target.as_mut(), + other_renderers.iter_mut().map(|x| &mut **x), + )?; + + if src_node == *render.node() { + // when we are on the same node, we are done + Ok(texture) + } else { + // else we need to copy + let mut texture_internal = texture.0.lock().unwrap(); + let target_id = render.renderer().context_id().erased(); + let mut target_texture = texture_internal.textures.remove(&target_id); + + let res = if let Some(target) = target.as_mut().filter(|target| src_node == *target.node()) { + let src_id = target.renderer().context_id().erased(); + let src_texture = match texture_internal.textures.get(&src_id).unwrap() { + GpuSingleTexture::Direct(tex) => tex + .downcast_ref::<<::Renderer as RendererSuper>::TextureId>() + .unwrap(), + _ => unreachable!(), + }; + + texture_copy::(target, render, src_texture, &mut target_texture, damage) + .map_err(Error::transpose) + } else if let Some(other) = other_renderers.iter_mut().find(|other| src_node == *other.node()) { + let src_id = other.renderer().context_id().erased(); + let src_texture = match texture_internal.textures.get(&src_id).unwrap() { + GpuSingleTexture::Direct(tex) => tex + .downcast_ref::<<::Renderer as RendererSuper>::TextureId>() + .unwrap(), + _ => unreachable!(), + }; + + texture_copy::(other, render, src_texture, &mut target_texture, damage) + .map_err(Error::generalize::) + } else { + Err(Error::DeviceMissing) + }; + + if let Some(target_texture) = target_texture.filter(|_| res.is_ok()) { + texture_internal.textures.insert(target_id, target_texture); + } + + std::mem::drop(texture_internal); + res.map(|_| texture) + } +} + +/// [`TextureMapping`]s produced by [`ExportMem`]-implementations of +/// [`MultiRenderer`]s. +pub struct MultiTextureMapping(TextureMappingInternal) +where + ::Renderer: ExportMem, + ::Renderer: ExportMem; +enum TextureMappingInternal +where + ::Renderer: ExportMem, + ::Renderer: ExportMem, +{ + Either(<::Renderer as ExportMem>::TextureMapping), + Or(<::Renderer as ExportMem>::TextureMapping), +} +impl fmt::Debug for MultiTextureMapping +where + ::Renderer: ExportMem, + ::Renderer: ExportMem, + <::Renderer as ExportMem>::TextureMapping: fmt::Debug, + <::Renderer as ExportMem>::TextureMapping: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + TextureMappingInternal::Either(mapping) => mapping.fmt(f), + TextureMappingInternal::Or(mapping) => mapping.fmt(f), + } + } +} + +impl Texture for MultiTextureMapping +where + ::Renderer: ExportMem, + ::Renderer: ExportMem, +{ + fn size(&self) -> Size { + match self { + MultiTextureMapping::(TextureMappingInternal::Either(x)) => x.size(), + MultiTextureMapping::(TextureMappingInternal::Or(x)) => x.size(), + } + } + + fn width(&self) -> u32 { + match self { + MultiTextureMapping::(TextureMappingInternal::Either(x)) => x.width(), + MultiTextureMapping::(TextureMappingInternal::Or(x)) => x.width(), + } + } + fn height(&self) -> u32 { + match self { + MultiTextureMapping::(TextureMappingInternal::Either(x)) => x.height(), + MultiTextureMapping::(TextureMappingInternal::Or(x)) => x.height(), + } + } + fn format(&self) -> Option { + match self { + MultiTextureMapping::(TextureMappingInternal::Either(x)) => Texture::format(x), + MultiTextureMapping::(TextureMappingInternal::Or(x)) => Texture::format(x), + } + } +} +impl TextureMapping for MultiTextureMapping +where + ::Renderer: ExportMem, + ::Renderer: ExportMem, +{ + fn flipped(&self) -> bool { + match self { + MultiTextureMapping::(TextureMappingInternal::Either(x)) => x.flipped(), + MultiTextureMapping::(TextureMappingInternal::Or(x)) => x.flipped(), + } + } + + fn format(&self) -> Fourcc { + match self { + MultiTextureMapping::(TextureMappingInternal::Either(x)) => TextureMapping::format(x), + MultiTextureMapping::(TextureMappingInternal::Or(x)) => TextureMapping::format(x), + } + } +} + +impl ExportMem for MultiRenderer<'_, '_, R, T> +where + ::Renderer: ExportMem, + ::Renderer: ExportMem, + // We need this because the Renderer-impl does and ExportMem requires Renderer + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + type TextureMapping = MultiTextureMapping; + + #[instrument(level = "trace", parent = &self.span, skip(self, framebuffer))] + #[profiling::function] + fn copy_framebuffer( + &mut self, + framebuffer: &MultiFramebuffer<'_, T>, + region: Rectangle, + format: Fourcc, + ) -> Result::Error> { + match &framebuffer.0 { + MultiFramebufferInternal::Target(fb) => { + let target = self.target.as_mut().unwrap(); + target + .device + .renderer_mut() + .copy_framebuffer(fb, region, format) + .map(|mapping| MultiTextureMapping(TextureMappingInternal::Either(mapping))) + .map_err(Error::Target) + } + MultiFramebufferInternal::Render(fb) => { + // SAFETY: We know this is fine, because target can only be `None` (and thus this framebuffer be of variant `Render`), if R == T. + let fb = unsafe { + std::mem::transmute::< + &<::Renderer as RendererSuper>::Framebuffer<'_>, + &<::Renderer as RendererSuper>::Framebuffer<'_>, + >(fb) + }; + self.render + .renderer_mut() + .copy_framebuffer(fb, region, format) + .map(|mapping| MultiTextureMapping(TextureMappingInternal::Or(mapping))) + .map_err(Error::Render) + } + } + } + + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + fn copy_texture( + &mut self, + texture: &Self::TextureId, + region: Rectangle, + format: Fourcc, + ) -> Result { + let tex = texture + .get::(&self.render.renderer().context_id()) + .ok_or_else(|| Error::MismatchedDevice(*self.render.node()))?; + self.render + .renderer_mut() + .copy_texture(&tex, region, format) + .map(|mapping| MultiTextureMapping(TextureMappingInternal::Or(mapping))) + .map_err(Error::Render) + } + + fn can_read_texture(&mut self, texture: &Self::TextureId) -> Result { + let tex = texture + .get::(&self.render.renderer().context_id()) + .ok_or_else(|| Error::MismatchedDevice(*self.render.node()))?; + self.render + .renderer_mut() + .can_read_texture(&tex) + .map_err(Error::Render) + } + + #[instrument(level = "trace", parent = &self.span, skip(self, texture_mapping))] + #[profiling::function] + fn map_texture<'c>( + &mut self, + texture_mapping: &'c Self::TextureMapping, + ) -> Result<&'c [u8], ::Error> { + match texture_mapping { + MultiTextureMapping(TextureMappingInternal::Either(target_mapping)) => self + .target + .as_mut() + .unwrap() + .device + .renderer_mut() + .map_texture(target_mapping) + .map_err(Error::Target), + MultiTextureMapping(TextureMappingInternal::Or(render_mapping)) => self + .render + .renderer_mut() + .map_texture(render_mapping) + .map_err(Error::Render), + } + } +} + +impl<'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> BlitFrame> + for MultiFrame<'_, '_, 'frame, 'buffer, R, T> +where + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + BlitFrame<<::Renderer as RendererSuper>::Framebuffer<'buffer>>, + ::Renderer: Blit, + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[instrument(level = "trace", parent = &self.span, skip(self, to))] + #[profiling::function] + fn blit_to( + &mut self, + to: &mut MultiFramebuffer<'buffer, T>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + self.flush_frame()?; + if let Some(target) = self.target.as_mut() { + let MultiFramebufferInternal::Target(to_fb) = &mut to.0 else { + unreachable!() + }; + let sync = target + .device + .renderer_mut() + .blit(target.framebuffer, to_fb, src, dst, filter) + .map_err(Error::Target)?; + Ok(sync) + } else { + let MultiFramebufferInternal::Render(to_fb) = &mut to.0 else { + unreachable!() + }; + // SAFETY: We know this is fine, because target can only be `None` (and thus this framebuffer be of variant `Render`), if R == T. + let to_fb = unsafe { + std::mem::transmute::< + &mut <::Renderer as RendererSuper>::Framebuffer<'_>, + &mut <::Renderer as RendererSuper>::Framebuffer<'_>, + >(to_fb) + }; + self.frame + .as_mut() + .unwrap() + .blit_to(to_fb, src, dst, filter) + .map_err(Error::Render) + } + } + + #[instrument(level = "trace", parent = &self.span, skip(self, from))] + #[profiling::function] + fn blit_from( + &mut self, + from: &MultiFramebuffer<'buffer, T>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + self.flush_frame()?; + if let Some(target) = self.target.as_mut() { + let MultiFramebufferInternal::Target(from_fb) = &from.0 else { + unreachable!() + }; + let sync = target + .device + .renderer_mut() + .blit(from_fb, target.framebuffer, src, dst, filter) + .map_err(Error::Target)?; + Ok(sync) + } else { + let MultiFramebufferInternal::Render(from_fb) = &from.0 else { + unreachable!() + }; + // SAFETY: We know this is fine, because target can only be `None` (and thus this framebuffer be of variant `Render`), if R == T. + let from_fb = unsafe { + std::mem::transmute::< + &<::Renderer as RendererSuper>::Framebuffer<'_>, + &<::Renderer as RendererSuper>::Framebuffer<'_>, + >(from_fb) + }; + self.frame + .as_mut() + .unwrap() + .blit_from(from_fb, src, dst, filter) + .map_err(Error::Render) + } + } +} + +impl Blit for MultiRenderer<'_, '_, R, T> +where + ::Renderer: Blit, + ::Renderer: Blit, + // We need this because the Renderer-impl does and Blit requires Renderer + R: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: Bind + ExportMem + ImportDma + ImportMem, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Error: 'static, +{ + #[instrument(level = "trace", parent = &self.span, skip(self, from, to))] + #[profiling::function] + fn blit( + &mut self, + from: &MultiFramebuffer<'_, T>, + to: &mut MultiFramebuffer<'_, T>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result::Error> { + if let Some(target) = self.target.as_mut() { + let MultiFramebufferInternal::Target(from_fb) = &from.0 else { + unreachable!() + }; + let MultiFramebufferInternal::Target(to_fb) = &mut to.0 else { + unreachable!() + }; + target + .device + .renderer_mut() + .blit(from_fb, to_fb, src, dst, filter) + .map_err(Error::Target) + } else { + let MultiFramebufferInternal::Render(from_fb) = &from.0 else { + unreachable!() + }; + let MultiFramebufferInternal::Render(to_fb) = &mut to.0 else { + unreachable!() + }; + // SAFETY: We know this is fine, because target can only be `None` (and thus this framebuffer be of variant `Render`), if R == T. + let from_fb = unsafe { + std::mem::transmute::< + &<::Renderer as RendererSuper>::Framebuffer<'_>, + &<::Renderer as RendererSuper>::Framebuffer<'_>, + >(from_fb) + }; + // SAFETY: We know this is fine, because target can only be `None` (and thus this framebuffer be of variant `Render`), if R == T. + let to_fb = unsafe { + std::mem::transmute::< + &mut <::Renderer as RendererSuper>::Framebuffer<'_>, + &mut <::Renderer as RendererSuper>::Framebuffer<'_>, + >(to_fb) + }; + self.render + .renderer_mut() + .blit(from_fb, to_fb, src, dst, filter) + .map_err(Error::Render) + } + } +} + +impl<'a, 'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> + FrameContext<'a, 'frame, 'buffer, LocalMultiRenderer<'a, 'render, 'target, 'frame, 'buffer, R, T>> + for MultiFrame<'render, 'target, 'frame, 'buffer, R, T> +where + 'frame: 'a, + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer> + 'a, +{ + type Guard = LocalRendererGuard<'a, 'render, 'target, 'frame, 'buffer, R, T>; + + fn renderer(&'a mut self) -> Self::Guard { + let guard = self.frame.as_mut().unwrap().renderer(); + + let span = trace_span!( + parent: &self.span, + "renderer_local_multi", + ) + .entered(); + + LocalRendererGuard { + renderer: LocalMultiRenderer { + guard, + render: self.render, + target: self.target.as_mut().map(|data| &mut *data.device), + other_renderers: &mut *self.other_renderers, + span, + }, + } + } +} + +/// Guard type wrapping a [`LocalMultiRenderer`] borrowed from a [`MultiFrame`]. +pub struct LocalRendererGuard<'a, 'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> +where + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + renderer: LocalMultiRenderer<'a, 'render, 'target, 'frame, 'buffer, R, T>, +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> fmt::Debug + for LocalRendererGuard<'a, '_, '_, 'frame, 'buffer, R, T> +where + 'buffer: 'frame, + ::Renderer: 'frame, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LocalMultiGuard") + .field("renderer", &self.renderer) + .finish() + } +} + +impl<'a, 'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> + AsRef> + for LocalRendererGuard<'a, 'render, 'target, 'frame, 'buffer, R, T> +where + 'buffer: 'frame, + ::Renderer: 'frame, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn as_ref(&self) -> &LocalMultiRenderer<'a, 'render, 'target, 'frame, 'buffer, R, T> { + &self.renderer + } +} + +impl<'a, 'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> + AsMut> + for LocalRendererGuard<'a, 'render, 'target, 'frame, 'buffer, R, T> +where + 'buffer: 'frame, + ::Renderer: 'frame, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn as_mut(&mut self) -> &mut LocalMultiRenderer<'a, 'render, 'target, 'frame, 'buffer, R, T> { + &mut self.renderer + } +} + +/// Variant of a [`MultiRenderer`], that doesn't render to framebuffer of the target-node, +/// but to framebuffers of the render-node for offscreen rendering via the use of the [`FrameContext`] trait +/// on a [`MultiFrame`]. +pub struct LocalMultiRenderer<'a, 'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> +where + 'buffer: 'frame, + ::Renderer: 'frame, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + guard: <<::Renderer as RendererSuper>::Frame<'frame, 'buffer> as FrameContext< + 'a, + 'frame, + 'buffer, + ::Renderer, + >>::Guard, + render: *mut &'render mut R::Device, + target: Option<&'a mut &'target mut T::Device>, + other_renderers: &'a mut [&'render mut R::Device], + span: tracing::span::EnteredSpan, +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> fmt::Debug + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LocalMultiRenderer") + .field("guard", &self.guard) + .field("render", &self.render) + .field("target", &self.target) + .field("other_renderers", &self.other_renderers) + .finish() + } +} + +impl<'a, 'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> RendererSuper + for LocalMultiRenderer<'a, 'render, 'target, 'frame, 'buffer, R, T> +where + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + type Error = Error; + type TextureId = MultiTexture; + type Framebuffer<'newbuffer> = MultiFramebuffer<'newbuffer, R>; + type Frame<'newframe, 'newbuffer> + = MultiFrame<'render, 'target, 'newframe, 'newbuffer, R, T> + where + 'newbuffer: 'newframe, + Self: 'newframe; +} + +impl<'a, 'render, 'target, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> Renderer + for LocalMultiRenderer<'a, 'render, 'target, 'frame, 'buffer, R, T> +where + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn context_id(&self) -> ContextId { + self.guard.as_ref().context_id().map() + } + + fn downscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.guard + .as_mut() + .downscale_filter(filter) + .map_err(Error::Render) + } + + fn upscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.guard.as_mut().upscale_filter(filter).map_err(Error::Render) + } + + fn set_debug_flags(&mut self, flags: DebugFlags) { + self.guard.as_mut().set_debug_flags(flags) + } + + fn debug_flags(&self) -> DebugFlags { + self.guard.as_ref().debug_flags() + } + + fn render<'newframe, 'newbuffer>( + &'newframe mut self, + framebuffer: &'newframe mut Self::Framebuffer<'newbuffer>, + size: Size, + dst_transform: Transform, + ) -> Result, Self::Error> + where + 'newbuffer: 'newframe, + { + let node = unsafe { *(*self.render).node() }; + let (MultiFramebufferInternal::Render(fb) | MultiFramebufferInternal::Target(fb)) = + &mut framebuffer.0; + let frame = self + .guard + .as_mut() + .render(fb, size, dst_transform) + .map_err(Error::Render)?; + + let span = trace_span!( + parent: &self.span, + "renderer_local_multi_frame", + ) + .entered(); + + Ok(MultiFrame { + node, + frame: Some(frame), + framebuffer: None, + target: None, + render: self.render, + other_renderers: &mut *self.other_renderers, + + dst_transform, + size, + damage: Vec::new(), + span, + }) + } + + fn wait(&mut self, sync: &sync::SyncPoint) -> Result<(), Self::Error> { + self.guard.as_mut().wait(sync).map_err(Error::Render) + } + + fn cleanup_texture_cache(&mut self) -> Result<(), Self::Error> { + self.guard.as_mut().cleanup_texture_cache().map_err(Error::Render) + } + + fn invalidate_caches(&mut self) -> Result<(), Self::Error> { + self.guard.as_mut().invalidate_caches().map_err(Error::Render) + } +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi, Target> Offscreen + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: Offscreen, + // TODO comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn create_buffer( + &mut self, + format: Fourcc, + size: Size, + ) -> Result { + self.guard + .as_mut() + .create_buffer(format, size) + .map_err(Error::Render) + } +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi, Target> Bind + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: Bind, + // TODO: comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn bind<'b>(&mut self, target: &'b mut Target) -> Result, Self::Error> { + self.guard + .as_mut() + .bind(target) + .map(MultiFramebufferInternal::Render) + .map(MultiFramebuffer) + .map_err(Error::Render) + } + + fn supported_formats(&self) -> Option { + self.guard.as_ref().supported_formats() + } +} + +#[cfg(feature = "wayland_frontend")] +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> ImportMemWl + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: ImportMemWl, + // TODO: comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn import_shm_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result { + let shm_texture = self + .guard + .as_mut() + .import_shm_buffer(buffer, surface, damage) + .map_err(Error::Render)?; + let (dimensions, format) = shm::with_buffer_contents(buffer, |_, _, data| { + Ok(( + (data.width, data.height).into(), + shm::shm_format_to_fourcc(data.format) + .map(|code| Format { + code, + modifier: Modifier::Linear, + }) + .ok_or(Error::ImportFailed)?, + )) + }) + .map_err(|_| Error::ImportFailed)??; + let mut texture = MultiTexture::from_surface(surface, dimensions, format); + texture.insert_texture::(&self.guard.as_ref().context_id(), shm_texture); + Ok(texture) + } + + fn shm_formats(&self) -> Box> { + ImportMemWl::shm_formats(self.guard.as_ref()) + } +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> ImportMem + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: ImportMem, + // TODO: comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn import_memory( + &mut self, + data: &[u8], + format: Fourcc, + size: Size, + flipped: bool, + ) -> Result<::TextureId, ::Error> { + let mem_texture = self + .guard + .as_mut() + .import_memory(data, format, size, flipped) + .map_err(Error::Render)?; + let mut texture = MultiTexture::new( + size, + Format { + code: format, + modifier: Modifier::Linear, + }, + ); + texture.insert_texture::(&self.guard.as_ref().context_id(), mem_texture); + Ok(texture) + } + + fn update_memory( + &mut self, + texture: &::TextureId, + data: &[u8], + region: Rectangle, + ) -> Result<(), ::Error> { + let mem_texture = texture + .get::(&self.guard.as_ref().context_id()) + .ok_or_else(|| Error::MismatchedDevice(*(unsafe { &*self.render }.node())))?; + self.guard + .as_mut() + .update_memory(&mem_texture, data, region) + .map_err(Error::Render) + } + + fn mem_formats(&self) -> Box> { + ImportMem::mem_formats(self.guard.as_ref()) + } +} + +#[cfg(feature = "wayland_frontend")] +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> ImportDmaWl + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: Bind, + ::Renderer: ExportMem + Bind, + // TODO: comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn import_dma_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&crate::wayland::compositor::SurfaceData>, + damage: &[Rectangle], + ) -> Result { + let dmabuf = get_dmabuf(buffer).expect("import_dma_buffer without checking buffer type?"); + let texture = MultiTexture::from_surface(surface, dmabuf.size(), dmabuf.format()); + let texture_ref = texture.0.clone(); + let res = import_dmabuf_internal::( + unsafe { *self.render }, + self.target.as_mut().map(|x| &mut ***x), + &mut *self.other_renderers, + dmabuf, + texture, + Some(damage), + ); + if res.is_ok() { + if let Some(surface) = surface { + surface.data_map.insert_if_missing_threadsafe(|| texture_ref); + } + } + res + } +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> ImportDma + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: Bind, + ::Renderer: ExportMem + Bind, + // TODO: comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn import_dmabuf( + &mut self, + dmabuf: &Dmabuf, + damage: Option<&[Rectangle]>, + ) -> Result { + let texture = MultiTexture::new(dmabuf.size(), dmabuf.format()); + import_dmabuf_internal( + unsafe { *self.render }, + self.target.as_mut().map(|target| &mut ***target), + &mut *self.other_renderers, + dmabuf, + texture, + damage, + ) + } + + fn dmabuf_formats(&self) -> FormatSet { + ImportDma::dmabuf_formats(self.guard.as_ref()) + } + + fn has_dmabuf_format(&self, format: Format) -> bool { + ImportDma::has_dmabuf_format(self.guard.as_ref(), format) + } +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> ExportMem + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: ExportMem, + // TODO: comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + type TextureMapping = <::Renderer as ExportMem>::TextureMapping; + + fn copy_framebuffer( + &mut self, + target: &Self::Framebuffer<'_>, + region: Rectangle, + format: Fourcc, + ) -> Result { + // because our `Framebuffer`-type is based on `R`, we cannot get a FramebufferInternal::Target variant, that isn't based on R. + // in other words the local multirenderer doesn't let you export a non-local framebuffer created from the non-local multirenderer + // through type bounds anyway. + let (MultiFramebufferInternal::Render(fb) | MultiFramebufferInternal::Target(fb)) = &target.0; + + self.guard + .as_mut() + .copy_framebuffer(fb, region, format) + .map_err(Error::Render) + } + + fn copy_texture( + &mut self, + texture: &Self::TextureId, + region: Rectangle, + format: Fourcc, + ) -> Result { + let tex = texture + .get::(&self.guard.as_ref().context_id()) + .ok_or_else(|| Error::MismatchedDevice(*unsafe { &*self.render }.node()))?; + self.guard + .as_mut() + .copy_texture(&tex, region, format) + .map_err(Error::Render) + } + + fn can_read_texture(&mut self, texture: &Self::TextureId) -> Result { + let tex = texture + .get::(&self.guard.as_ref().context_id()) + .ok_or_else(|| Error::MismatchedDevice(*unsafe { &*self.render }.node()))?; + self.guard.as_mut().can_read_texture(&tex).map_err(Error::Render) + } + + fn map_texture<'b>( + &mut self, + texture_mapping: &'b Self::TextureMapping, + ) -> Result<&'b [u8], Self::Error> { + self.guard + .as_mut() + .map_texture(texture_mapping) + .map_err(Error::Render) + } +} + +impl<'a, 'frame, 'buffer, R: GraphicsApi, T: GraphicsApi> Blit + for LocalMultiRenderer<'a, '_, '_, 'frame, 'buffer, R, T> +where + ::Renderer: Blit, + // TODO: comment + R: 'static, + T: 'static, + R::Error: 'static, + T::Error: 'static, + ::Renderer: ExportMem + ImportDma + ImportMem, + <::Renderer as RendererSuper>::TextureId: Clone + Send, + <::Renderer as RendererSuper>::Error: 'static, + ::Renderer: ImportDma + ImportMem, + <::Renderer as RendererSuper>::Error: 'static, + <::Renderer as RendererSuper>::Frame<'frame, 'buffer>: + FrameContext<'a, 'frame, 'buffer, ::Renderer>, +{ + fn blit( + &mut self, + from: &Self::Framebuffer<'_>, + to: &mut Self::Framebuffer<'_>, + src: Rectangle, + dst: Rectangle, + filter: TextureFilter, + ) -> Result { + // because our `Framebuffer`-type is based on `R`, we cannot get a FramebufferInternal::Target variant, that isn't based on R. + // in other words the local multirenderer doesn't let you export a non-local framebuffer created from the non-local multirenderer + // through type bounds anyway. + let (MultiFramebufferInternal::Render(from_fb) | MultiFramebufferInternal::Target(from_fb)) = &from.0; + let (MultiFramebufferInternal::Render(to_fb) | MultiFramebufferInternal::Target(to_fb)) = &mut to.0; + + self.guard + .as_mut() + .blit(from_fb, to_fb, src, dst, filter) + .map_err(Error::Render) + } +} diff --git a/vendor/smithay/src/backend/renderer/pixman/error.rs b/vendor/smithay/src/backend/renderer/pixman/error.rs new file mode 100644 index 0000000..b62c0ad --- /dev/null +++ b/vendor/smithay/src/backend/renderer/pixman/error.rs @@ -0,0 +1,74 @@ +use drm_fourcc::{DrmFourcc, DrmModifier}; +use thiserror::Error; + +use crate::backend::{ + SwapBuffersError, + allocator::dmabuf::{DmabufMappingFailed, DmabufSyncFailed}, +}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_shm; + +/// Error returned during rendering using pixman +#[derive(Debug, Error)] +pub enum PixmanError { + /// The given buffer has an unsupported number of planes + #[error("Unsupported number of planes")] + UnsupportedNumberOfPlanes, + /// The given buffer has an unsupported pixel format + #[error("Unsupported pixel format: {0:?}")] + UnsupportedPixelFormat(DrmFourcc), + /// The given buffer has an unsupported modifier + #[error("Unsupported modifier: {0:?}")] + UnsupportedModifier(DrmModifier), + /// The given wl buffer has an unsupported pixel format + #[error("Unsupported wl_shm format: {0:?}")] + #[cfg(feature = "wayland_frontend")] + UnsupportedWlPixelFormat(wl_shm::Format), + /// The given buffer is incomplete + #[error("Incomplete buffer {expected} < {actual}")] + IncompleteBuffer { + /// Expected len of the buffer + expected: usize, + /// Actual len of the buffer + actual: usize, + }, + /// The given buffer was not accessible + #[error("Error accessing the buffer ({0:?})")] + #[cfg(feature = "wayland_frontend")] + BufferAccessError(#[from] crate::wayland::shm::BufferAccessError), + /// Failed to import the given buffer + #[error("Import failed")] + ImportFailed, + /// The underlying buffer has been destroyed + #[error("The underlying buffer has been destroyed")] + BufferDestroyed, + /// Mapping the buffer failed + #[error("Mapping the buffer failed: {0}")] + Map(#[from] DmabufMappingFailed), + /// Synchronizing access to the buffer failed + #[error("Synchronizing buffer failed: {0}")] + Sync(#[from] DmabufSyncFailed), + /// The requested operation failed + #[error("The requested operation failed")] + Failed(#[from] pixman::OperationFailed), + /// No target is currently bound + #[error("No target is currently bound")] + NoTargetBound, + /// The requested operation is not supported + #[error("The requested operation is not supported")] + Unsupported, + /// Blocking for a synchronization primitive failed + #[error("Blocking for a synchronization primitive got interrupted")] + SyncInterrupted, +} + +impl From for SwapBuffersError { + #[inline] + fn from(value: PixmanError) -> Self { + match value { + x @ PixmanError::SyncInterrupted => SwapBuffersError::TemporaryFailure(Box::new(x)), + x => SwapBuffersError::ContextLost(Box::new(x)), + } + } +} diff --git a/vendor/smithay/src/backend/renderer/pixman/mod.rs b/vendor/smithay/src/backend/renderer/pixman/mod.rs new file mode 100644 index 0000000..14310cd --- /dev/null +++ b/vendor/smithay/src/backend/renderer/pixman/mod.rs @@ -0,0 +1,1406 @@ +//! Implementation of the rendering traits using pixman + +use std::sync::{ + Arc, LazyLock, Mutex, + atomic::{AtomicBool, Ordering}, +}; + +use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier}; +use pixman::{Filter, FormatCode, Image, Operation, Repeat}; +use tracing::warn; + +use crate::{ + backend::{ + allocator::{ + Buffer, + dmabuf::{ + Dmabuf, DmabufMapping, DmabufMappingMode, DmabufSyncFailed, DmabufSyncFlags, WeakDmabuf, + }, + format::{FormatSet, has_alpha}, + }, + renderer::FrameContext, + }, + utils::{Buffer as BufferCoords, Physical, Rectangle, Scale, Size, Transform}, +}; + +#[cfg(feature = "wayland_frontend")] +use crate::{ + backend::renderer::{ImportDmaWl, ImportMemWl}, + wayland::{compositor::SurfaceData, shm}, +}; +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_buffer; + +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +use super::ImportEgl; +use super::{ + Bind, Color32F, ContextId, DebugFlags, ExportMem, Frame, ImportDma, ImportMem, Offscreen, Renderer, + RendererSuper, Texture, TextureFilter, TextureMapping, sync::SyncPoint, +}; + +mod error; + +pub use error::*; + +const SUPPORTED_FORMATS: &[DrmFourcc] = &[ + #[cfg(target_endian = "little")] + DrmFourcc::Rgb565, + DrmFourcc::Xrgb8888, + DrmFourcc::Argb8888, + DrmFourcc::Xbgr8888, + DrmFourcc::Abgr8888, + DrmFourcc::Rgbx8888, + DrmFourcc::Rgba8888, + DrmFourcc::Bgrx8888, + DrmFourcc::Bgra8888, + #[cfg(target_endian = "little")] + DrmFourcc::Xrgb2101010, + #[cfg(target_endian = "little")] + DrmFourcc::Argb2101010, + #[cfg(target_endian = "little")] + DrmFourcc::Xbgr2101010, + #[cfg(target_endian = "little")] + DrmFourcc::Abgr2101010, +]; + +/// A framebuffer of an [`PixmanRenderer`]. +#[derive(Debug)] +pub struct PixmanTarget<'a>(PixmanTargetInternal<'a>); +#[derive(Debug)] +enum PixmanTargetInternal<'a> { + Dmabuf { dmabuf: &'a Dmabuf, image: PixmanImage }, + Image(&'a mut pixman::Image<'static, 'static>), +} + +impl Texture for PixmanTarget<'_> { + fn width(&self) -> u32 { + match &self.0 { + PixmanTargetInternal::Dmabuf { dmabuf, .. } => dmabuf.width(), + PixmanTargetInternal::Image(image) => image.width() as u32, + } + } + + fn height(&self) -> u32 { + match &self.0 { + PixmanTargetInternal::Dmabuf { dmabuf, .. } => dmabuf.height(), + PixmanTargetInternal::Image(image) => image.height() as u32, + } + } + + fn format(&self) -> Option { + match &self.0 { + PixmanTargetInternal::Dmabuf { dmabuf, .. } => Some(dmabuf.format().code), + PixmanTargetInternal::Image(image) => DrmFourcc::try_from(image.format()).ok(), + } + } + + fn size(&self) -> Size { + match &self.0 { + PixmanTargetInternal::Dmabuf { dmabuf, .. } => dmabuf.size(), + PixmanTargetInternal::Image(image) => Size::from((image.width() as i32, image.height() as i32)), + } + } +} + +#[derive(Debug)] +struct PixmanDmabufMapping { + dmabuf: WeakDmabuf, + _mapping: DmabufMapping, +} + +#[derive(Debug)] +struct PixmanImageInner { + #[cfg(feature = "wayland_frontend")] + buffer: Option, + dmabuf: Option, + image: Mutex>, + _flipped: bool, /* TODO: What about flipped textures? */ +} + +#[derive(Debug, Clone)] +struct PixmanImage(Arc); + +impl PixmanImage { + #[profiling::function] + fn accessor<'l>(&'l self) -> Result, PixmanError> { + let guard = if let Some(mapping) = self.0.dmabuf.as_ref() { + let dmabuf = mapping.dmabuf.upgrade().ok_or(PixmanError::BufferDestroyed)?; + Some(DmabufReadGuard::new(dmabuf)?) + } else { + None + }; + + Ok(TextureAccessor { + #[cfg(feature = "wayland_frontend")] + buffer: self.0.buffer.clone(), + image: &self.0.image, + _guard: guard, + }) + } +} + +/// A handle to a pixman texture +#[derive(Debug, Clone)] +pub struct PixmanTexture(PixmanImage); + +impl From> for PixmanTexture { + #[inline] + fn from(image: pixman::Image<'static, 'static>) -> Self { + Self(PixmanImage(Arc::new(PixmanImageInner { + #[cfg(feature = "wayland_frontend")] + buffer: None, + dmabuf: None, + _flipped: false, + image: Mutex::new(image), + }))) + } +} + +struct DmabufReadGuard { + dmabuf: Dmabuf, +} + +impl DmabufReadGuard { + #[profiling::function] + pub fn new(dmabuf: Dmabuf) -> Result { + dmabuf.sync_plane(0, DmabufSyncFlags::START | DmabufSyncFlags::READ)?; + Ok(Self { dmabuf }) + } +} + +impl Drop for DmabufReadGuard { + #[profiling::function] + fn drop(&mut self) { + if let Err(err) = self + .dmabuf + .sync_plane(0, DmabufSyncFlags::END | DmabufSyncFlags::READ) + { + tracing::warn!(?err, "failed to end sync read"); + } + } +} + +struct TextureAccessor<'l> { + #[cfg(feature = "wayland_frontend")] + buffer: Option, + image: &'l Mutex>, + _guard: Option, +} + +impl TextureAccessor<'_> { + fn with_image(&self, f: F) -> Result + where + F: for<'a> FnOnce(&'a mut Image<'static, 'static>) -> R, + { + let mut image = self.image.lock().unwrap(); + + #[cfg(feature = "wayland_frontend")] + if let Some(buffer) = self.buffer.as_ref() { + // We only have a buffer in case the image was created from + // a shm buffer. In this case we need to guard against SIGPIPE + // when accessing the image + return shm::with_buffer_contents(buffer, move |ptr, len, data| { + if unsafe { ptr.offset(data.offset as isize) as *mut u32 } != unsafe { image.data() } { + // Our stored data ptr changed, this is most likely the result of a shm pool resize. + // In this case we need to re-map the image + let expected_len = (data.offset + data.stride * data.height) as usize; + if len < expected_len { + return Err(PixmanError::IncompleteBuffer { + expected: expected_len, + actual: len, + }); + } + + let remapped_image = unsafe { + // SAFETY: We guarantee that this image is only used for reading, + // so it is safe to cast the ptr to *mut + Image::from_raw_mut( + image.format(), + data.width as usize, + data.height as usize, + ptr.offset(data.offset as isize) as *mut u32, + data.stride as usize, + false, + ) + } + .map_err(|_| PixmanError::ImportFailed)?; + + *image = remapped_image; + + let res = f(&mut image); + Ok(res) + } else { + Ok(f(&mut image)) + } + })?; + } + + Ok(f(&mut image)) + } +} + +impl PixmanTexture { + #[profiling::function] + fn accessor<'l>(&'l self) -> Result, PixmanError> { + self.0.accessor() + } +} + +impl Texture for PixmanTexture { + fn width(&self) -> u32 { + self.0.0.image.lock().unwrap().width() as u32 + } + + fn height(&self) -> u32 { + self.0.0.image.lock().unwrap().height() as u32 + } + + fn size(&self) -> Size { + let lock = self.0.0.image.lock().unwrap(); + Size::from((lock.width() as i32, lock.height() as i32)) + } + + fn format(&self) -> Option { + DrmFourcc::try_from(self.0.0.image.lock().unwrap().format()).ok() + } +} + +/// Handle to the currently rendered frame during [`PixmanRenderer::render`](Renderer::render). +#[derive(Debug)] +pub struct PixmanFrame<'frame, 'buffer> { + renderer: &'frame mut PixmanRenderer, + target: &'frame mut PixmanTarget<'buffer>, + + transform: Transform, + output_size: Size, + size: Size, + + finished: AtomicBool, +} + +impl PixmanFrame<'_, '_> { + fn draw_solid_color( + &mut self, + dst: Rectangle, + damage: &[Rectangle], + color: Color32F, + op: Operation, + debug: DebugFlags, + ) -> Result<(), PixmanError> { + let mut binding; + let target_image = match &mut self.target.0 { + PixmanTargetInternal::Dmabuf { image, .. } => { + binding = image.0.image.lock().unwrap(); + &mut *binding + } + PixmanTargetInternal::Image(b) => b, + }; + + let solid = pixman::Solid::new(color.components()).map_err(|_| PixmanError::Unsupported)?; + + let mut clip_region = + pixman::Region32::init_rect(0, 0, self.output_size.w as u32, self.output_size.h as u32); + + let damage_boxes = damage + .iter() + .copied() + .map(|mut rect| { + rect.loc += dst.loc; + + let rect = self.transform.transform_rect_in(rect, &self.size); + + let p1 = rect.loc; + let p2 = p1 + rect.size.to_point(); + pixman::Box32 { + x1: p1.x, + y1: p1.y, + x2: p2.x, + y2: p2.y, + } + }) + .collect::>(); + let damage_region = pixman::Region32::init_rects(&damage_boxes); + clip_region = clip_region.intersect(&damage_region); + + target_image.set_clip_region32(Some(&clip_region))?; + + target_image.composite32( + op, + &solid, + None, + (0, 0), + (0, 0), + (0, 0), + (target_image.width() as i32, target_image.height() as i32), + ); + + if debug.contains(DebugFlags::TINT) { + target_image.composite32( + Operation::Over, + &self.renderer.tint, + None, + (0, 0), + (0, 0), + (0, 0), + (target_image.width() as i32, target_image.height() as i32), + ); + } + + target_image.set_clip_region32(None)?; + + Ok(()) + } +} + +impl Frame for PixmanFrame<'_, '_> { + type Error = PixmanError; + + type TextureId = PixmanTexture; + + fn context_id(&self) -> ContextId { + self.renderer.context_id() + } + + #[profiling::function] + fn clear(&mut self, color: Color32F, at: &[Rectangle]) -> Result<(), Self::Error> { + self.draw_solid_color( + Rectangle::from_size(self.size), + at, + color, + Operation::Src, + DebugFlags::empty(), + ) + } + + #[profiling::function] + fn draw_solid( + &mut self, + dst: Rectangle, + damage: &[Rectangle], + color: Color32F, + ) -> Result<(), Self::Error> { + let op = if color.is_opaque() { + Operation::Src + } else { + Operation::Over + }; + self.draw_solid_color(dst, damage, color, op, self.renderer.debug_flags) + } + + #[profiling::function] + fn render_texture_from_to( + &mut self, + texture: &Self::TextureId, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque_regions: &[Rectangle], + src_transform: Transform, + alpha: f32, + ) -> Result<(), Self::Error> { + let mut binding; + let target_image = match &mut self.target.0 { + PixmanTargetInternal::Dmabuf { image, .. } => { + binding = image.0.image.lock().unwrap(); + &mut *binding + } + PixmanTargetInternal::Image(b) => b, + }; + let src_image_accessor = texture.accessor()?; + + let dst_loc = dst.loc; + let dst = self.transform.transform_rect_in(dst, &self.size); + + // Our renderer works with clock-wise rotation, but the scr_transform in contrast to + // the output transform is specified counter-clock-wise. + let src_transform = src_transform.invert(); + + let src: Rectangle = src.to_i32_up::(); + + let image_transform = match (src_transform, self.transform) { + (Transform::Normal, output_transform) => output_transform, + + (Transform::_90, Transform::Normal) => Transform::_270, + (Transform::_90, Transform::_90) => Transform::Normal, + (Transform::_90, Transform::_180) => Transform::_90, + (Transform::_90, Transform::_270) => Transform::_180, + (Transform::_90, Transform::Flipped) => Transform::Flipped90, + (Transform::_90, Transform::Flipped90) => Transform::Flipped180, + (Transform::_90, Transform::Flipped180) => Transform::Flipped270, + (Transform::_90, Transform::Flipped270) => Transform::Flipped, + + (Transform::_180, Transform::Normal) => Transform::_180, + (Transform::_180, Transform::_90) => Transform::_270, + (Transform::_180, Transform::_180) => Transform::Normal, + (Transform::_180, Transform::_270) => Transform::_90, + (Transform::_180, Transform::Flipped) => Transform::Flipped180, + (Transform::_180, Transform::Flipped90) => Transform::Flipped270, + (Transform::_180, Transform::Flipped180) => Transform::Flipped, + (Transform::_180, Transform::Flipped270) => Transform::Flipped90, + + (Transform::_270, Transform::Normal) => Transform::_90, + (Transform::_270, Transform::_90) => Transform::_180, + (Transform::_270, Transform::_180) => Transform::_270, + (Transform::_270, Transform::_270) => Transform::Normal, + (Transform::_270, Transform::Flipped) => Transform::Flipped270, + (Transform::_270, Transform::Flipped90) => Transform::Flipped, + (Transform::_270, Transform::Flipped180) => Transform::Flipped90, + (Transform::_270, Transform::Flipped270) => Transform::Flipped180, + + (Transform::Flipped, Transform::Normal) => Transform::Flipped, + (Transform::Flipped, Transform::_90) => Transform::Flipped90, + (Transform::Flipped, Transform::_180) => Transform::Flipped180, + (Transform::Flipped, Transform::_270) => Transform::Flipped270, + (Transform::Flipped, Transform::Flipped) => Transform::Normal, + (Transform::Flipped, Transform::Flipped90) => Transform::_90, + (Transform::Flipped, Transform::Flipped180) => Transform::_180, + (Transform::Flipped, Transform::Flipped270) => Transform::_270, + + (Transform::Flipped90, Transform::Normal) => Transform::Flipped90, + (Transform::Flipped90, Transform::_90) => Transform::Flipped180, + (Transform::Flipped90, Transform::_180) => Transform::Flipped270, + (Transform::Flipped90, Transform::_270) => Transform::Flipped, + (Transform::Flipped90, Transform::Flipped) => Transform::_270, + (Transform::Flipped90, Transform::Flipped90) => Transform::Normal, + (Transform::Flipped90, Transform::Flipped180) => Transform::_90, + (Transform::Flipped90, Transform::Flipped270) => Transform::_180, + + (Transform::Flipped180, Transform::Normal) => Transform::Flipped180, + (Transform::Flipped180, Transform::_90) => Transform::Flipped270, + (Transform::Flipped180, Transform::_180) => Transform::Flipped, + (Transform::Flipped180, Transform::_270) => Transform::Flipped90, + (Transform::Flipped180, Transform::Flipped) => Transform::_180, + (Transform::Flipped180, Transform::Flipped90) => Transform::_270, + (Transform::Flipped180, Transform::Flipped180) => Transform::Normal, + (Transform::Flipped180, Transform::Flipped270) => Transform::_90, + + (Transform::Flipped270, Transform::Normal) => Transform::Flipped270, + (Transform::Flipped270, Transform::_90) => Transform::Flipped, + (Transform::Flipped270, Transform::_180) => Transform::Flipped90, + (Transform::Flipped270, Transform::_270) => Transform::Flipped180, + (Transform::Flipped270, Transform::Flipped) => Transform::_90, + (Transform::Flipped270, Transform::Flipped90) => Transform::_180, + (Transform::Flipped270, Transform::Flipped180) => Transform::_270, + (Transform::Flipped270, Transform::Flipped270) => Transform::Normal, + }; + + let dst_src_size = image_transform.transform_size(src.size); + let scale = dst_src_size.to_f64() / dst.size.to_f64(); + + let (src_x, src_y, dest_x, dest_y, width, height, transform) = + if image_transform != Transform::Normal || scale != Scale::from(1f64) { + let mut transform = pixman::Transform::identity(); + + // compensate for offset + transform = transform + .translate(-dst.loc.x, -dst.loc.y, false) + .ok_or(PixmanError::Unsupported)?; + + // scale to src image size + transform = transform + .scale(scale.x, scale.y, false) + .ok_or(PixmanError::Unsupported)?; + + let (cos, sin, x, y) = match image_transform { + Transform::Normal => (1, 0, 0, 0), + Transform::_90 => (0, -1, 0, src.size.h), + Transform::_180 => (-1, 0, src.size.w, src.size.h), + Transform::_270 => (0, 1, src.size.w, 0), + Transform::Flipped => (1, 0, src.size.w, 0), + Transform::Flipped90 => (0, -1, src.size.w, src.size.h), + Transform::Flipped180 => (-1, 0, 0, src.size.h), + Transform::Flipped270 => (0, 1, 0, 0), + }; + + // rotation + transform = transform + .rotate(cos, sin, false) + .ok_or(PixmanError::Unsupported)?; + + // flipped + if image_transform.flipped() { + transform = transform.scale(-1, 1, false).ok_or(PixmanError::Unsupported)?; + } + + // Compensate rotation and flipped + transform = transform.translate(x, y, false).ok_or(PixmanError::Unsupported)?; + + // crop src + transform = transform + .translate(src.loc.x, src.loc.y, false) + .ok_or(PixmanError::Unsupported)?; + + ( + 0, + 0, + 0, + 0, + target_image.width() as i32, + target_image.height() as i32, + Some(transform), + ) + } else { + ( + src.loc.x, src.loc.y, dst.loc.x, dst.loc.y, src.size.w, src.size.h, None, + ) + }; + + let mut clip_region = + pixman::Region32::init_rect(0, 0, self.output_size.w as u32, self.output_size.h as u32) + .intersect(&pixman::Region32::init_rect( + dst.loc.x, + dst.loc.y, + dst.size.w as u32, + dst.size.h as u32, + )); + + let damage_boxes = damage + .iter() + .copied() + .map(|mut rect| { + rect.loc += dst_loc; + + let rect = self.transform.transform_rect_in(rect, &self.size); + + let p1 = rect.loc; + let p2 = p1 + rect.size.to_point(); + pixman::Box32 { + x1: p1.x, + y1: p1.y, + x2: p2.x, + y2: p2.y, + } + }) + .collect::>(); + let damage_region = pixman::Region32::init_rects(&damage_boxes); + clip_region = clip_region.intersect(&damage_region); + + target_image.set_clip_region32(Some(&clip_region))?; + + src_image_accessor.with_image(|src_image| { + if let Some(transform) = transform { + src_image.set_transform(transform)?; + } else { + src_image.clear_transform()?; + } + + let filter = match self.renderer.upscale_filter { + TextureFilter::Linear => Filter::Bilinear, + TextureFilter::Nearest => Filter::Nearest, + }; + + src_image.set_filter(filter, &[])?; + src_image.set_repeat(Repeat::None); + + let has_alpha = DrmFourcc::try_from(src_image.format()) + .ok() + .map(has_alpha) + .unwrap_or(true); + + let op = if has_alpha { + Operation::Over + } else { + Operation::Src + }; + + let mask = if alpha != 1f32 { + Some(pixman::Solid::new([0f32, 0f32, 0f32, alpha]).map_err(|_| PixmanError::Unsupported)?) + } else { + None + }; + + target_image.composite32( + op, + src_image, + mask.as_deref(), + (src_x, src_y), + (0, 0), + (dest_x, dest_y), + (width, height), + ); + + src_image.clear_transform()?; + + Result::<(), PixmanError>::Ok(()) + })??; + + if self.renderer.debug_flags.contains(DebugFlags::TINT) { + target_image.composite32( + Operation::Over, + &self.renderer.tint, + None, + (0, 0), + (0, 0), + (0, 0), + (target_image.width() as i32, target_image.height() as i32), + ); + } + + target_image.set_clip_region32(None)?; + + Ok(()) + } + + fn transformation(&self) -> Transform { + self.transform + } + + fn output_size(&self) -> Size { + self.output_size + } + + fn wait(&mut self, sync: &SyncPoint) -> Result<(), Self::Error> { + sync.wait().map_err(|_| PixmanError::SyncInterrupted) + } + + #[profiling::function] + fn finish(mut self) -> Result { + self.finish_internal() + } +} + +impl PixmanFrame<'_, '_> { + #[profiling::function] + fn finish_internal(&mut self) -> Result { + if self.finished.swap(true, Ordering::SeqCst) { + return Ok(SyncPoint::signaled()); + } + + if let PixmanTargetInternal::Dmabuf { dmabuf, .. } = &self.target.0 { + dmabuf + .sync_plane( + 0, + DmabufSyncFlags::END | DmabufSyncFlags::READ | DmabufSyncFlags::WRITE, + ) + .map_err(PixmanError::Sync)?; + } + + Ok(SyncPoint::signaled()) + } +} + +impl Drop for PixmanFrame<'_, '_> { + fn drop(&mut self) { + match self.finish_internal() { + Ok(sync) => { + let _ = sync.wait(); + } + Err(err) => { + warn!("Ignored error finishing PixmanFrame on drop: {}", err); + } + } + } +} + +/// A renderer utilizing pixman +#[derive(Debug)] +pub struct PixmanRenderer { + downscale_filter: TextureFilter, + upscale_filter: TextureFilter, + debug_flags: DebugFlags, + tint: pixman::Solid<'static>, + + // caches + buffers: Vec, + dmabuf_cache: Vec, +} + +impl PixmanRenderer { + /// Creates a new pixman renderer + pub fn new() -> Result { + let tint = pixman::Solid::new([0.0, 0.2, 0.0, 0.2]).map_err(|_| PixmanError::Unsupported)?; + Ok(Self { + downscale_filter: TextureFilter::Linear, + upscale_filter: TextureFilter::Linear, + debug_flags: DebugFlags::empty(), + tint, + + buffers: Default::default(), + dmabuf_cache: Default::default(), + }) + } +} + +impl PixmanRenderer { + fn existing_dmabuf(&self, dmabuf: &Dmabuf) -> Option { + self.dmabuf_cache + .iter() + .find(|image| { + image + .0 + .dmabuf + .as_ref() + .and_then(|map| map.dmabuf.upgrade().map(|buf| &buf == dmabuf)) + .unwrap_or(false) + }) + .cloned() + } + + fn import_dmabuf( + &mut self, + dmabuf: &Dmabuf, + mode: DmabufMappingMode, + ) -> Result { + if dmabuf.num_planes() != 1 { + return Err(PixmanError::UnsupportedNumberOfPlanes); + } + + let size = dmabuf.size(); + let format = dmabuf.format(); + + if format.modifier != DrmModifier::Linear { + return Err(PixmanError::UnsupportedModifier(format.modifier)); + } + let format = pixman::FormatCode::try_from(format.code) + .map_err(|_| PixmanError::UnsupportedPixelFormat(format.code))?; + + let dmabuf_mapping = dmabuf.map_plane(0, mode)?; + let stride = dmabuf.strides().next().expect("already checked") as usize; + let expected_len = stride * size.h as usize; + + if dmabuf_mapping.length() < expected_len { + return Err(PixmanError::IncompleteBuffer { + expected: expected_len, + actual: dmabuf_mapping.length(), + }); + } + + dmabuf.sync_plane(0, DmabufSyncFlags::START | DmabufSyncFlags::READ)?; + dmabuf.sync_plane(0, DmabufSyncFlags::END | DmabufSyncFlags::READ)?; + + let image: Image<'_, '_> = unsafe { + pixman::Image::from_raw_mut( + format, + size.w as usize, + size.h as usize, + dmabuf_mapping.ptr() as *mut u32, + stride, + false, + ) + } + .map_err(|_| PixmanError::ImportFailed)?; + + Ok(PixmanImage(Arc::new(PixmanImageInner { + #[cfg(feature = "wayland_frontend")] + buffer: None, + dmabuf: Some(PixmanDmabufMapping { + dmabuf: dmabuf.weak(), + _mapping: dmabuf_mapping, + }), + image: Mutex::new(image), + _flipped: false, + }))) + } + + fn cleanup(&mut self) { + self.dmabuf_cache.retain(|image| { + image + .0 + .dmabuf + .as_ref() + .map(|map| !map.dmabuf.is_gone()) + .unwrap_or(false) + }); + self.buffers.retain(|image| { + image + .0 + .dmabuf + .as_ref() + .map(|map| !map.dmabuf.is_gone()) + .unwrap_or(false) + }); + } +} + +impl RendererSuper for PixmanRenderer { + type Error = PixmanError; + type TextureId = PixmanTexture; + type Framebuffer<'buffer> = PixmanTarget<'buffer>; + type Frame<'frame, 'buffer> + = PixmanFrame<'frame, 'buffer> + where + 'buffer: 'frame; +} + +impl Renderer for PixmanRenderer { + fn context_id(&self) -> ContextId { + // Pixman textures are just memory slices, and there's nothing in the API + // that prevents sharing them between different `PixmanRenderer` instances. + // So they all share the same static `ContextId`. + static CONTEXT_ID: LazyLock> = LazyLock::new(ContextId::new); + CONTEXT_ID.clone() + } + + fn downscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.downscale_filter = filter; + Ok(()) + } + + fn upscale_filter(&mut self, filter: TextureFilter) -> Result<(), Self::Error> { + self.upscale_filter = filter; + Ok(()) + } + + fn set_debug_flags(&mut self, flags: DebugFlags) { + self.debug_flags = flags; + } + + fn debug_flags(&self) -> DebugFlags { + self.debug_flags + } + + #[profiling::function] + fn render<'frame, 'buffer>( + &'frame mut self, + target: &'frame mut PixmanTarget<'buffer>, + output_size: Size, + dst_transform: Transform, + ) -> Result, Self::Error> + where + 'buffer: 'frame, + { + self.cleanup(); + + if let PixmanTargetInternal::Dmabuf { dmabuf, .. } = &target.0 { + dmabuf + .sync_plane( + 0, + DmabufSyncFlags::START | DmabufSyncFlags::READ | DmabufSyncFlags::WRITE, + ) + .map_err(PixmanError::Sync)?; + } + Ok(PixmanFrame { + renderer: self, + target, + + transform: dst_transform, + output_size, + size: dst_transform.transform_size(output_size), + + finished: AtomicBool::new(false), + }) + } + fn wait(&mut self, sync: &SyncPoint) -> Result<(), Self::Error> { + sync.wait().map_err(|_| PixmanError::SyncInterrupted) + } + + fn cleanup_texture_cache(&mut self) -> Result<(), Self::Error> { + self.cleanup(); + Ok(()) + } + + fn invalidate_caches(&mut self) -> Result<(), Self::Error> { + self.dmabuf_cache.clear(); + self.buffers.clear(); + Ok(()) + } +} + +impl ImportMem for PixmanRenderer { + #[profiling::function] + fn import_memory( + &mut self, + data: &[u8], + format: drm_fourcc::DrmFourcc, + size: Size, + flipped: bool, + ) -> Result { + let format = + pixman::FormatCode::try_from(format).map_err(|_| PixmanError::UnsupportedPixelFormat(format))?; + let image = pixman::Image::new(format, size.w as usize, size.h as usize, false) + .map_err(|_| PixmanError::Unsupported)?; + let expected_len = image.stride() * image.height(); + if data.len() < expected_len { + return Err(PixmanError::IncompleteBuffer { + expected: expected_len, + actual: data.len(), + }); + } + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr(), image.data() as *mut u8, expected_len); + } + Ok(PixmanTexture(PixmanImage(Arc::new(PixmanImageInner { + #[cfg(feature = "wayland_frontend")] + buffer: None, + dmabuf: None, + image: Mutex::new(image), + _flipped: flipped, + })))) + } + + #[profiling::function] + fn update_memory( + &mut self, + texture: &Self::TextureId, + data: &[u8], + region: Rectangle, + ) -> Result<(), Self::Error> { + #[cfg(feature = "wayland_frontend")] + if texture.0.0.buffer.is_some() { + return Err(PixmanError::ImportFailed); + } + + if texture.0.0.dmabuf.is_some() { + return Err(PixmanError::ImportFailed); + } + + let mut image = texture.0.0.image.lock().unwrap(); + let stride = image.stride(); + let expected_len = stride * image.height(); + + if data.len() < expected_len { + return Err(PixmanError::IncompleteBuffer { + expected: expected_len, + actual: data.len(), + }); + } + + let src_image = unsafe { + // SAFETY: As we are never going to write to this image + // it is safe to cast the passed slice to a mut pointer + pixman::Image::from_raw_mut( + image.format(), + image.width(), + image.height(), + data.as_ptr() as *mut _, + stride, + false, + ) + } + .map_err(|_| PixmanError::ImportFailed)?; + + image.composite32( + Operation::Src, + &src_image, + None, + region.loc.into(), + (0, 0), + region.loc.into(), + region.size.into(), + ); + + Ok(()) + } + + fn mem_formats(&self) -> Box> { + Box::new(SUPPORTED_FORMATS.iter().copied()) + } +} + +/// Texture mapping of a pixman texture +#[derive(Debug)] +pub struct PixmanMapping(pixman::Image<'static, 'static>); + +impl Texture for PixmanMapping { + fn width(&self) -> u32 { + self.0.width() as u32 + } + + fn height(&self) -> u32 { + self.0.height() as u32 + } + + fn format(&self) -> Option { + DrmFourcc::try_from(self.0.format()).ok() + } +} + +impl TextureMapping for PixmanMapping { + fn flipped(&self) -> bool { + false + } +} + +impl ExportMem for PixmanRenderer { + type TextureMapping = PixmanMapping; + + #[profiling::function] + fn copy_framebuffer( + &mut self, + target: &PixmanTarget<'_>, + region: Rectangle, + format: DrmFourcc, + ) -> Result { + let format_code = + pixman::FormatCode::try_from(format).map_err(|_| PixmanError::UnsupportedPixelFormat(format))?; + let mut copy_image = + pixman::Image::new(format_code, region.size.w as usize, region.size.h as usize, false) + .map_err(|_| PixmanError::Unsupported)?; + + let binding; + let target_image = match &target.0 { + PixmanTargetInternal::Dmabuf { dmabuf, image } => { + dmabuf.sync_plane(0, DmabufSyncFlags::START | DmabufSyncFlags::READ)?; + binding = image.0.image.lock().unwrap(); + &*binding + } + PixmanTargetInternal::Image(b) => *b, + }; + + copy_image.composite32( + Operation::Src, + target_image, + None, + region.loc.into(), + (0, 0), + (0, 0), + region.size.into(), + ); + if let PixmanTargetInternal::Dmabuf { dmabuf, .. } = &target.0 { + dmabuf.sync_plane(0, DmabufSyncFlags::END | DmabufSyncFlags::READ)?; + }; + + Ok(PixmanMapping(copy_image)) + } + + #[profiling::function] + fn copy_texture( + &mut self, + texture: &Self::TextureId, + region: Rectangle, + format: DrmFourcc, + ) -> Result { + let accessor = texture.accessor()?; + let format_code = + pixman::FormatCode::try_from(format).map_err(|_| PixmanError::UnsupportedPixelFormat(format))?; + let mut copy_image = + pixman::Image::new(format_code, region.size.w as usize, region.size.h as usize, false) + .map_err(|_| PixmanError::Unsupported)?; + accessor.with_image(|image| { + copy_image.composite32( + Operation::Src, + image, + None, + region.loc.into(), + (0, 0), + (0, 0), + region.size.into(), + ); + })?; + Ok(PixmanMapping(copy_image)) + } + + fn can_read_texture(&mut self, _texture: &Self::TextureId) -> Result { + Ok(true) + } + + #[profiling::function] + fn map_texture<'a>( + &mut self, + texture_mapping: &'a Self::TextureMapping, + ) -> Result<&'a [u8], Self::Error> { + Ok(unsafe { + std::slice::from_raw_parts( + texture_mapping.0.data() as *const u8, + texture_mapping.0.stride() * texture_mapping.0.height(), + ) + }) + } +} + +#[cfg(all( + feature = "wayland_frontend", + feature = "backend_egl", + feature = "use_system_lib" +))] +impl ImportEgl for PixmanRenderer { + fn bind_wl_display( + &mut self, + _display: &wayland_server::DisplayHandle, + ) -> Result<(), crate::backend::egl::Error> { + Err(crate::backend::egl::Error::NoEGLDisplayBound) + } + + fn unbind_wl_display(&mut self) {} + + fn egl_reader(&self) -> Option<&crate::backend::egl::display::EGLBufferReader> { + None + } + + fn import_egl_buffer( + &mut self, + _buffer: &wl_buffer::WlBuffer, + _surface: Option<&SurfaceData>, + _damage: &[Rectangle], + ) -> Result { + Err(PixmanError::Unsupported) + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportMemWl for PixmanRenderer { + #[profiling::function] + fn import_shm_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + _surface: Option<&SurfaceData>, + _damage: &[Rectangle], + ) -> Result { + let image = shm::with_buffer_contents(buffer, |ptr, len, data| { + let format = FormatCode::try_from( + shm::shm_format_to_fourcc(data.format) + .ok_or(PixmanError::UnsupportedWlPixelFormat(data.format))?, + ) + .map_err(|_| PixmanError::UnsupportedWlPixelFormat(data.format))?; + + let expected_len = (data.offset + data.stride * data.height) as usize; + if len < expected_len { + return Err(PixmanError::IncompleteBuffer { + expected: expected_len, + actual: len, + }); + } + + let image = unsafe { + // SAFETY: We guarantee that this image is only used for reading, + // so it is safe to cast the ptr to *mut + Image::from_raw_mut( + format, + data.width as usize, + data.height as usize, + ptr.offset(data.offset as isize) as *mut u32, + data.stride as usize, + false, + ) + } + .map_err(|_| PixmanError::ImportFailed)?; + std::result::Result::<_, PixmanError>::Ok(image) + })??; + Ok(PixmanTexture(PixmanImage(Arc::new(PixmanImageInner { + buffer: Some(buffer.clone()), + dmabuf: None, + image: Mutex::new(image), + _flipped: false, + })))) + } +} + +impl ImportDma for PixmanRenderer { + #[profiling::function] + fn import_dmabuf( + &mut self, + dmabuf: &Dmabuf, + _damage: Option<&[Rectangle]>, + ) -> Result { + if let Some(image) = self.existing_dmabuf(dmabuf) { + return Ok(PixmanTexture(image)); + }; + + let image = self.import_dmabuf(dmabuf, DmabufMappingMode::READ)?; + self.dmabuf_cache.push(image.clone()); + Ok(PixmanTexture(image)) + } + + fn dmabuf_formats(&self) -> FormatSet { + static DMABUF_FORMATS: LazyLock = LazyLock::new(|| { + SUPPORTED_FORMATS + .iter() + .map(|code| DrmFormat { + code: *code, + modifier: DrmModifier::Linear, + }) + .collect() + }); + + DMABUF_FORMATS.clone() + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportDmaWl for PixmanRenderer {} + +impl Bind for PixmanRenderer { + #[profiling::function] + fn bind<'a>(&mut self, target: &'a mut Dmabuf) -> Result, Self::Error> { + let existing_image = self + .buffers + .iter() + .find(|image| { + image + .0 + .dmabuf + .as_ref() + .and_then(|map| map.dmabuf.upgrade().map(|buf| buf == *target)) + .unwrap_or(false) + }) + .cloned(); + + let image = if let Some(image) = existing_image { + image + } else { + let image = self.import_dmabuf(target, DmabufMappingMode::READ | DmabufMappingMode::WRITE)?; + self.buffers.push(image.clone()); + image + }; + + Ok(PixmanTarget(PixmanTargetInternal::Dmabuf { + dmabuf: target, + image, + })) + } + + fn supported_formats(&self) -> Option { + static DMABUF_FORMATS: LazyLock = LazyLock::new(|| { + SUPPORTED_FORMATS + .iter() + .map(|code| DrmFormat { + code: *code, + modifier: DrmModifier::Linear, + }) + .collect() + }); + + Some(DMABUF_FORMATS.clone()) + } +} + +impl Offscreen> for PixmanRenderer { + #[profiling::function] + fn create_buffer( + &mut self, + format: DrmFourcc, + size: Size, + ) -> Result, Self::Error> { + let format_code = + FormatCode::try_from(format).map_err(|_| PixmanError::UnsupportedPixelFormat(format))?; + let image = pixman::Image::new(format_code, size.w as usize, size.h as usize, true) + .map_err(|_| PixmanError::Unsupported)?; + Ok(image) + } +} + +impl Bind> for PixmanRenderer { + #[profiling::function] + fn bind<'a>(&mut self, target: &'a mut Image<'static, 'static>) -> Result, Self::Error> { + Ok(PixmanTarget(PixmanTargetInternal::Image(target))) + } + + fn supported_formats(&self) -> Option { + static RENDER_BUFFER_FORMATS: LazyLock = LazyLock::new(|| { + SUPPORTED_FORMATS + .iter() + .map(|code| DrmFormat { + code: *code, + modifier: DrmModifier::Linear, + }) + .collect() + }); + + Some(RENDER_BUFFER_FORMATS.clone()) + } +} + +/// Guard type wrapping the underlying `PixmanRenderer` of a `PixmanFrameGuard`. +#[derive(Debug)] +pub struct PixmanFrameGuard<'a, 'frame> { + renderer: &'a mut &'frame mut PixmanRenderer, +} + +impl AsRef for PixmanFrameGuard<'_, '_> { + fn as_ref(&self) -> &PixmanRenderer { + self.renderer + } +} + +impl AsMut for PixmanFrameGuard<'_, '_> { + fn as_mut(&mut self) -> &mut PixmanRenderer { + self.renderer + } +} + +impl<'a, 'frame, 'buffer> FrameContext<'a, 'frame, 'buffer, PixmanRenderer> for PixmanFrame<'frame, 'buffer> +where + 'frame: 'a, +{ + type Guard = PixmanFrameGuard<'a, 'frame>; + + fn renderer(&'a mut self) -> Self::Guard { + PixmanFrameGuard { + renderer: &mut self.renderer, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::backend::allocator::dmabuf::DmabufFlags; + + const SIZE: i32 = 4; + const BYTES_PER_PIXEL: u32 = 4; + const STRIDE: u32 = SIZE as u32 * BYTES_PER_PIXEL; + + // A memfd stands in for a real dmabuf: `Dmabuf::builder` accepts any fd and + // `map_plane` is a plain mmap, which is all the cache entries below need. + fn memfd_dmabuf() -> Dmabuf { + let fd = rustix::fs::memfd_create("dmabuf-stand-in", rustix::fs::MemfdFlags::CLOEXEC).unwrap(); + rustix::fs::ftruncate(&fd, (STRIDE * SIZE as u32) as u64).unwrap(); + let mut builder = Dmabuf::builder( + (SIZE, SIZE), + DrmFourcc::Argb8888, + DrmModifier::Linear, + DmabufFlags::empty(), + ); + builder.add_plane(fd, 0, STRIDE); + builder.build().unwrap() + } + + fn cache_entry(dmabuf: &Dmabuf) -> PixmanImage { + let mapping = dmabuf.map_plane(0, DmabufMappingMode::READ).unwrap(); + let image = pixman::Image::new(FormatCode::A8R8G8B8, SIZE as usize, SIZE as usize, false).unwrap(); + PixmanImage(Arc::new(PixmanImageInner { + #[cfg(feature = "wayland_frontend")] + buffer: None, + dmabuf: Some(PixmanDmabufMapping { + dmabuf: dmabuf.weak(), + _mapping: mapping, + }), + image: Mutex::new(image), + _flipped: false, + })) + } + + #[test] + fn cleanup_texture_cache_retains_live_entries() { + let mut renderer = PixmanRenderer::new().unwrap(); + let dmabuf = memfd_dmabuf(); + renderer.dmabuf_cache.push(cache_entry(&dmabuf)); + renderer.buffers.push(cache_entry(&dmabuf)); + + renderer.cleanup_texture_cache().unwrap(); + + assert_eq!(renderer.dmabuf_cache.len(), 1); + assert_eq!(renderer.buffers.len(), 1); + } + + #[test] + fn cleanup_texture_cache_drops_dead_entries() { + let mut renderer = PixmanRenderer::new().unwrap(); + let dmabuf = memfd_dmabuf(); + renderer.dmabuf_cache.push(cache_entry(&dmabuf)); + renderer.buffers.push(cache_entry(&dmabuf)); + drop(dmabuf); + + renderer.cleanup_texture_cache().unwrap(); + + assert!(renderer.dmabuf_cache.is_empty()); + assert!(renderer.buffers.is_empty()); + } + + #[test] + fn invalidate_caches_drops_live_entries() { + let mut renderer = PixmanRenderer::new().unwrap(); + let dmabuf = memfd_dmabuf(); + renderer.dmabuf_cache.push(cache_entry(&dmabuf)); + renderer.buffers.push(cache_entry(&dmabuf)); + + renderer.invalidate_caches().unwrap(); + + assert!(renderer.dmabuf_cache.is_empty()); + assert!(renderer.buffers.is_empty()); + } +} diff --git a/vendor/smithay/src/backend/renderer/sync/egl.rs b/vendor/smithay/src/backend/renderer/sync/egl.rs new file mode 100644 index 0000000..27d7157 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/sync/egl.rs @@ -0,0 +1,27 @@ +use std::{os::unix::io::OwnedFd, time::Duration}; + +use crate::backend::{ + egl::fence::EGLFence, + renderer::sync::{Fence, Interrupted}, +}; + +impl Fence for EGLFence { + fn wait(&self) -> Result<(), Interrupted> { + self.client_wait(None, false).map(|_| ()).map_err(|err| { + tracing::warn!(?err, "Waiting for fence was interrupted"); + Interrupted + }) + } + + fn is_exportable(&self) -> bool { + self.is_native() + } + + fn export(&self) -> Option { + self.export().ok() + } + + fn is_signaled(&self) -> bool { + self.client_wait(Some(Duration::ZERO), false).unwrap_or(false) + } +} diff --git a/vendor/smithay/src/backend/renderer/sync/mod.rs b/vendor/smithay/src/backend/renderer/sync/mod.rs new file mode 100644 index 0000000..9a63636 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/sync/mod.rs @@ -0,0 +1,116 @@ +//! Helper for synchronizing rendering operations +use std::{error::Error, fmt, os::unix::io::OwnedFd, sync::Arc}; + +use downcast_rs::{Downcast, impl_downcast}; + +#[cfg(feature = "backend_egl")] +mod egl; + +/// Waiting for the fence was interrupted for an unknown reason. +/// +/// This does not mean that the fence is signalled or not, neither that +/// any timeout was reached. Waiting should be attempted again. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Interrupted; + +impl fmt::Display for Interrupted { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Wait for Fence was interrupted") + } +} +impl Error for Interrupted {} + +/// A fence that will be signaled in finite time +pub trait Fence: std::fmt::Debug + Send + Sync + Downcast { + /// Queries the state of the fence + fn is_signaled(&self) -> bool; + + /// Blocks the current thread until the fence is signaled + fn wait(&self) -> Result<(), Interrupted>; + + /// Returns whether this fence can be exported + /// as a native fence fd + fn is_exportable(&self) -> bool; + + /// Export this fence as a native fence fd + fn export(&self) -> Option; +} +impl_downcast!(Fence); + +/// A sync point the will be signaled in finite time +#[derive(Debug, Clone)] +#[must_use = "this `SyncPoint` may contain a fence that should be awaited, failing to do so may result in unexpected rendering artifacts"] +pub struct SyncPoint { + fence: Option>, +} + +impl Default for SyncPoint { + fn default() -> Self { + Self::signaled() + } +} + +impl SyncPoint { + /// Create an already signaled sync point + pub fn signaled() -> Self { + Self { + fence: Default::default(), + } + } + + /// Returns `true` if `SyncPoint` contains a [`Fence`] + pub fn contains_fence(&self) -> bool { + self.fence.is_some() + } + + /// Get a reference to the underlying [`Fence`] if any + /// + /// Returns `None` if the sync point does not contain a fence + /// or contains a different type of fence + pub fn get(&self) -> Option<&F> { + self.fence.as_ref().and_then(|f| f.downcast_ref()) + } + + /// Queries the state of the sync point + /// + /// Will always return `true` in case the sync point does not contain a fence + pub fn is_reached(&self) -> bool { + self.fence.as_ref().map(|f| f.is_signaled()).unwrap_or(true) + } + + /// Blocks the current thread until the sync point is signaled + /// + /// If the sync point does not contain a fence this will never block. + #[profiling::function] + pub fn wait(&self) -> Result<(), Interrupted> { + if let Some(fence) = self.fence.as_ref() { + fence.wait() + } else { + Ok(()) + } + } + + /// Returns whether this sync point can be exported as a native fence fd + /// + /// Will always return `false` in case the sync point does not contain a fence + pub fn is_exportable(&self) -> bool { + self.fence.as_ref().map(|f| f.is_exportable()).unwrap_or(false) + } + + /// Export this [`SyncPoint`] as a native fence fd + /// + /// Will always return `None` in case the sync point does not contain a fence + #[profiling::function] + pub fn export(&self) -> Option { + self.fence.as_ref().and_then(|f| f.export()) + } +} + +impl From for SyncPoint { + #[inline] + fn from(value: T) -> Self { + SyncPoint { + fence: Some(Arc::new(value)), + } + } +} diff --git a/vendor/smithay/src/backend/renderer/test.rs b/vendor/smithay/src/backend/renderer/test.rs new file mode 100644 index 0000000..dc9d8d6 --- /dev/null +++ b/vendor/smithay/src/backend/renderer/test.rs @@ -0,0 +1,302 @@ +#![allow(missing_docs)] + +#[cfg(all( + feature = "wayland_frontend", + feature = "use_system_lib", + feature = "backend_egl" +))] +use crate::backend::renderer::ImportEgl; +#[cfg(feature = "wayland_frontend")] +use crate::{ + backend::renderer::{ImportDmaWl, ImportMemWl}, + reexports::wayland_server::protocol::wl_buffer, + wayland::{self, compositor::SurfaceData, shm::BufferAccessError}, +}; +use crate::{ + backend::{ + SwapBuffersError, + allocator::{Fourcc, dmabuf::Dmabuf}, + renderer::{ + DebugFlags, Frame, ImportDma, ImportMem, Renderer, RendererSuper, Texture, TextureFilter, + sync::SyncPoint, + }, + }, + utils::{Buffer, Physical, Rectangle, Size, Transform}, +}; + +#[cfg(feature = "wayland_frontend")] +use std::cell::Cell; +use std::sync::LazyLock; + +use super::{Color32F, ContextId}; + +/// All [`DummyRenderer`] instances share the same static [`ContextId`]. +static CONTEXT_ID: LazyLock> = LazyLock::new(ContextId::new); + +#[derive(Debug, Default)] +pub struct DummyRenderer; + +/// Error returned by the DummyRenderer +#[derive(thiserror::Error, Debug)] +pub enum DummyError { + /// Error accessing shm buffer + #[cfg(feature = "wayland_frontend")] + #[error("Error accessing buffer ({0:?})")] + BufferAccessError(BufferAccessError), + /// Blocking for a synchronization primitive failed + #[error("Blocking for a synchronization primitive got interrupted")] + SyncInterrupted, +} + +impl From for SwapBuffersError { + #[inline] + fn from(value: DummyError) -> Self { + SwapBuffersError::TemporaryFailure(Box::new(value)) + } +} + +impl RendererSuper for DummyRenderer { + type Error = DummyError; + type TextureId = DummyTexture; + type Framebuffer<'buffer> = DummyFramebuffer; + type Frame<'frame, 'buffer> + = DummyFrame + where + 'buffer: 'frame, + Self: 'frame; +} + +impl Renderer for DummyRenderer { + fn context_id(&self) -> ContextId { + CONTEXT_ID.clone() + } + + fn downscale_filter(&mut self, _filter: TextureFilter) -> Result<(), Self::Error> { + Ok(()) + } + + fn upscale_filter(&mut self, _filter: TextureFilter) -> Result<(), Self::Error> { + Ok(()) + } + + fn set_debug_flags(&mut self, _flags: DebugFlags) {} + + fn debug_flags(&self) -> DebugFlags { + DebugFlags::empty() + } + + fn render<'frame, 'buffer>( + &'frame mut self, + _target: &'frame mut DummyFramebuffer, + _size: Size, + _dst_transform: Transform, + ) -> Result + where + 'buffer: 'frame, + { + Ok(DummyFrame) + } + + fn wait(&mut self, sync: &SyncPoint) -> Result<(), Self::Error> { + sync.wait().map_err(|_| DummyError::SyncInterrupted) + } +} + +impl ImportMem for DummyRenderer { + fn import_memory( + &mut self, + _data: &[u8], + _format: Fourcc, + _size: Size, + _flipped: bool, + ) -> Result { + unimplemented!() + } + + fn update_memory( + &mut self, + _texture: &Self::TextureId, + _data: &[u8], + _region: Rectangle, + ) -> Result<(), Self::Error> { + unimplemented!() + } + + fn mem_formats(&self) -> Box> { + Box::new([Fourcc::Argb8888, Fourcc::Xrgb8888].iter().copied()) + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportMemWl for DummyRenderer { + fn import_shm_buffer( + &mut self, + buffer: &wl_buffer::WlBuffer, + surface: Option<&SurfaceData>, + _damage: &[Rectangle], + ) -> Result { + use std::ptr; + use wayland::shm::with_buffer_contents; + let ret = with_buffer_contents(buffer, |ptr, len, data| { + let offset = data.offset as u32; + let width = data.width as u32; + let height = data.height as u32; + let stride = data.stride as u32; + + let mut x = 0; + for h in 0..height { + for w in 0..width { + let idx = (offset + w + h * stride) as usize; + assert!(idx < len); + x |= unsafe { ptr::read(ptr.add(idx)) }; + } + } + + if let Some(data) = surface { + data.data_map.insert_if_missing(|| Cell::new(0u8)); + data.data_map.get::>().unwrap().set(x); + } + + (width, height) + }); + + match ret { + Ok((width, height)) => Ok(DummyTexture { width, height }), + Err(e) => Err(DummyError::BufferAccessError(e)), + } + } +} + +impl ImportDma for DummyRenderer { + fn import_dmabuf( + &mut self, + _dmabuf: &Dmabuf, + _damage: Option<&[Rectangle]>, + ) -> Result { + unimplemented!() + } +} + +#[cfg(all( + feature = "wayland_frontend", + feature = "use_system_lib", + feature = "backend_egl" +))] +impl ImportEgl for DummyRenderer { + fn bind_wl_display( + &mut self, + _display: &::wayland_server::DisplayHandle, + ) -> Result<(), crate::backend::egl::Error> { + unimplemented!() + } + + fn unbind_wl_display(&mut self) { + unimplemented!() + } + + fn egl_reader(&self) -> Option<&crate::backend::egl::display::EGLBufferReader> { + unimplemented!() + } + + fn import_egl_buffer( + &mut self, + _buffer: &wl_buffer::WlBuffer, + _surface: Option<&wayland::compositor::SurfaceData>, + _damage: &[Rectangle], + ) -> Result { + unimplemented!() + } +} + +#[cfg(feature = "wayland_frontend")] +impl ImportDmaWl for DummyRenderer {} + +#[derive(Debug)] +pub struct DummyFramebuffer; + +impl Texture for DummyFramebuffer { + fn width(&self) -> u32 { + 0 + } + + fn height(&self) -> u32 { + 0 + } + + fn format(&self) -> Option { + None + } +} + +#[derive(Debug)] +pub struct DummyFrame; + +impl Frame for DummyFrame { + type Error = DummyError; + type TextureId = DummyTexture; + + fn context_id(&self) -> ContextId { + CONTEXT_ID.clone() + } + + fn clear(&mut self, _color: Color32F, _damage: &[Rectangle]) -> Result<(), Self::Error> { + Ok(()) + } + + fn draw_solid( + &mut self, + _dst: Rectangle, + _damage: &[Rectangle], + _color: Color32F, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn render_texture_from_to( + &mut self, + _texture: &Self::TextureId, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _src_transform: Transform, + _alpha: f32, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn transformation(&self) -> Transform { + Transform::Normal + } + fn output_size(&self) -> Size { + Size::default() + } + + fn wait(&mut self, sync: &SyncPoint) -> Result<(), Self::Error> { + sync.wait().map_err(|_| DummyError::SyncInterrupted) + } + + fn finish(self) -> Result { + Ok(SyncPoint::default()) + } +} + +#[derive(Clone, Debug)] +pub struct DummyTexture { + width: u32, + height: u32, +} + +impl Texture for DummyTexture { + fn width(&self) -> u32 { + self.width + } + + fn height(&self) -> u32 { + self.height + } + + fn format(&self) -> Option { + None + } +} diff --git a/vendor/smithay/src/backend/renderer/utils/mod.rs b/vendor/smithay/src/backend/renderer/utils/mod.rs new file mode 100644 index 0000000..6c5b72b --- /dev/null +++ b/vendor/smithay/src/backend/renderer/utils/mod.rs @@ -0,0 +1,546 @@ +//! Utility module for helpers around drawing [`WlSurface`](wayland_server::protocol::wl_surface::WlSurface)s +//! and [`RenderElement`](super::element::RenderElement)s with [`Renderer`](super::Renderer)s. + +use crate::utils::{Buffer as BufferCoord, Coordinate, Logical, Physical, Point, Rectangle, Size}; +use std::{collections::VecDeque, fmt, sync::Arc}; + +#[cfg(feature = "wayland_frontend")] +mod wayland; +#[cfg(feature = "wayland_frontend")] +pub use self::wayland::*; + +/// A simple wrapper for counting commits +/// +/// The purpose of the counter is to keep track +/// on the number of times something has changed. +/// It provides an easy way to obtain the distance +/// between two instances of a [`CommitCounter`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct CommitCounter(usize); + +impl CommitCounter { + /// Increment the commit counter + pub fn increment(&mut self) { + self.0 = self.0.wrapping_add(1) + } + + /// Get the distance between two [`CommitCounter`]s + /// + /// If the [`CommitCounter`] is incremented on each recorded + /// damage this returns the count of damage that happened + /// between the [`CommitCounter`]s + /// + /// Returns `None` in case the distance could not be calculated. + /// If uses as part of damage tracking the tracked element + /// should be considered as fully damaged. + pub fn distance(&self, previous_commit: Option) -> Option { + // if commit > commit_count we have overflown, in that case the following map might result + // in a false-positive, if commit is still very large. So we force false in those cases. + // That will result in a potentially sub-optimal full damage every usize::MAX frames, + // which is acceptable. + previous_commit + .filter(|commit| commit <= self) + .map(|commit| self.0.wrapping_sub(commit.0)) + } +} + +impl From for CommitCounter { + #[inline] + fn from(counter: usize) -> Self { + CommitCounter(counter) + } +} + +/// A tracker for holding damage +/// +/// It keeps track of the submitted damage +/// and automatically caps the damage +/// with the specified limit. +/// +/// See [`DamageSnapshot`] for more +/// information. +pub struct DamageBag { + limit: usize, + state: DamageSnapshot, +} + +/// A snapshot of the current state of a [`DamageBag`] +/// +/// The snapshot can be used to get an immutable view +/// into the current state of a [`DamageBag`]. +/// It provides an easy way to get the damage between two +/// [`CommitCounter`]s. +pub struct DamageSnapshot { + limit: usize, + commit_counter: CommitCounter, + damage: Arc; MAX_DAMAGE_RECTS]>>>, +} + +impl Clone for DamageSnapshot { + #[inline] + fn clone(&self) -> Self { + Self { + limit: self.limit, + commit_counter: self.commit_counter, + damage: self.damage.clone(), + } + } +} + +impl fmt::Debug for DamageBag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageBag") + .field("limit", &self.limit) + .field("state", &self.state) + .finish() + } +} + +impl fmt::Debug for DamageBag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageBag") + .field("limit", &self.limit) + .field("state", &self.state) + .finish() + } +} + +impl fmt::Debug for DamageBag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageBag") + .field("limit", &self.limit) + .field("state", &self.state) + .finish() + } +} + +impl fmt::Debug for DamageSnapshot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSnapshot") + .field("commit_counter", &self.commit_counter) + .field("damage", &self.damage) + .finish() + } +} + +impl fmt::Debug for DamageSnapshot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSnapshot") + .field("commit_counter", &self.commit_counter) + .field("damage", &self.damage) + .finish() + } +} + +impl fmt::Debug for DamageSnapshot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSnapshot") + .field("commit_counter", &self.commit_counter) + .field("damage", &self.damage) + .finish() + } +} + +const MAX_DAMAGE_AGE: usize = 4; +const MAX_DAMAGE_RECTS: usize = 16; +const MAX_DAMAGE_SET: usize = MAX_DAMAGE_RECTS * 2; + +impl Default for DamageBag { + #[inline] + fn default() -> Self { + DamageBag::new(MAX_DAMAGE_AGE) + } +} + +impl DamageSnapshot { + fn new(limit: usize) -> Self { + DamageSnapshot { + limit, + commit_counter: CommitCounter::default(), + damage: Arc::new(VecDeque::with_capacity(limit)), + } + } + + /// Create an empty damage snapshot + pub fn empty() -> Self { + DamageSnapshot { + limit: 0, + commit_counter: CommitCounter::default(), + damage: Default::default(), + } + } + + /// Gets the current [`CommitCounter`] of this snapshot + /// + /// The returned [`CommitCounter`] should be stored after + /// calling [`damage_since`](DamageSnapshot::damage_since) + /// and provided to the next call of [`damage_since`](DamageSnapshot::damage_since) + /// to query the damage between these two [`CommitCounter`]s. + #[inline] + pub fn current_commit(&self) -> CommitCounter { + self.commit_counter + } + + /// Provides raw access to the stored damage + pub fn raw(&self) -> impl Iterator>> { + self.damage.iter().map(|d| d.iter()) + } + + fn reset(&mut self) { + Arc::make_mut(&mut self.damage).clear(); + self.commit_counter.increment(); + } +} + +impl DamageSnapshot { + /// Get the damage since the last commit + /// + /// Returns `None` in case the [`CommitCounter`] is too old + /// or the damage has been reset. In that case the whole + /// element geometry should be considered as damaged + /// + /// If the commit is recent enough and no damage has occurred + /// an empty `Vec` will be returned + pub fn damage_since(&self, commit: Option) -> Option> { + let distance = self.commit_counter.distance(commit); + + if distance + .map(|distance| distance <= self.damage.len()) + .unwrap_or(false) + { + let mut damage_set = DamageSet::default(); + for damage in self.damage.iter().take(distance.unwrap()) { + damage_set.damage.extend_from_slice(damage); + } + Some(damage_set) + } else { + None + } + } + + fn add(&mut self, damage: impl IntoIterator>) { + // FIXME: Get rid of this allocation here + let mut damage = damage.into_iter().filter(|d| !d.is_empty()).collect::>(); + + if damage.is_empty() { + // do not track empty damage + return; + } + + damage.dedup(); + + let inner_damage = Arc::make_mut(&mut self.damage); + inner_damage.push_front(smallvec::SmallVec::from_vec(damage)); + inner_damage.truncate(self.limit); + + self.commit_counter.increment(); + } +} + +impl DamageBag { + /// Initialize a a new [`DamageBag`] with the specified limit + pub fn new(limit: usize) -> Self { + DamageBag { + limit, + state: DamageSnapshot::new(limit), + } + } + + /// Gets the current [`CommitCounter`] of this tracker + #[inline] + pub fn current_commit(&self) -> CommitCounter { + self.state.current_commit() + } + + /// Provides raw access to the stored damage + pub fn raw(&self) -> impl Iterator>> { + self.state.raw() + } + + /// Reset the damage + /// + /// This should be called when the + /// tracked item has been resized + pub fn reset(&mut self) { + self.state.reset() + } +} + +impl DamageBag { + /// Get a snapshot of the current damage + pub fn snapshot(&self) -> DamageSnapshot { + self.state.clone() + } +} + +impl DamageBag { + /// Add some damage to the tracker + pub fn add(&mut self, damage: impl IntoIterator>) { + self.state.add(damage) + } + + /// Get the damage since the last commit + /// + /// Returns `None` in case the [`CommitCounter`] is too old + /// or the damage has been reset. In that case the whole + /// element geometry should be considered as damaged + /// + /// If the commit is recent enough and no damage has occurred + /// an empty `Vec` will be returned + pub fn damage_since(&self, commit: Option) -> Option> { + self.state.damage_since(commit) + } +} + +/// A set of damage returned from [`DamageBag::damage_since`] of [`DamageSnapshot::damage_since`] +pub struct DamageSet { + damage: smallvec::SmallVec<[Rectangle; MAX_DAMAGE_SET]>, +} + +impl Default for DamageSet { + fn default() -> Self { + Self { + damage: Default::default(), + } + } +} + +impl DamageSet { + /// Copy the damage from a slice into a new `DamageSet`. + #[inline] + pub fn from_slice(slice: &[Rectangle]) -> Self { + Self { + damage: smallvec::SmallVec::from_slice(slice), + } + } +} + +impl std::ops::Deref for DamageSet { + type Target = [Rectangle]; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.damage + } +} + +impl IntoIterator for DamageSet { + type Item = Rectangle; + + type IntoIter = DamageSetIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + DamageSetIter { + inner: self.damage.into_iter(), + } + } +} + +impl FromIterator> for DamageSet { + #[inline] + fn from_iter>>(iter: T) -> Self { + Self { + damage: smallvec::SmallVec::from_iter(iter), + } + } +} + +/// Iterator for [`DamageSet::into_iter`] +pub struct DamageSetIter { + inner: smallvec::IntoIter<[Rectangle; MAX_DAMAGE_SET]>, +} + +impl fmt::Debug for DamageSetIter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSetIter") + .field("inner", &self.inner) + .finish() + } +} + +impl fmt::Debug for DamageSetIter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSetIter") + .field("inner", &self.inner) + .finish() + } +} + +impl fmt::Debug for DamageSetIter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSetIter") + .field("inner", &self.inner) + .finish() + } +} + +impl Iterator for DamageSetIter { + type Item = Rectangle; + + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl fmt::Debug for DamageSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSet").field("damage", &self.damage).finish() + } +} + +impl fmt::Debug for DamageSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSet").field("damage", &self.damage).finish() + } +} + +impl fmt::Debug for DamageSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DamageSet").field("damage", &self.damage).finish() + } +} + +const MAX_OPAQUE_REGIONS: usize = 16; + +/// Wrapper for a set of opaque regions +pub struct OpaqueRegions { + regions: smallvec::SmallVec<[Rectangle; MAX_OPAQUE_REGIONS]>, +} + +impl Default for OpaqueRegions +where + N: Default, +{ + #[inline] + fn default() -> Self { + Self { + regions: Default::default(), + } + } +} + +impl OpaqueRegions { + /// Copy the opaque regions from a slice into a new `OpaqueRegions`. + #[inline] + pub fn from_slice(slice: &[Rectangle]) -> Self { + Self { + regions: smallvec::SmallVec::from_slice(slice), + } + } +} + +impl std::ops::Deref for OpaqueRegions { + type Target = [Rectangle]; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.regions + } +} + +impl IntoIterator for OpaqueRegions { + type Item = Rectangle; + + type IntoIter = OpaqueRegionsIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + OpaqueRegionsIter { + inner: self.regions.into_iter(), + } + } +} + +impl FromIterator> for OpaqueRegions { + #[inline] + fn from_iter>>(iter: T) -> Self { + Self { + regions: smallvec::SmallVec::from_iter(iter), + } + } +} + +/// Iterator for [`OpaqueRegions::into_iter`] +pub struct OpaqueRegionsIter { + inner: smallvec::IntoIter<[Rectangle; MAX_OPAQUE_REGIONS]>, +} + +impl fmt::Debug for OpaqueRegionsIter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueRegionsIter") + .field("inner", &self.inner) + .finish() + } +} + +impl fmt::Debug for OpaqueRegionsIter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueRegionsIter") + .field("inner", &self.inner) + .finish() + } +} + +impl fmt::Debug for OpaqueRegionsIter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueRegionsIter") + .field("inner", &self.inner) + .finish() + } +} + +impl Iterator for OpaqueRegionsIter { + type Item = Rectangle; + + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl fmt::Debug for OpaqueRegions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueRegions") + .field("regions", &self.regions) + .finish() + } +} + +impl fmt::Debug for OpaqueRegions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueRegions") + .field("regions", &self.regions) + .finish() + } +} + +impl fmt::Debug for OpaqueRegions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OpaqueRegions") + .field("regions", &self.regions) + .finish() + } +} + +/// Defines a view into the surface +#[derive(Debug, Default, PartialEq, Clone, Copy)] +pub struct SurfaceView { + /// The logical source used for cropping + pub src: Rectangle, + /// The logical destination size used for scaling + pub dst: Size, + /// The logical offset for a sub-surface + pub offset: Point, +} diff --git a/vendor/smithay/src/backend/renderer/utils/wayland.rs b/vendor/smithay/src/backend/renderer/utils/wayland.rs new file mode 100644 index 0000000..ef9f5ed --- /dev/null +++ b/vendor/smithay/src/backend/renderer/utils/wayland.rs @@ -0,0 +1,686 @@ +#[cfg(feature = "backend_drm")] +use crate::wayland::drm_syncobj::{DrmSyncPoint, DrmSyncobjCachedState}; +use crate::{ + backend::renderer::{ + ContextId, ErasedContextId, ImportAll, Renderer, Texture, buffer_dimensions, buffer_has_alpha, + element::RenderElement, + }, + utils::{Buffer as BufferCoord, Coordinate, Logical, Physical, Point, Rectangle, Scale, Size, Transform}, + wayland::{ + compositor::{ + self, BufferAssignment, Damage, RectangleKind, SUBSURFACE_ROLE, SubsurfaceCachedState, + SurfaceAttributes, SurfaceData, TraversalAction, add_destruction_hook, is_sync_subsurface, + with_surface_tree_downward, with_surface_tree_upward, + }, + viewporter, + }, +}; + +use std::{ + any::Any, + collections::{HashMap, hash_map::Entry}, + sync::{Arc, Mutex}, +}; + +use super::{CommitCounter, DamageBag, DamageSet, DamageSnapshot, SurfaceView}; +use tracing::{error, instrument, warn}; +use wayland_server::protocol::{wl_buffer::WlBuffer, wl_surface::WlSurface}; + +/// Type stored in WlSurface states data_map +/// +/// ```rs +/// compositor::with_states(surface, |states| { +/// let data = states.data_map.get::(); +/// }); +/// ``` +pub type RendererSurfaceStateUserData = Mutex; + +/// Surface state for rendering related data +#[derive(Default, Debug)] +pub struct RendererSurfaceState { + pub(crate) buffer_dimensions: Option>, + pub(crate) buffer_scale: i32, + pub(crate) buffer_transform: Transform, + pub(crate) buffer_has_alpha: Option, + pub(crate) buffer: Option, + pub(crate) damage: DamageBag, + pub(crate) renderer_seen: HashMap, + pub(crate) textures: HashMap>, + pub(crate) surface_view: Option, + pub(crate) opaque_regions: Vec>, +} + +/// SAFETY: Only thing unsafe here is the `Box`, which are the textures. +/// Those are guarded by our Renderers handling thread-safety and the `ContextId`. +/// Theoretically a renderer could be thread-safe, but its texture type isn't, but that is **very** theoretical. +unsafe impl Send for RendererSurfaceState {} +unsafe impl Sync for RendererSurfaceState {} + +#[derive(Debug)] +struct InnerBuffer { + buffer: WlBuffer, + #[cfg(feature = "backend_drm")] + acquire_point: Option, + #[cfg(feature = "backend_drm")] + release_point: Option, +} + +impl Drop for InnerBuffer { + #[inline] + fn drop(&mut self) { + self.buffer.release(); + #[cfg(feature = "backend_drm")] + if let Some(release_point) = &self.release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } + } +} + +/// A wayland buffer +#[derive(Debug, Clone)] +pub struct Buffer { + inner: Arc, +} + +impl Buffer { + /// Create a buffer with implicit sync + pub fn with_implicit(buffer: WlBuffer) -> Self { + Self { + inner: Arc::new(InnerBuffer { + buffer, + #[cfg(feature = "backend_drm")] + acquire_point: None, + #[cfg(feature = "backend_drm")] + release_point: None, + }), + } + } + + /// Create a buffer with explicit acquire and release sync points + #[cfg(feature = "backend_drm")] + pub fn with_explicit(buffer: WlBuffer, acquire_point: DrmSyncPoint, release_point: DrmSyncPoint) -> Self { + Self { + inner: Arc::new(InnerBuffer { + buffer, + acquire_point: Some(acquire_point), + release_point: Some(release_point), + }), + } + } + + #[cfg(feature = "backend_drm")] + #[allow(dead_code)] + pub(crate) fn acquire_point(&self) -> Option<&DrmSyncPoint> { + self.inner.acquire_point.as_ref() + } +} + +impl std::ops::Deref for Buffer { + type Target = WlBuffer; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner.buffer + } +} + +impl PartialEq for Buffer { + #[inline] + fn eq(&self, other: &WlBuffer) -> bool { + self.inner.buffer == *other + } +} + +impl PartialEq for &Buffer { + #[inline] + fn eq(&self, other: &WlBuffer) -> bool { + self.inner.buffer == *other + } +} + +impl RendererSurfaceState { + #[profiling::function] + pub(crate) fn update_buffer(&mut self, states: &SurfaceData) { + #[cfg(feature = "backend_drm")] + let mut guard = states.cached_state.get::(); + #[cfg(feature = "backend_drm")] + let syncobj_state = guard.current(); + + let mut guard = states.cached_state.get::(); + let attrs = guard.current(); + + let new_buffer = matches!(attrs.buffer, Some(BufferAssignment::NewBuffer(_))); + match attrs.buffer.take() { + Some(BufferAssignment::NewBuffer(buffer)) => { + self.buffer_dimensions = buffer_dimensions(&buffer); + if self.buffer_dimensions.is_none() { + // This results in us rendering nothing (can happen e.g. for failed egl-buffer-calls), + // but it is better than crashing the compositor for a bad buffer + self.reset(); + return; + } + self.buffer_has_alpha = buffer_has_alpha(&buffer); + self.buffer_scale = attrs.buffer_scale; + self.buffer_transform = attrs.buffer_transform.into(); + + if !self.buffer.as_ref().is_some_and(|b| b == buffer) { + self.buffer = Some(Buffer { + inner: Arc::new(InnerBuffer { + buffer, + #[cfg(feature = "backend_drm")] + acquire_point: syncobj_state.acquire_point.take(), + #[cfg(feature = "backend_drm")] + release_point: syncobj_state.release_point.take(), + }), + }); + } + + self.textures.clear(); + } + Some(BufferAssignment::Removed) => { + self.reset(); + return; + } + None => {} + }; + + let Some(buffer_dimensions) = self.buffer_dimensions else { + // nothing to be done without a buffer + return; + }; + + let surface_size = buffer_dimensions.to_logical(self.buffer_scale, self.buffer_transform); + let surface_view = SurfaceView::from_states(states, surface_size, attrs.client_scale); + let surface_view_changed = self.surface_view.replace(surface_view) != Some(surface_view); + + // if we received a new buffer also process the attached damage + if new_buffer { + let buffer_damage = attrs.damage.drain(..).flat_map(|dmg| { + match dmg { + Damage::Buffer(rect) => rect, + Damage::Surface(rect) => surface_view.rect_to_local(rect).to_i32_up().to_buffer( + self.buffer_scale, + self.buffer_transform, + &surface_size, + ), + } + .intersection(Rectangle::from_size(buffer_dimensions)) + }); + self.damage.add(buffer_damage); + } + + // if the buffer or our view changed rebuild our opaque regions + if new_buffer || surface_view_changed { + self.opaque_regions.clear(); + if !self.buffer_has_alpha.unwrap_or(true) { + self.opaque_regions.push(Rectangle::from_size(surface_view.dst)) + } else if let Some(region_attributes) = &attrs.opaque_region { + let opaque_regions = region_attributes + .rects + .iter() + .map(|(kind, rect)| { + let dest_size = surface_view.dst; + + let rect_constrained_loc = rect.loc.constrain(Rectangle::from_size(dest_size)); + let rect_clamped_size = rect + .size + .clamp((0, 0), (dest_size.to_point() - rect_constrained_loc).to_size()); + + let rect = Rectangle::new(rect_constrained_loc, rect_clamped_size); + + (kind, rect) + }) + .fold( + std::mem::take(&mut self.opaque_regions), + |mut new_regions, (kind, rect)| { + match kind { + RectangleKind::Add => { + let added_regions = rect.subtract_rects( + new_regions + .iter() + .filter(|region| region.overlaps_or_touches(rect)) + .copied(), + ); + new_regions.extend(added_regions); + } + RectangleKind::Subtract => { + new_regions = + Rectangle::subtract_rects_many_in_place(new_regions, [rect]); + } + } + + new_regions + }, + ); + + self.opaque_regions = opaque_regions; + } + } + } + + /// Get the current commit position of this surface + /// + /// The position should be saved after calling [`damage_since`](RendererSurfaceState::damage_since) and + /// provided as the commit in the next call. + pub fn current_commit(&self) -> CommitCounter { + self.damage.current_commit() + } + + /// Gets the damage since the last commit + /// + /// If either the commit is `None` or the commit is too old + /// the whole buffer will be returned as damage. + pub fn damage_since(&self, commit: Option) -> DamageSet { + self.damage.damage_since(commit).unwrap_or_else(|| { + self.buffer_dimensions + .as_ref() + .map(|size| DamageSet::from_slice(&[Rectangle::from_size(*size)])) + .unwrap_or_default() + }) + } + + /// Gets the current damage of this surface + pub fn damage(&self) -> DamageSnapshot { + self.damage.snapshot() + } + + /// Returns the logical size of the current attached buffer + pub fn buffer_size(&self) -> Option> { + self.buffer_dimensions + .as_ref() + .map(|dim| dim.to_logical(self.buffer_scale, self.buffer_transform)) + } + + /// Returns the scale of the current attached buffer + pub fn buffer_scale(&self) -> i32 { + self.buffer_scale + } + + /// Returns the transform of the current attached buffer + pub fn buffer_transform(&self) -> Transform { + self.buffer_transform + } + + /// Returns the logical size of the surface. + /// + /// Note: The surface size may not be equal to the buffer size in case + /// a viewport has been attached to the surface. + pub fn surface_size(&self) -> Option> { + self.surface_view.map(|view| view.dst) + } + + /// Get the attached buffer. + /// Can be used to check if surface is mapped + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + /// Gets a reference to the texture for the specified renderer context + pub fn texture(&self, id: ContextId) -> Option<&T> + where + T: Texture + 'static, + { + self.textures.get(&id.erased()).and_then(|e| e.downcast_ref()) + } + + /// Gets the opaque regions of this surface + pub fn opaque_regions(&self) -> Option<&[Rectangle]> { + // If the surface is unmapped there can be no opaque regions + self.surface_size()?; + + // To make it easier for upstream, but to no re-allocate a + // new vec if the opaque regions change for return None + // on empty regions + if self.opaque_regions.is_empty() { + return None; + } + + Some(&self.opaque_regions[..]) + } + + /// Gets the [`SurfaceView`] of this surface + pub fn view(&self) -> Option { + self.surface_view + } + + fn reset(&mut self) { + self.buffer_dimensions = None; + self.buffer = None; + self.textures.clear(); + self.damage.reset(); + self.surface_view = None; + self.buffer_has_alpha = None; + self.opaque_regions.clear(); + } +} + +/// Handler to let smithay take over buffer management. +/// +/// Needs to be called first on the commit-callback of +/// [`crate::wayland::compositor::CompositorHandler::commit`]. +/// +/// Consumes the buffer of [`SurfaceAttributes`], the buffer will +/// not be accessible anymore, but [`draw_render_elements`] and other +/// `draw_*` helpers of the [desktop module](`crate::desktop`) will +/// become usable for surfaces handled this way. +#[profiling::function] +pub fn on_commit_buffer_handler(surface: &WlSurface) { + if !is_sync_subsurface(surface) { + let mut new_surfaces = Vec::new(); + with_surface_tree_upward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |surf, states, _| { + if states + .data_map + .insert_if_missing_threadsafe(|| Mutex::new(RendererSurfaceState::default())) + { + new_surfaces.push(surf.clone()); + } + let mut data = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + data.update_buffer(states); + }, + |_, _, _| true, + ); + for surf in &new_surfaces { + add_destruction_hook(surf, |_: &mut D, surface| { + // We reset the state on destruction before the user_data is dropped + // to prevent a deadlock which can happen if we try to send a buffer + // release during drop. This also enables us to free resources earlier + // like the stored textures + compositor::with_states(surface, |data| { + if let Some(mut state) = data + .data_map + .get::() + .map(|s| s.lock().unwrap()) + { + state.reset(); + } + #[cfg(feature = "renderer_multi")] + crate::backend::renderer::multigpu::clear_surface_textures(data); + }); + }); + } + } +} + +impl SurfaceView { + fn from_states(states: &SurfaceData, surface_size: Size, client_scale: f64) -> SurfaceView { + viewporter::ensure_viewport_valid(states, surface_size); + let mut viewport_state = states.cached_state.get::(); + let viewport = viewport_state.current(); + + let src = viewport + .src + .unwrap_or_else(|| Rectangle::from_size(surface_size.to_f64())); + let dst = viewport.size().unwrap_or( + surface_size + .to_f64() + .to_client(1.) + .to_logical(client_scale) + .to_i32_round(), + ); + let offset = if states.role == Some(SUBSURFACE_ROLE) { + states + .cached_state + .get::() + .current() + .location + } else { + Default::default() + }; + SurfaceView { src, dst, offset } + } + + pub(crate) fn rect_to_global(&self, rect: Rectangle) -> Rectangle + where + N: Coordinate, + { + let scale = self.scale(); + let mut rect = rect.to_f64(); + rect.loc -= self.src.loc; + rect.upscale(scale) + } + + pub(crate) fn rect_to_local(&self, rect: Rectangle) -> Rectangle + where + N: Coordinate, + { + let scale = self.scale(); + let mut rect = rect.to_f64().downscale(scale); + rect.loc += self.src.loc; + rect + } + + fn scale(&self) -> Scale { + Scale::from(( + self.dst.w as f64 / self.src.size.w, + self.dst.h as f64 / self.src.size.h, + )) + } +} + +/// Access the buffer related states associated to this surface +/// +/// Calls [`compositor::with_states`] internally. +/// +/// Returns `None`, if there never was a commit processed through `on_commit_buffer_handler`. +pub fn with_renderer_surface_state(surface: &WlSurface, cb: F) -> Option +where + F: FnOnce(&mut RendererSurfaceState) -> T, +{ + compositor::with_states(surface, |states| { + let data = states.data_map.get::()?; + Some(cb(&mut data.lock().unwrap())) + }) +} + +/// Imports buffers of a surface using a given [`Renderer`] +/// +/// This (or `import_surface_tree`) need to be called before`draw_render_elements`, if used later. +/// +/// Note: This will do nothing, if you are not using +/// [`crate::backend::renderer::utils::on_commit_buffer_handler`] +/// to let smithay handle buffer management. +#[instrument(level = "trace", skip_all)] +#[profiling::function] +pub fn import_surface(renderer: &mut R, states: &SurfaceData) -> Result<(), R::Error> +where + R: Renderer + ImportAll, + R::TextureId: 'static, +{ + if let Some(data) = states.data_map.get::() { + let context_id = renderer.context_id().erased(); + let mut data_ref = data.lock().unwrap(); + let data = &mut *data_ref; + + let last_commit = data.renderer_seen.get(&context_id); + let buffer_damage = data.damage_since(last_commit.copied()); + if let Entry::Vacant(e) = data.textures.entry(context_id.clone()) { + if let Some(buffer) = data.buffer.as_ref() { + // There is no point in importing a single pixel buffer + if matches!( + crate::backend::renderer::buffer_type(buffer), + Some(crate::backend::renderer::BufferType::SinglePixel) + ) { + return Ok(()); + } + + match renderer.import_buffer(buffer, Some(states), &buffer_damage) { + Some(Ok(m)) => { + e.insert(Box::new(m)); + data.renderer_seen.insert(context_id, data.current_commit()); + } + Some(Err(err)) => { + warn!("Error loading buffer: {}", err); + return Err(err); + } + None => { + error!("Unknown buffer format for: {:?}", buffer); + } + } + } + } + } + + Ok(()) +} + +/// Imports buffers of a surface and its subsurfaces using a given [`Renderer`]. +/// +/// This (or `import_surface`) need to be called before `draw_render_elements`, if used later. +/// +/// Note: This will do nothing, if you are not using +/// [`crate::backend::renderer::utils::on_commit_buffer_handler`] +/// to let smithay handle buffer management. +#[instrument(level = "trace", skip_all)] +#[profiling::function] +pub fn import_surface_tree(renderer: &mut R, surface: &WlSurface) -> Result<(), R::Error> +where + R: Renderer + ImportAll, + R::TextureId: 'static, +{ + let scale = 1.0; + let location: Point = (0.0, 0.0).into(); + + let mut result = Ok(()); + with_surface_tree_downward( + surface, + location, + |_surface, states, location| { + let mut location = *location; + // Import a new buffer if necessary + if let Err(err) = import_surface(renderer, states) { + result = Err(err); + } + + if let Some(data) = states.data_map.get::() { + let mut data_ref = data.lock().unwrap(); + let data = &mut *data_ref; + // Now, should we be drawn ? + if data.textures.contains_key(&renderer.context_id().erased()) { + // if yes, also process the children + let surface_view = data.surface_view.unwrap(); + location += surface_view.offset.to_f64().to_physical(scale); + TraversalAction::DoChildren(location) + } else { + // we are not displayed, so our children are neither + TraversalAction::SkipChildren + } + } else { + // we are not displayed, so our children are neither + TraversalAction::SkipChildren + } + }, + |_, _, _| {}, + |_, _, _| true, + ); + result +} + +/// Draws the render elements using a given [`Renderer`] and [`Frame`](crate::backend::renderer::Frame) +/// +/// - `scale` needs to be equivalent to the fractional scale the rendered result should have. +/// - `location` is the position the surface should be drawn at. +/// - `damage` is the set of regions that should be drawn relative to the same origin as the location. +/// +/// Note: This element will render nothing, if you are not using +/// [`crate::backend::renderer::utils::on_commit_buffer_handler`] +/// to let smithay handle buffer management. +#[instrument(level = "trace", skip(frame, scale, elements))] +#[profiling::function] +pub fn draw_render_elements( + frame: &mut R::Frame<'_, '_>, + scale: S, + elements: &[E], + damage: &[Rectangle], +) -> Result>>, R::Error> +where + R: Renderer, + R::TextureId: 'static, + S: Into>, + E: RenderElement, +{ + let scale = scale.into(); + + let mut render_elements: Vec<&E> = Vec::with_capacity(elements.len()); + let mut opaque_regions: Vec> = Vec::new(); + let mut render_damage: Vec> = Vec::with_capacity(damage.len()); + + for element in elements { + let element_geometry = element.geometry(scale); + + // Then test if the element is completely hidden behind opaque regions + let is_hidden = element_geometry + .subtract_rects(opaque_regions.iter().copied()) + .is_empty(); + + if is_hidden { + // No need to draw a completely hidden element + continue; + } + + render_damage.extend(Rectangle::subtract_rects_many( + damage.iter().copied(), + opaque_regions.iter().copied(), + )); + + opaque_regions.extend(element.opaque_regions(scale).into_iter().map(|mut region| { + region.loc += element_geometry.loc; + region + })); + render_elements.insert(0, element); + } + + // Optimize the damage for rendering + render_damage.dedup(); + render_damage.retain(|rect| !rect.is_empty()); + // filter damage outside of the output gep and merge overlapping rectangles + render_damage = render_damage + .into_iter() + .fold(Vec::new(), |new_damage, mut rect| { + // replace with drain_filter, when that becomes stable to reuse the original Vec's memory + let (overlapping, mut new_damage): (Vec<_>, Vec<_>) = new_damage + .into_iter() + .partition(|other| other.overlaps_or_touches(rect)); + + for overlap in overlapping { + rect = rect.merge(overlap); + } + new_damage.push(rect); + new_damage + }); + + if render_damage.is_empty() { + return Ok(None); + } + + for element in render_elements.iter() { + let element_geometry = element.geometry(scale); + + let element_damage = damage + .iter() + .filter_map(|d| d.intersection(element_geometry)) + .map(|mut d| { + d.loc -= element_geometry.loc; + d + }) + .collect::>(); + + if element_damage.is_empty() { + continue; + } + + element.draw(frame, element.src(), element_geometry, &element_damage, &[], None)?; + } + + Ok(Some(render_damage)) +} diff --git a/vendor/smithay/src/backend/session/libseat.rs b/vendor/smithay/src/backend/session/libseat.rs new file mode 100644 index 0000000..8a47aaa --- /dev/null +++ b/vendor/smithay/src/backend/session/libseat.rs @@ -0,0 +1,311 @@ +//! +//! Implementation of the [`Session`] trait through the libseat. +//! +//! This requires libseat to be available on the system. + +use libseat::{Seat, SeatEvent}; +use std::{ + cell::RefCell, + collections::HashMap, + os::unix::io::{AsFd, AsRawFd, FromRawFd, OwnedFd, RawFd}, + path::Path, + rc::{Rc, Weak}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use rustix::{fs::OFlags, io::Errno}; + +use calloop::{ + EventSource, Poll, PostAction, Readiness, Token, TokenFactory, + channel::{self, Channel}, +}; + +use crate::backend::session::{AsErrno, Event as SessionEvent, Session}; + +use tracing::{debug, info_span, instrument}; + +#[derive(Debug)] +struct LibSeatSessionImpl { + seat: RefCell, + active: Arc, + devices: RefCell>, +} + +impl Drop for LibSeatSessionImpl { + fn drop(&mut self) { + debug!("Closing seat") + } +} + +/// [`Session`] via the libseat +#[derive(Debug, Clone)] +pub struct LibSeatSession { + internal: Weak, + seat_name: String, + span: tracing::Span, +} + +/// `SessionNotifier` via the libseat +#[derive(Debug)] +pub struct LibSeatSessionNotifier { + internal: Rc, + rx: Channel, + token: Option, + span: tracing::Span, +} + +impl LibSeatSession { + /// Tries to create a new session via libseat. + pub fn new() -> Result<(LibSeatSession, LibSeatSessionNotifier), Error> { + let span = info_span!("backend_session", "type" = "libseat"); + let _guard = span.enter(); + let (tx, rx) = calloop::channel::channel(); + + let seat = { + Seat::open(move |_seat, event| match event { + SeatEvent::Enable => { + debug!("Enable callback called"); + tx.send(event).unwrap(); + } + SeatEvent::Disable => { + debug!("Disable callback called"); + tx.send(event).unwrap(); + } + }) + }; + + drop(_guard); + seat.map(|mut seat| { + let seat_name = seat.name().to_owned(); + + // In some cases enable_seat event is available right after startup + // so, we can dispatch it + seat.dispatch(0).unwrap(); + let active = matches!(rx.try_recv(), Ok(SeatEvent::Enable)); + + let internal = Rc::new(LibSeatSessionImpl { + seat: RefCell::new(seat), + active: Arc::new(AtomicBool::new(active)), + devices: RefCell::new(HashMap::new()), + }); + + let session = LibSeatSession { + internal: Rc::downgrade(&internal), + seat_name, + span: span.clone(), + }; + + let notifier = LibSeatSessionNotifier { + internal, + rx, + token: None, + span, + }; + + (session, notifier) + }) + .map_err(|err| Error::FailedToOpenSession(Errno::from_raw_os_error(err.into()))) + } +} + +impl Session for LibSeatSession { + type Error = Error; + + #[instrument(parent = &self.span, skip(self))] + fn open(&mut self, path: &Path, _flags: OFlags) -> Result { + if let Some(session) = self.internal.upgrade() { + debug!("Opening device: {:?}", path); + + session + .seat + .borrow_mut() + .open_device(&path) + .map(|device| { + let raw_fd = device.as_fd().as_raw_fd(); + + session.devices.borrow_mut().insert(raw_fd, device); + + // SAFETY: `libseat::Device` does not close fd on drop + unsafe { OwnedFd::from_raw_fd(raw_fd) } + }) + .map_err(|err| Error::FailedToOpenDevice(Errno::from_raw_os_error(err.into()))) + } else { + Err(Error::SessionLost) + } + } + + #[instrument(parent = &self.span, skip(self))] + fn close(&mut self, fd: OwnedFd) -> Result<(), Self::Error> { + if let Some(session) = self.internal.upgrade() { + debug!("Closing device: {:?}", fd); + + if let Some(dev) = session.devices.borrow_mut().remove(&fd.as_fd().as_raw_fd()) { + session + .seat + .borrow_mut() + .close_device(dev) + .map_err(|err| Error::FailedToCloseDevice(Errno::from_raw_os_error(err.into()))) + } else { + Ok(()) + } + // `fd` is closed on drop + } else { + Err(Error::SessionLost) + } + } + + #[instrument(parent = &self.span, skip(self))] + fn change_vt(&mut self, vt: i32) -> Result<(), Self::Error> { + if let Some(session) = self.internal.upgrade() { + debug!("Session switch: {:?}", vt); + session + .seat + .borrow_mut() + .switch_session(vt) + .map_err(|err| Error::FailedToChangeVt(Errno::from_raw_os_error(err.into()))) + } else { + Err(Error::SessionLost) + } + } + + fn is_active(&self) -> bool { + if let Some(internal) = self.internal.upgrade() { + internal.active.load(Ordering::SeqCst) + } else { + false + } + } + + fn seat(&self) -> String { + self.seat_name.clone() + } +} + +impl LibSeatSessionNotifier { + /// Creates a new session object belonging to this notifier. + pub fn session(&self) -> LibSeatSession { + LibSeatSession { + internal: Rc::downgrade(&self.internal), + seat_name: self.internal.seat.borrow_mut().name().to_owned(), + span: self.span.clone(), + } + } +} + +impl EventSource for LibSeatSessionNotifier { + type Event = SessionEvent; + type Metadata = (); + type Ret = (); + type Error = Error; + + #[profiling::function] + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(SessionEvent, &mut ()), + { + if Some(token) == self.token { + self.internal.seat.borrow_mut().dispatch(0).unwrap(); + } + + let internal = &self.internal; + self.rx + .process_events(readiness, token, |event, _| match event { + channel::Event::Msg(event) => match event { + SeatEvent::Enable => { + internal.active.store(true, Ordering::SeqCst); + callback(SessionEvent::ActivateSession, &mut ()); + } + SeatEvent::Disable => { + internal.active.store(false, Ordering::SeqCst); + internal.seat.borrow_mut().disable().unwrap(); + callback(SessionEvent::PauseSession, &mut ()); + } + }, + channel::Event::Closed => { + // Tx is stored inside of Seat, and Rc is stored in LibSeatSessionNotifier so this is unreachable + } + }) + .map_err(|_| Error::SessionLost) + } + + fn register(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.rx.register(poll, factory)?; + + self.token = Some(factory.token()); + let mut seat = self.internal.seat.borrow_mut(); + // Safety: the seat fd cannot be close without removing the LibSeatSessionNotifier from the event loop + unsafe { + poll.register( + seat.get_fd().unwrap(), + calloop::Interest::READ, + calloop::Mode::Level, + self.token.unwrap(), + ) + } + } + + fn reregister(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.rx.reregister(poll, factory)?; + + self.token = Some(factory.token()); + let mut seat = self.internal.seat.borrow_mut(); + poll.reregister( + seat.get_fd().unwrap(), + calloop::Interest::READ, + calloop::Mode::Level, + self.token.unwrap(), + ) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.rx.unregister(poll)?; + + self.token = None; + let mut seat = self.internal.seat.borrow_mut(); + poll.unregister(seat.get_fd().unwrap()) + } +} + +/// Errors related to direct/tty sessions +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failed to open session + #[error("Failed to open session: {0}")] + FailedToOpenSession(Errno), + + /// Failed to open device + #[error("Failed to open device: {0}")] + FailedToOpenDevice(Errno), + + /// Failed to close device + #[error("Failed to close device: {0}")] + FailedToCloseDevice(Errno), + + /// Failed to close device + #[error("Failed to change vt: {0}")] + FailedToChangeVt(Errno), + + /// Session is already closed, + #[error("Session is already closed")] + SessionLost, +} + +impl AsErrno for Error { + fn as_errno(&self) -> Option { + match self { + &Self::FailedToOpenSession(errno) + | &Self::FailedToOpenDevice(errno) + | &Self::FailedToCloseDevice(errno) + | &Self::FailedToChangeVt(errno) => Some(errno.raw_os_error()), + _ => None, + } + } +} diff --git a/vendor/smithay/src/backend/session/mod.rs b/vendor/smithay/src/backend/session/mod.rs new file mode 100644 index 0000000..9c65ec1 --- /dev/null +++ b/vendor/smithay/src/backend/session/mod.rs @@ -0,0 +1,155 @@ +//! Abstraction of different session APIs. +//! +//! Sessions provide a way for multiple graphical systems to run in parallel by providing +//! mechanisms to switch between and handle device access and permissions for every running +//! instance. They are crucial to allow unprivileged processes to use graphical or input +//! devices. +//! +//! The functions of the provided interfaces lies in two main components: the ability to +//! open privileged devices, and be notified about when the session is paused and resumed +//! (for example when the user switches to an other TTY, or is the computer goes to sleep). +//! +//! ## General use +//! +//! Session handling in Smithay is done through a pair of types that each session provider implements. +//! +//! The first is a handle implementing the [`Session`] trait, which allows you to request the opening +//! of devices, a VT change, or information about the session state. +//! +//! The second is a notifier which informs you when the session is enabled or disabled by the system. +//! This notifier takes the form of a [`calloop`] event source to deliver pause and activation events. +//! +//! ## Available providers +//! +//! This module provides just one session implementation, through [libseat](https://sr.ht/~kennylevinsen/seatd/), +//! gated by the `backend_session_libseat` cargo feature. +//! +//! Other implementations can be provided out-of-tree. + +use rustix::fs::OFlags; +use std::{ + cell::RefCell, + os::unix::io::OwnedFd, + path::Path, + rc::Rc, + sync::{Arc, Mutex}, +}; + +/// General session interface. +/// +/// Provides a way to open and close devices and change the active vt. +pub trait Session { + /// Error type of the implementation + type Error: AsErrno; + + /// Opens a device at the given `path` with the given flags. + /// + /// Returns a raw file descriptor + fn open(&mut self, path: &Path, flags: OFlags) -> Result; + /// Close a previously opened file descriptor + fn close(&mut self, fd: OwnedFd) -> Result<(), Self::Error>; + + /// Change the currently active virtual terminal + fn change_vt(&mut self, vt: i32) -> Result<(), Self::Error>; + + /// Check if this session is currently active + fn is_active(&self) -> bool; + /// Which seat this session is on + fn seat(&self) -> String; +} + +/// Events that can be generated by a session +#[derive(Copy, Clone, Debug)] +pub enum Event { + /// The whole session has been paused + /// + /// All devices should be considered as paused + PauseSession, + /// The whole session has been activated + ActivateSession, +} + +impl Session for () { + type Error = (); + + fn open(&mut self, _path: &Path, _flags: OFlags) -> Result { + Err(()) + } + fn close(&mut self, _fd: OwnedFd) -> Result<(), Self::Error> { + Err(()) + } + + fn change_vt(&mut self, _vt: i32) -> Result<(), Self::Error> { + Err(()) + } + + fn is_active(&self) -> bool { + false + } + fn seat(&self) -> String { + String::from("seat0") + } +} + +impl Session for Rc> { + type Error = S::Error; + + fn open(&mut self, path: &Path, flags: OFlags) -> Result { + self.borrow_mut().open(path, flags) + } + + fn close(&mut self, fd: OwnedFd) -> Result<(), Self::Error> { + self.borrow_mut().close(fd) + } + + fn change_vt(&mut self, vt: i32) -> Result<(), Self::Error> { + self.borrow_mut().change_vt(vt) + } + + fn is_active(&self) -> bool { + self.borrow().is_active() + } + + fn seat(&self) -> String { + self.borrow().seat() + } +} + +impl Session for Arc> { + type Error = S::Error; + + fn open(&mut self, path: &Path, flags: OFlags) -> Result { + self.lock().unwrap().open(path, flags) + } + + fn close(&mut self, fd: OwnedFd) -> Result<(), Self::Error> { + self.lock().unwrap().close(fd) + } + + fn change_vt(&mut self, vt: i32) -> Result<(), Self::Error> { + self.lock().unwrap().change_vt(vt) + } + + fn is_active(&self) -> bool { + self.lock().unwrap().is_active() + } + + fn seat(&self) -> String { + self.lock().unwrap().seat() + } +} + +/// Allows errors to be described by an error number +pub trait AsErrno: ::std::fmt::Debug { + /// Returns the error number representing this error if any + fn as_errno(&self) -> Option; +} + +impl AsErrno for () { + fn as_errno(&self) -> Option { + None + } +} + +#[cfg(feature = "backend_session_libseat")] +pub mod libseat; diff --git a/vendor/smithay/src/backend/udev.rs b/vendor/smithay/src/backend/udev.rs new file mode 100644 index 0000000..f7ff265 --- /dev/null +++ b/vendor/smithay/src/backend/udev.rs @@ -0,0 +1,364 @@ +//! `udev` related functionality for automated device scanning +//! +//! This module mainly provides the [`UdevBackend`], which monitors available DRM devices and acts as +//! an event source to be inserted in [`calloop`], generating events whenever these devices change. +//! +//! *Note:* Once inserted into the event loop, the [`UdevBackend`] will only notify you about *changes* +//! in the device list. To get an initial snapshot of the state during your initialization, you need to +//! call its `device_list` method. +//! +//! ```no_run +//! use smithay::backend::udev::{UdevBackend, UdevEvent}; +//! +//! let udev = UdevBackend::new("seat0").expect("Failed to monitor udev."); +//! +//! for (dev_id, node_path) in udev.device_list() { +//! // process the initial list of devices +//! } +//! +//! # let event_loop = smithay::reexports::calloop::EventLoop::<()>::try_new().unwrap(); +//! # let loop_handle = event_loop.handle(); +//! // setup the event source for long-term monitoring +//! loop_handle.insert_source(udev, |event, _, _dispatch_data| match event { +//! UdevEvent::Added { device_id, path } => { +//! // a new device has been added +//! }, +//! UdevEvent::Changed { device_id } => { +//! // a device has been changed +//! }, +//! UdevEvent::Removed { device_id } => { +//! // a device has been removed +//! } +//! }).expect("Failed to insert the udev source into the event loop"); +//! ``` +//! +//! Additionally this contains some utility functions related to scanning. +//! +//! See also `anvil/src/udev.rs` for pure hardware backed example of a compositor utilizing this +//! backend. + +#[cfg(feature = "backend_drm")] +use drm::node::{DrmNode, NodeType}; +use libc::dev_t; +use rustix::fs::stat; +use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashMap, + ffi::OsString, + fmt, io, + os::unix::io::{AsFd, BorrowedFd}, + path::{Path, PathBuf}, +}; +use udev::{Device, Enumerator, EventType, MonitorBuilder, MonitorSocket}; + +use calloop::{EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory}; + +use tracing::{debug, debug_span, info, warn}; + +/// Backend to monitor available drm devices. +/// +/// Provides a way to automatically scan for available gpus and notifies the +/// given handler of any changes. Can be used to provide hot-plug functionality for gpus and +/// attached monitors. +pub struct UdevBackend { + devices: UdevDevices, + monitor: MonitorSocket, + token: Option, + span: tracing::Span, +} + +// MonitorSocket does not implement debug, so we have to impl Debug manually +impl fmt::Debug for UdevBackend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use udev::AsRaw; + f.debug_struct("UdevBackend") + .field("devices", &self.devices) + .field("monitor", &format!("MonitorSocket ({:?})", self.monitor.as_raw())) + .finish() + } +} + +impl AsFd for UdevBackend { + fn as_fd(&self) -> BorrowedFd<'_> { + self.monitor.as_fd() + } +} + +impl UdevBackend { + /// Creates a new [`UdevBackend`] + /// + /// ## Arguments + /// `seat` - system seat which should be bound + pub fn new>(seat: S) -> io::Result { + let seat = seat.as_ref(); + let span = debug_span!("backend_udev", seat = seat.to_string()); + let _guard = span.enter(); + + let devices = all_gpus(seat)? + .into_iter() + // Create devices + .flat_map(|path| match stat(&path) { + Ok(stat) => Some((stat.st_rdev, path)), + Err(err) => { + warn!("Unable to get id of {:?}, Error: {:?}. Skipping", path, err); + None + } + }) + .collect(); + let devices = UdevDevices(devices); + + let monitor = MonitorBuilder::new()?.match_subsystem("drm")?.listen()?; + + drop(_guard); + Ok(UdevBackend { + devices, + monitor, + token: None, + span, + }) + } + + /// Get a list of DRM devices currently known to the backend + /// + /// You should call this once before inserting the event source into your + /// event loop, to get an initial snapshot of the device state. + pub fn device_list(&self) -> impl Iterator { + self.devices.device_list() + } +} + +impl EventSource for UdevBackend { + type Event = UdevEvent; + type Metadata = UdevDevices; + type Ret = (); + type Error = io::Error; + + #[profiling::function] + fn process_events( + &mut self, + _: Readiness, + token: Token, + mut callback: F, + ) -> std::io::Result + where + F: FnMut(Self::Event, &mut Self::Metadata), + { + if Some(token) != self.token { + return Ok(PostAction::Continue); + } + + let _guard = self.span.enter(); + for event in self.monitor.iter() { + debug!( + "Udev event: type={}, devnum={:?} devnode={:?}", + event.event_type(), + event.devnum(), + event.devnode() + ); + match event.event_type() { + // New device + EventType::Add => { + if let (Some(path), Some(devnum)) = (event.devnode(), event.devnum()) { + info!("New device: #{} at {}", devnum, path.display()); + if self.devices.insert(devnum, path.to_path_buf()).is_none() { + callback( + UdevEvent::Added { + device_id: devnum, + path: path.to_path_buf(), + }, + &mut self.devices, + ); + } + } + } + // Device removed + EventType::Remove => { + if let Some(devnum) = event.devnum() { + info!("Device removed: #{}", devnum); + if self.devices.remove(&devnum).is_some() { + callback(UdevEvent::Removed { device_id: devnum }, &mut self.devices); + } + } + } + // New connector + EventType::Change => { + if let Some(devnum) = event.devnum() { + info!("Device changed: #{}", devnum); + if self.devices.contains_key(&devnum) { + callback(UdevEvent::Changed { device_id: devnum }, &mut self.devices); + } + } + } + _ => {} + } + } + Ok(PostAction::Continue) + } + + fn register(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.token = Some(factory.token()); + // Safety: the fd is owned by the UdevBackend and cannot be closed before it is removed from the event loop + unsafe { poll.register(self.as_fd(), Interest::READ, Mode::Level, self.token.unwrap()) } + } + + fn reregister(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + self.token = Some(factory.token()); + poll.reregister(self.as_fd(), Interest::READ, Mode::Level, self.token.unwrap()) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.token = None; + poll.unregister(self.as_fd()) + } +} + +/// Udev DRM devices. +#[derive(Debug)] +pub struct UdevDevices(HashMap); + +impl UdevDevices { + /// Get a list of DRM devices currently known to the backend + /// + /// You should call this once before inserting the event source into your + /// event loop, to get an initial snapshot of the device state. + pub fn device_list(&self) -> impl Iterator { + self.0.iter().map(|(&id, path)| (id, path.as_ref())) + } +} + +impl Deref for UdevDevices { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for UdevDevices { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Events generated by the [`UdevBackend`], notifying you of changes in system devices +#[derive(Debug)] +pub enum UdevEvent { + /// A new device has been detected + Added { + /// ID of the new device + device_id: dev_t, + /// Path of the new device + path: PathBuf, + }, + /// A device has changed + Changed { + /// ID of the changed device + device_id: dev_t, + }, + /// A device has been removed + Removed { + /// ID of the removed device + device_id: dev_t, + }, +} + +/// Returns the path of the primary GPU device if any +/// +/// Might be used for filtering of [`UdevEvent::Added`] or for manual +/// [`DrmDevice`](crate::backend::drm::DrmDevice) initialization. +pub fn primary_gpu>(seat: S) -> io::Result> { + let gpu_devices = gpus_for_seat(seat)?; + + fn has_boot_vga(device: &Device) -> bool { + if let Ok(Some(pci)) = device.parent_with_subsystem(Path::new("pci")) { + if let Some(id) = pci.attribute_value("boot_vga") { + return id == "1"; + } + } + false + } + + // 1st priority: GPU with boot_vga=1 + if let Some(path) = gpu_devices + .iter() + .filter(|device| has_boot_vga(device)) + .flat_map(|device| device.devnode().map(PathBuf::from)) + .next() + { + return Ok(Some(path)); + } + + let gpu_paths = { + let mut gpu_paths = gpu_devices + .into_iter() + .flat_map(|device| device.devnode().map(PathBuf::from)) + .collect::>(); + gpu_paths.sort(); + gpu_paths + }; + + // 2nd priority: GPU with a render node + #[cfg(feature = "backend_drm")] + if let Some(path) = gpu_paths.iter().find(|path| { + DrmNode::from_path(path) + .ok() + .and_then(|node| node.node_with_type(NodeType::Render)) + .is_some_and(|res| res.is_ok()) + }) { + return Ok(Some(path.clone())); + } + + // 3rd priority: first GPU in alphabetical order + Ok(gpu_paths.first().cloned()) +} + +/// Returns the paths of all available GPU devices +/// +/// Might be used for manual [`DrmDevice`](crate::backend::drm::DrmDevice) +/// initialization. +pub fn all_gpus>(seat: S) -> io::Result> { + let mut gpus = gpus_for_seat(seat)? + .into_iter() + .flat_map(|device| device.devnode().map(PathBuf::from)) + .collect::>(); + gpus.sort(); + Ok(gpus) +} + +fn gpus_for_seat>(seat: S) -> io::Result> { + let mut enumerator = Enumerator::new()?; + enumerator.match_subsystem("drm")?; + enumerator.match_sysname("card[0-9]*")?; + let gpus = enumerator + .scan_devices()? + .filter(|device| { + device + .property_value("ID_SEAT") + .map(|x| x.to_os_string()) + .unwrap_or_else(|| OsString::from("seat0")) + == *seat.as_ref() + }) + .collect::>(); + Ok(gpus) +} + +/// Returns the loaded driver for a device named by it's [`dev_t`]. +pub fn driver(dev: dev_t) -> io::Result> { + let mut enumerator = Enumerator::new()?; + enumerator.match_subsystem("drm")?; + enumerator.match_sysname("card[0-9]*")?; + Ok(enumerator + .scan_devices()? + .filter(|device| device.devnum() == Some(dev)) + .flat_map(|dev| { + let mut device = Some(dev); + while let Some(dev) = device { + if dev.driver().is_some() { + return dev.driver().map(std::ffi::OsStr::to_os_string); + } + device = dev.parent(); + } + None + }) + .next()) +} diff --git a/vendor/smithay/src/backend/vulkan/inner.rs b/vendor/smithay/src/backend/vulkan/inner.rs new file mode 100644 index 0000000..91714f1 --- /dev/null +++ b/vendor/smithay/src/backend/vulkan/inner.rs @@ -0,0 +1,77 @@ +use std::{ + ffi::{CStr, CString}, + fmt, +}; + +use ash::{ext, vk}; + +use super::{LIBRARY, LoadError, version::Version}; + +pub struct InstanceInner { + pub instance: ash::Instance, + pub version: Version, + pub debug_state: Option, + pub span: tracing::Span, + + /// Enabled instance extensions. + pub enabled_extensions: Vec<&'static CStr>, +} + +// SAFETY: Destruction is externally synchronized (`InstanceInner` owns the +// `Instance`, and is held by a single thread when `Drop` is called). +unsafe impl Send for InstanceInner {} +unsafe impl Sync for InstanceInner {} + +pub struct DebugState { + pub debug_utils: ext::debug_utils::Instance, + pub debug_messenger: vk::DebugUtilsMessengerEXT, + pub span_ptr: *mut tracing::Span, +} + +impl fmt::Debug for InstanceInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InstanceInner") + .field("instance", &self.instance.handle()) + .finish_non_exhaustive() + } +} + +impl Drop for InstanceInner { + fn drop(&mut self) { + let span = if let Some(debug) = &self.debug_state { + unsafe { + debug + .debug_utils + .destroy_debug_utils_messenger(debug.debug_messenger, None); + } + Some(unsafe { Box::from_raw(debug.span_ptr) }) + } else { + None + }; + + // Users of `Instance` are responsible for compliance with `VUID-vkDestroyInstance-instance-00629`. + + // SAFETY (Host Synchronization): InstanceInner is always stored in an Arc, therefore destruction is + // synchronized (since the inner value of an Arc is always dropped on a single thread). + unsafe { self.instance.destroy_instance(None) }; + + // Now that the instance has been destroyed, we can destroy the span. + drop(span); + } +} + +impl super::Instance { + pub(super) fn enumerate_layers() -> Result, LoadError> { + let library = LIBRARY.as_ref().or(Err(LoadError))?; + + let layers = unsafe { library.enumerate_instance_layer_properties() } + .or(Err(LoadError))? + .into_iter() + .map(|properties| { + // SAFETY: Vulkan guarantees the string is null terminated. + unsafe { CStr::from_ptr(&properties.layer_name as *const _) }.to_owned() + }); + + Ok(layers) + } +} diff --git a/vendor/smithay/src/backend/vulkan/mod.rs b/vendor/smithay/src/backend/vulkan/mod.rs new file mode 100644 index 0000000..2885768 --- /dev/null +++ b/vendor/smithay/src/backend/vulkan/mod.rs @@ -0,0 +1,744 @@ +//! Types to initialize and use Vulkan. +//! +//! This module provides some thin abstractions over [`ash`](https://crates.io/crates/ash) for initializing +//! Vulkan. +//! +//! This module does not provide abstractions for logical devices, rendering or memory allocation. These +//! should instead be provided in higher level abstractions. +//! +//! Smithay requires at least Vulkan 1.1[^version]. +//! +//! # [`Instance`] +//! +//! To use Vulkan, you would first instantiate an [`Instance`]. An instance is effectively the Vulkan library +//! and provides some information about the environment. This includes the list of supported +//! [instance extensions](Instance::enumerate_extensions) and the list of available physical devices. +//! +//! An instance is constructed using an [`Instance::new`] or [`Instance::with_extensions`]. +//! +//! ## Layers +//! +//! The validation layers will be enabled if debug assertions are enabled and the validation layers are +//! available on your system. +//! +//! ## Instance extensions +//! +//! Instances may be created with some enabled extensions. Note that any features gated by an extension are +//! only available if the extension (and it's dependencies) are enabled (you will get a validation error if +//! you ignore that). +//! +//! Some features such as window system integration are only available if their features are enabled. +//! Available instance extensions may be obtained using [`Instance::enumerate_extensions`]. +//! +//! # [`PhysicalDevice`] +//! +//! Once you have an instance, you may want to find a suitable device to use. A [`PhysicalDevice`] describes a +//! Vulkan implementation that may correspond to a real or virtual device. +//! +//! To get all the available devices, use [`PhysicalDevice::enumerate`]. +//! +//! Physical devices are also describe the logical devices that can be created. A physical device can describe +//! a variety of properties that may be used for device selection, including but not limited to: +//! - [Device name](PhysicalDevice::name) +//! - [Supported Vulkan API version](PhysicalDevice::api_version) +//! - [Type](PhysicalDevice::ty) of the device +//! - [Driver information](PhysicalDevice::driver) +//! - [Extensions](PhysicalDevice::device_extensions) +//! - [Features](PhysicalDevice::features) and [limits](PhysicalDevice::limits) +//! +//! Physical devices implement [`Eq`][^device_eq], meaning two physical devices can be tested for equality. +//! +//! ## Device extensions +//! +//! Depending on the device extension (see the Vulkan specification), a device extension may indicate some +//! physical device data is available or indicate some device feature is supported. Any features that are +//! added using a device extension must be enabled in order to be used. +//! +//! [^version]: Internally Vulkan 1.1 is required because several extensions that were made part of core are +//! used quite extensively in some of the extensions our abstractions use. The vast majority of systems using +//! Vulkan also support at least Vulkan 1.1. If you need Vulkan 1.0 support, please open an issue and we can +//! discuss Vulkan 1.0 support. +//! +//! [^device_eq]: Two physical devices are only equal if both physical devices are created from the same +//! instance and have the same physical device handle. + +#![warn(missing_debug_implementations)] + +use std::{ + env::{self, VarError}, + ffi::{CStr, CString}, + sync::{Arc, LazyLock}, +}; + +use ash::{ + Entry, ext, + prelude::VkResult, + vk::{self, PhysicalDeviceDriverProperties, PhysicalDeviceDrmPropertiesEXT}, +}; +use libc::c_void; +use scopeguard::ScopeGuard; +use tracing::{error, info, info_span, instrument, trace, warn}; + +use crate::backend::vulkan::inner::DebugState; + +use self::{inner::InstanceInner, version::Version}; + +#[cfg(feature = "backend_drm")] +use super::drm::DrmNode; + +mod inner; +mod phd; + +pub mod version; + +static LIBRARY: LazyLock> = + LazyLock::new(|| unsafe { Entry::load().map_err(|_| LoadError) }); + +/// Error loading the Vulkan library +#[derive(Debug, thiserror::Error)] +#[error("Failed to load the Vulkan library")] +pub struct LoadError; + +/// An error that may occur when creating an [`Instance`]. +#[derive(Debug, thiserror::Error)] +pub enum InstanceError { + /// The instance was created using Vulkan 1.0. + #[error("Smithay requires at least Vulkan 1.1")] + UnsupportedVersion, + + /// Failed to load the Vulkan library. + #[error(transparent)] + Load(#[from] LoadError), + + /// Vulkan API error. + #[error(transparent)] + Vk(#[from] vk::Result), +} + +/// Error returned when a physical device property is not supported +#[derive(Debug, thiserror::Error)] +pub enum UnsupportedProperty { + /// Some required extensions are not available. + #[error("The following extensions are not available {0:?}")] + Extensions(&'static [&'static CStr]), +} + +/// App info to be passed to the Vulkan implementation. +#[derive(Debug)] +pub struct AppInfo { + /// Name of the app. + pub name: String, + /// Version of the app. + pub version: Version, +} + +/// A Vulkan instance. +/// +/// An instance is the object which tracks an application's Vulkan state. An instance allows an application to +/// get a list of physical devices. +/// +/// In the Vulkan it is common to have objects which may not outlive the parent instance. A great way to +/// ensure compliance when using child objects is to [`Clone`] the instance and keep a handle with the child +/// object. This will ensure the child object does not outlive the instance. +/// +/// An instance is [`Send`] and [`Sync`] which allows meaning multiple threads to access the Vulkan state. +/// Note that this **does not** mean the entire Vulkan API is thread safe, you will need to read the +/// specification to determine what parts of the Vulkan API require external synchronization. +/// +/// # Instance extensions +/// +/// In order to use features exposed through instance extensions (such as window system integration), you must +/// enable the extensions corresponding to the feature. +/// +/// By default, [`Instance`] will automatically try to enable the following instance extensions if available: +/// * `VK_EXT_debug_utils` +/// +/// No users should assume the instance extensions that are automatically enabled are available. +#[derive(Debug, Clone)] +pub struct Instance(Arc); + +impl Instance { + /// Creates a new [`Instance`]. + pub fn new(max_version: Version, app_info: Option) -> Result { + unsafe { Self::with_extensions(max_version, app_info, &[]) } + } + + /// Creates a new [`Instance`] with some additionally specified extensions. + /// + /// # Safety + /// + /// * All valid usage requirements specified by [`vkCreateInstance`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkCreateInstance.html) + /// must be satisfied. + /// * Any enabled extensions must also have the dependency extensions enabled + /// (see `VUID-vkCreateInstance-ppEnabledExtensionNames-01388`). + pub unsafe fn with_extensions( + max_version: Version, + app_info: Option, + extensions: &[&'static CStr], + ) -> Result { + assert!( + max_version >= Version::VERSION_1_1, + "Smithay requires at least Vulkan 1.1" + ); + let requested_max_version = get_env_or_max_version(max_version); + + let span = info_span!("backend_vulkan", version = tracing::field::Empty); + let _guard = span.enter(); + + // Determine the maximum instance version that is possible. + let max_version = { + unsafe { + LIBRARY + .as_ref() + .or(Err(LoadError))? + .try_enumerate_instance_version() + } + // Any allocation errors must be the result of the loader or layers + .or(Err(LoadError))? + .map(Version::from_raw) + // Vulkan 1.0 does not have `vkEnumerateInstanceVersion`. + .unwrap_or(Version::VERSION_1_0) + }; + + if max_version == Version::VERSION_1_0 { + error!("Vulkan does not support version 1.1"); + return Err(InstanceError::UnsupportedVersion); + } + + // Pick the lower of the requested max version and max possible version + let api_version = Version::from_raw(u32::min(max_version.to_raw(), requested_max_version.to_raw())); + span.record("version", tracing::field::display(api_version)); + + let available_layers = Self::enumerate_layers()?.collect::>(); + let available_extensions = Self::enumerate_extensions()?.collect::>(); + + let mut layers = Vec::new(); + + // Enable debug layers if present and debug assertions are enabled. + if cfg!(debug_assertions) { + const VALIDATION: &CStr = c"VK_LAYER_KHRONOS_validation"; + + if available_layers + .iter() + .any(|layer| layer.as_c_str() == VALIDATION) + { + layers.push(VALIDATION); + } else { + warn!("Validation layers not available. These can be installed through your package manager",); + } + } + + let mut enabled_extensions = Vec::<&'static CStr>::new(); + enabled_extensions.extend(extensions); + + // Enable debug utils if available. + let has_debug_utils = available_extensions + .iter() + .any(|name| name.as_c_str() == ext::debug_utils::NAME); + + if has_debug_utils { + enabled_extensions.push(ext::debug_utils::NAME); + } + + // Both of these are safe because both vecs contain static CStrs. + let extension_pointers = enabled_extensions + .iter() + .map(|name| name.as_ptr()) + .collect::>(); + let layer_pointers = layers.iter().map(|name| name.as_ptr()).collect::>(); + + let app_version = app_info.as_ref().map(|info| info.version.to_raw()); + let app_name = + app_info.map(|info| CString::new(info.name).expect("app name contains null terminator")); + let mut app_info = vk::ApplicationInfo::default() + .api_version(api_version.to_raw()) + // SAFETY: null terminated with no interior null bytes. + .engine_name(c"Smithay") + .engine_version(Version::SMITHAY.to_raw()); + + if let Some(app_version) = app_version { + app_info = app_info.application_version(app_version); + } + + if let Some(app_name) = &app_name { + app_info = app_info.application_name(app_name); + } + + let library = LIBRARY.as_ref().map_err(|_| LoadError)?; + let create_info = vk::InstanceCreateInfo::default() + .application_info(&app_info) + .enabled_layer_names(&layer_pointers) + .enabled_extension_names(&extension_pointers); + + // Place the instance in a scopeguard in case creating the debug messenger fails. + let instance = scopeguard::guard( + unsafe { library.create_instance(&create_info, None) }?, + |instance| unsafe { + instance.destroy_instance(None); + }, + ); + + // Setup the debug utils + let debug_state = if has_debug_utils { + let span = info_span!("backend_vulkan_debug"); + let debug_utils = ext::debug_utils::Instance::new(library, &instance); + // Place the pointer to the span in a scopeguard to prevent a memory leak in case creating the + // debug messenger fails. + let span_ptr = scopeguard::guard(Box::into_raw(Box::new(span)), |ptr| unsafe { + let _ = Box::from_raw(ptr); + }); + + let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default() + .message_severity( + vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + | vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE + | vk::DebugUtilsMessageSeverityFlagsEXT::INFO + | vk::DebugUtilsMessageSeverityFlagsEXT::ERROR, + ) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION, + ) + .pfn_user_callback(Some(vulkan_debug_utils_callback)) + .user_data(*span_ptr as *mut _); + + let debug_messenger = unsafe { debug_utils.create_debug_utils_messenger(&create_info, None) }?; + + // Disarm the destructor for the logger pointer since the instance is now responsible for + // destroying the logger. + let span_ptr = ScopeGuard::into_inner(span_ptr); + + Some(DebugState { + debug_utils, + debug_messenger, + span_ptr, + }) + } else { + None + }; + + // Creating the debug messenger was successful, disarm the scopeguard and let InstanceInner manage + // destroying the instance. + let instance = ScopeGuard::into_inner(instance); + drop(_guard); + let inner = InstanceInner { + instance, + version: api_version, + debug_state, + span, + enabled_extensions, + }; + + info!("Created new instance"); + info!("Enabled instance extensions: {:?}", inner.enabled_extensions); + + #[allow(clippy::arc_with_non_send_sync)] + Ok(Instance(Arc::new(inner))) + } + + /// Returns an iterator which contains the available instance extensions on the system. + pub fn enumerate_extensions() -> Result, LoadError> { + let library = LIBRARY.as_ref().or(Err(LoadError))?; + + let extensions = unsafe { library.enumerate_instance_extension_properties(None) } + .or(Err(LoadError))? + .into_iter() + .map(|properties| { + // SAFETY: Vulkan guarantees the string is null terminated. + unsafe { CStr::from_ptr(&properties.extension_name as *const _) }.to_owned() + }) + .collect::>() + .into_iter(); + + Ok(extensions) + } + + /// Returns the enabled instance extensions. + pub fn enabled_extensions(&self) -> impl Iterator { + self.0.enabled_extensions.iter().copied() + } + + /// Returns true if the specified instance extension is enabled. + /// + /// This function may be used to ensure safe access to features provided by instance extensions. + pub fn is_extension_enabled(&self, extension: &CStr) -> bool { + self.enabled_extensions().any(|name| name == extension) + } + + /// Returns the version of Vulkan supported by this instance. + /// + /// This corresponds to the version specified when building the instance. + pub fn api_version(&self) -> Version { + self.0.version + } + + /// Returns a reference to the underlying [`ash::Instance`]. + /// + /// Any objects created using the handle must be destroyed before the final instance is dropped per the + /// valid usage requirements (`VUID-vkDestroyInstance-instance-00629`). + pub fn handle(&self) -> &ash::Instance { + &self.0.instance + } +} + +/// A Vulkan physical device. +/// +/// A physical device refers to a Vulkan implementation. A physical device has no associated resources and may +/// be used to create a logical device. +#[derive(Debug, Clone)] +pub struct PhysicalDevice { + phd: vk::PhysicalDevice, + info: PhdInfo, + extensions: Vec, + instance: Instance, + span: tracing::Span, +} + +impl PhysicalDevice { + /// Enumerates over all physical devices available on the system, returning an iterator of [`PhysicalDevice`] + pub fn enumerate(instance: &Instance) -> VkResult> { + let _span = instance.0.span.enter(); + + // Must clone instance or else the returned iterator has a lifetime over `&Instance` + let instance = instance.clone(); + let devices = unsafe { instance.handle().enumerate_physical_devices() }?; + let devices = devices + .into_iter() + // TODO: Warn if any physical devices have an error when getting device properties. + .flat_map(move |phd| unsafe { PhysicalDevice::from_phd(&instance, phd) }) + .flatten(); + + Ok(devices) + } + + /// Returns the name of the device. + pub fn name(&self) -> &str { + &self.info.name + } + + /// Returns the version of Vulkan supported by this device. + /// + /// Unlike the `api_version` property, which is the version reported by the device directly, this function + /// returns the version the device can actually support, based on the instance’s, `api_version`. + /// + /// The Vulkan specification provides more information about the version requirements: + pub fn api_version(&self) -> Version { + self.info.api_version + } + + /// Returns the device type. + /// + /// This may be used during device selection to choose a higher performance GPU. + pub fn ty(&self) -> vk::PhysicalDeviceType { + self.info.properties.device_type + } + + /// Returns the Vulkan 1.0 physical device features. + pub fn features(&self) -> vk::PhysicalDeviceFeatures { + self.info.features + } + + /// Returns the physical device properties. + /// + /// Some properties such as the device name can be obtained using other functions defined on + /// [`PhysicalDevice`]. + pub fn properties(&self) -> vk::PhysicalDeviceProperties { + self.info.properties + } + + /// Returns the device's descriptor set properties. + /// + /// This also describes the maximum memory allocation size. + pub fn properties_maintenance_3(&self) -> vk::PhysicalDeviceMaintenance3Properties<'_> { + self.info.maintenance_3 + } + + /// Information about universally unique identifiers (UUIDs) that identify this device. + pub fn id_properties(&self) -> vk::PhysicalDeviceIDProperties<'_> { + self.info.id + } + + /// Returns the physical device limits. + pub fn limits(&self) -> vk::PhysicalDeviceLimits { + self.info.properties.limits + } + + /// Information about the Vulkan driver. + /// + /// This may return [`None`] for a few reasons: + /// * The Vulkan implementation is not at least 1.2 + /// * If the Vulkan implementation is not at least Vulkan 1.2, the `VK_KHR_driver_properties` device + /// extension is not available. + pub fn driver(&self) -> Option<&DriverInfo> { + self.info.driver.as_ref() + } + + /// Returns the major and minor numbers of the primary node which corresponds to this physical device's DRM + /// device. + #[cfg(feature = "backend_drm")] + #[instrument(level = "debug", parent = &self.span, skip(self))] + pub fn primary_node(&self) -> Result, UnsupportedProperty> { + let properties_drm = self.info.get_drm_properties()?; + let node = Some(properties_drm) + .filter(|props| props.has_primary == vk::TRUE) + .and_then(|props| { + DrmNode::from_dev_id(libc::makedev(props.primary_major as _, props.primary_minor as _)).ok() + }); + + Ok(node) + } + + /// Returns the major and minor numbers of the render node which corresponds to this physical device's DRM + /// device. + /// + /// Note that not every device has a render node. If there is no render node (this function returns [`None`]) + /// then try to use the primary node. + #[cfg(feature = "backend_drm")] + #[instrument(level = "debug", parent = &self.span, skip(self))] + pub fn render_node(&self) -> Result, UnsupportedProperty> { + let properties_drm = self.info.get_drm_properties()?; + let node = Some(properties_drm) + .filter(|props| props.has_render == vk::TRUE) + .and_then(|props| { + DrmNode::from_dev_id(libc::makedev(props.render_major as _, props.render_minor as _)).ok() + }); + + Ok(node) + } + + /// Get physical device properties. + /// + /// This function is equivalent to calling [`vkGetPhysicalDeviceProperties2`]. + /// + /// # Safety + /// + /// - All valid usage requirements for [`vkGetPhysicalDeviceProperties2`] apply. Read the specification + /// for more information. + /// + /// [`vkGetPhysicalDeviceProperties2`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkGetPhysicalDeviceProperties2.html + pub unsafe fn get_properties(&self, props: &mut vk::PhysicalDeviceProperties2<'_>) { + let instance = self.instance().handle(); + // SAFETY: The caller has guaranteed all valid usage requirements for vkGetPhysicalDeviceProperties2 + // are satisfied. + unsafe { instance.get_physical_device_properties2(self.handle(), props) } + } + + /// Get physical device format properties. + /// + /// This function is equivalent to calling [`vkGetPhysicalDeviceFormatProperties2`]. + /// + /// # Safety + /// + /// - All valid usage requirements for [`vkGetPhysicalDeviceFormatProperties2`] apply. Read the specification + /// for more information. + /// + /// [`vkGetPhysicalDeviceFormatProperties2`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkGetPhysicalDeviceFormatProperties2.html + pub unsafe fn get_format_properties(&self, format: vk::Format, props: &mut vk::FormatProperties2<'_>) { + let instance = self.instance().handle(); + // SAFETY: The caller has guaranteed all valid usage requirements for vkGetPhysicalDeviceFormatProperties2 + // are satisfied. + unsafe { instance.get_physical_device_format_properties2(self.handle(), format, props) } + } + + /// Returns properties for each supported DRM modifier for the specified format. + /// + /// Returns [`Err`] if the `VK_EXT_image_drm_format_modifier` extension is not supported. + #[instrument(level = "debug", parent = &self.span, skip(self))] + pub fn get_format_modifier_properties( + &self, + format: vk::Format, + ) -> Result, UnsupportedProperty> { + if !self.has_device_extension(ext::image_drm_format_modifier::NAME) { + const EXTENSIONS: &[&CStr] = &[ext::image_drm_format_modifier::NAME]; + return Err(UnsupportedProperty::Extensions(EXTENSIONS)); + } + + // First get the number of modifiers the driver supports. + let count = unsafe { + let mut list = vk::DrmFormatModifierPropertiesListEXT::default(); + let mut format_properties2 = vk::FormatProperties2::default().push_next(&mut list); + self.get_format_properties(format, &mut format_properties2); + list.drm_format_modifier_count as usize + }; + + // Allocate the vector to receive the modifiers in. + let mut data = Vec::with_capacity(count); + + unsafe { + let mut list = vk::DrmFormatModifierPropertiesListEXT { + // We cannot use the builder here because the Vec is currently empty, so we need to tell Vulkan + // where to write out the modifier properties and tell it how large the Vec is. + p_drm_format_modifier_properties: data.as_mut_ptr(), + drm_format_modifier_count: count as u32, + ..Default::default() + }; + + let mut format_properties2 = vk::FormatProperties2::default().push_next(&mut list); + self.get_format_properties(format, &mut format_properties2); + // SAFETY: Vulkan just initialized the elements of the vector. + data.set_len(list.drm_format_modifier_count as usize); + } + + Ok(data) + } + + /// Returns the device extensions supported by the physical device. + pub fn device_extensions(&self) -> impl Iterator { + self.extensions.iter().map(CString::as_c_str) + } + + /// Returns `true` if this device supports the specified device extension. + pub fn has_device_extension(&self, extension: &CStr) -> bool { + self.device_extensions().any(|name| name == extension) + } + + /// Returns a handle to the underlying [`vk::PhysicalDevice`]. + /// + /// The handle refers to a specific physical device advertised by the instance. This handle is only valid + /// for the lifetime of the instance. + pub fn handle(&self) -> vk::PhysicalDevice { + self.phd + } + + /// The instance which provided this physical device. + pub fn instance(&self) -> &Instance { + &self.instance + } +} + +impl PartialEq for PhysicalDevice { + #[inline] + fn eq(&self, other: &Self) -> bool { + // Both the physical device handle and instance handle must be the same + self.phd == other.phd && self.instance().handle().handle() == other.instance().handle().handle() + } +} + +// SAFETY: The internal pointers in the PhysicalDevice*Properties are always null and only copies of the +// PhysicalDevice*Properties types are returned. +unsafe impl Send for PhysicalDevice {} +unsafe impl Sync for PhysicalDevice {} + +/// Information about the driver providing a [`PhysicalDevice`]. +#[derive(Debug, Clone)] +pub struct DriverInfo { + /// ID which identifies the driver. + pub id: vk::DriverId, + + /// The name of the driver. + pub name: String, + + /// Information describing the driver. + /// + /// This may include information such as the driver version. + pub info: String, + + /// The Vulkan conformance test this driver is conformant against. + pub conformance: vk::ConformanceVersion, +} + +#[derive(Debug, Clone, Default)] +struct PhdInfo { + api_version: Version, + name: String, + properties: vk::PhysicalDeviceProperties, + features: vk::PhysicalDeviceFeatures, + maintenance_3: vk::PhysicalDeviceMaintenance3Properties<'static>, + id: vk::PhysicalDeviceIDProperties<'static>, + properties_driver: Option>, + /// Information about the DRM device which corresponds to this physical device. + #[cfg_attr(not(feature = "backend_drm"), allow(dead_code))] + properties_drm: Option>, + driver: Option, +} + +fn get_env_or_max_version(max_version: Version) -> Version { + // Consider max version overrides from env + match env::var("SMITHAY_VK_VERSION") { + Ok(version) => { + let overridden_version = match &version[..] { + "1.0" => { + warn!("Smithay does not support Vulkan 1.0, ignoring SMITHAY_VK_VERSION"); + return max_version; + } + "1.1" => Some(Version::VERSION_1_1), + "1.2" => Some(Version::VERSION_1_2), + "1.3" => Some(Version::VERSION_1_3), + _ => None, + }; + + // The env var can only lower the maximum version, not raise it. + if let Some(overridden_version) = overridden_version { + if overridden_version > max_version { + warn!( + "Ignoring SMITHAY_VK_VERSION since the requested max version is higher than the maximum of {}.{}", + max_version.major, max_version.minor + ); + max_version + } else { + overridden_version + } + } else { + warn!("SMITHAY_VK_VERSION was set to an unknown Vulkan version"); + max_version + } + } + + Err(VarError::NotUnicode(_)) => { + warn!("Value of SMITHAY_VK_VERSION is not valid Unicode, ignoring."); + + max_version + } + + Err(VarError::NotPresent) => max_version, + } +} + +unsafe extern "system" fn vulkan_debug_utils_callback( + message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, + message_type: vk::DebugUtilsMessageTypeFlagsEXT, + p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, + span: *mut c_void, +) -> vk::Bool32 { + let _ = std::panic::catch_unwind(|| { + // Get the span from the user data pointer we gave to Vulkan. + // + // The span is allocated on the heap using a box, but we do not want to drop the span, + // so read from the pointer. + let _guard = unsafe { (span as *mut tracing::Span).as_ref() }.unwrap().enter(); + + // VUID-VkDebugUtilsMessengerCallbackDataEXT-pMessage-parameter: Message must be valid UTF-8 with a null + // terminator. + let message = unsafe { CStr::from_ptr((*p_callback_data).p_message) }.to_string_lossy(); + // Message type is in full uppercase since we print the bitflag debug representation. + let ty = format!("{message_type:?}").to_lowercase(); + + match message_severity { + vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => { + trace!(ty, "{message}") + } + vk::DebugUtilsMessageSeverityFlagsEXT::INFO => info!(ty, "{message}"), + vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => warn!(ty, "{message}"), + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => error!(ty, "{message}"), + _ => (), + } + }); + + // Must always return false. + vk::FALSE +} + +#[cfg(test)] +mod tests { + use super::{Instance, PhysicalDevice}; + + fn is_send_sync() {} + + /// Test that both [`Instance`] and [`PhysicalDevice`] are Send and Sync. + #[test] + fn send_sync() { + is_send_sync::(); + is_send_sync::(); + } +} diff --git a/vendor/smithay/src/backend/vulkan/phd.rs b/vendor/smithay/src/backend/vulkan/phd.rs new file mode 100644 index 0000000..40a434e --- /dev/null +++ b/vendor/smithay/src/backend/vulkan/phd.rs @@ -0,0 +1,163 @@ +//! Functions to get physical device information + +use std::ffi::{CStr, CString}; + +use ash::{ext, khr, prelude::VkResult, vk}; +use tracing::info_span; + +use super::{DriverInfo, PhdInfo, UnsupportedProperty, version::Version}; + +impl super::PhysicalDevice { + /// # Safety: + /// + /// The physical device must belong to the specified instance. + pub(super) unsafe fn from_phd( + instance: &super::Instance, + phd: vk::PhysicalDevice, + ) -> VkResult> { + let instance = instance.clone(); + + let extensions = unsafe { instance.handle().enumerate_device_extension_properties(phd) }?; + let extensions = extensions + .iter() + .map(|extension| { + // SAFETY: Vulkan guarantees the device name is valid UTF-8 with a null terminator. + unsafe { CStr::from_ptr(&extension.extension_name as *const _) }.to_owned() + }) + .collect::>(); + + if let Some(info) = + unsafe { PhdInfo::from_phd(instance.handle(), instance.api_version(), phd, &extensions) } + { + let span = info_span!(parent: &instance.0.span, "backend_vulkan_device", name = info.name); + Ok(Some(Self { + phd, + info, + extensions, + instance, + span, + })) + } else { + Ok(None) + } + } +} + +impl super::PhdInfo { + /// Returns [`None`] if the physical device does not support Vulkan 1.1 + /// + /// # Panics + /// + /// - If the instance version is not at least Vulkan 1.1 + /// + /// # Safety + /// + /// - The instance version must be the same version the instance was created with. + /// - The physical device must belong to the specified instance. + unsafe fn from_phd( + instance: &ash::Instance, + instance_version: Version, + phd: vk::PhysicalDevice, + supported_extensions: &[CString], + ) -> Option { + assert!(instance_version >= Version::VERSION_1_1); + + let properties = unsafe { instance.get_physical_device_properties(phd) }; + + // Pick the lower of the instance version and device version to get the actual version of Vulkan that + // can be used with the device. + let api_version = Version::from_raw(u32::min(properties.api_version, instance_version.to_raw())); + + if api_version < Version::VERSION_1_1 { + // Device does not support Vulkan 1.1, so ignore it. + return None; + } + + // SAFETY: Vulkan guarantees the device name is valid UTF-8 with a null terminator. + let name = unsafe { CStr::from_ptr(&properties.device_name as *const _) } + .to_str() + .unwrap() + .to_string(); + + // Initialize the type with the api_version. + let mut info = PhdInfo { + api_version, + name, + ..Default::default() + }; + + let mut properties = vk::PhysicalDeviceProperties2::default(); + + // Maintenance3 and IDProperties are both Core in Vulkan 1.1 + // + // SAFETY: Maintenance3 extension is supported since Vulkan 1.1 + properties = properties + .push_next(&mut info.maintenance_3) + .push_next(&mut info.id); + + // VK_EXT_physical_device_drm + if supported_extensions + .iter() + .any(|name| name.as_c_str() == ext::physical_device_drm::NAME) + { + // SAFETY: The caller has guaranteed the physical device supports VK_EXT_physical_device_drm + let next = info + .properties_drm + .insert(vk::PhysicalDeviceDrmPropertiesEXT::default()); + properties = properties.push_next(next); + } + + // VK_KHR_driver_properties or Vulkan 1.2 + if api_version >= Version::VERSION_1_2 + || supported_extensions + .iter() + .any(|name| name.as_c_str() == khr::driver_properties::NAME) + { + // SAFETY: VK_KHR_driver_properties is supported + let next = info + .properties_driver + .insert(vk::PhysicalDeviceDriverProperties::default()); + properties = properties.push_next(next); + } + + unsafe { instance.get_physical_device_properties2(phd, &mut properties) }; + + info.properties = properties.properties; + // Initialize the driver info + info.driver = info.properties_driver.map(DriverInfo::from_driver_properties); + + Some(info) + } + + #[cfg_attr(not(feature = "backend_drm"), allow(dead_code))] + pub(super) fn get_drm_properties( + &self, + ) -> Result, UnsupportedProperty> { + const EXTENSIONS: &[&CStr] = &[ext::physical_device_drm::NAME]; + self.properties_drm + .ok_or(UnsupportedProperty::Extensions(EXTENSIONS)) + } +} + +impl super::DriverInfo { + fn from_driver_properties(properties: vk::PhysicalDeviceDriverProperties<'_>) -> DriverInfo { + // SAFETY: Vulkan guarantees the driver name is valid UTF-8 with a null terminator. + let name = unsafe { CStr::from_ptr(&properties.driver_name as *const _) } + .to_str() + .unwrap() + .to_string(); + + // SAFETY: Vulkan guarantees the driver info is valid UTF-8 with a null terminator. + let info = unsafe { CStr::from_ptr(&properties.driver_info as *const _) } + .to_str() + .unwrap() + .to_string(); + + DriverInfo { + id: properties.driver_id, + name, + info, + conformance: properties.conformance_version, + } + } +} diff --git a/vendor/smithay/src/backend/vulkan/version.rs b/vendor/smithay/src/backend/vulkan/version.rs new file mode 100644 index 0000000..d98b3c9 --- /dev/null +++ b/vendor/smithay/src/backend/vulkan/version.rs @@ -0,0 +1,104 @@ +//! The [`Version`] type. + +use std::{ + cmp::Ordering, + fmt::{self, Formatter}, +}; + +use ash::vk; + +/// A Vulkan API version. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +pub struct Version { + /// The variant of the Vulkan API. + /// + /// Generally this value will be `0` because the Vulkan specification uses variant `0`. + pub variant: u32, + + /// The major version of the Vulkan API. + pub major: u32, + + /// The minor version of the Vulkan API. + pub minor: u32, + + /// The patch version of the Vulkan API. + /// + /// Most Vulkan API calls which take a version typically ignore the patch value. Consumers of the Vulkan API may + /// typically ignore the patch value. + pub patch: u32, +} + +impl Version { + /// Version 1.0 of the Vulkan API. + pub const VERSION_1_0: Version = Version::from_raw(vk::API_VERSION_1_0); + + /// Version 1.1 of the Vulkan API. + pub const VERSION_1_1: Version = Version::from_raw(vk::API_VERSION_1_1); + + /// Version 1.2 of the Vulkan API. + pub const VERSION_1_2: Version = Version::from_raw(vk::API_VERSION_1_2); + + /// Version 1.3 of the Vulkan API. + pub const VERSION_1_3: Version = Version::from_raw(vk::API_VERSION_1_3); + + /// The version of Smithay. + pub const SMITHAY: Version = Version { + // TODO: May be useful to place the version information in a single spot that isn't just Vulkan + variant: 0, + major: 0, + minor: 3, + patch: 0, + }; + + /// Converts a packed version into a version struct. + pub const fn from_raw(raw: u32) -> Version { + Version { + variant: vk::api_version_variant(raw), + major: vk::api_version_major(raw), + minor: vk::api_version_minor(raw), + patch: vk::api_version_patch(raw), + } + } + + /// Converts a version struct into a packed version. + pub const fn to_raw(self) -> u32 { + vk::make_api_version(self.variant, self.major, self.minor, self.patch) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{}.{}.{} variant {}", + self.major, self.minor, self.patch, self.variant + ) + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + match self.variant.cmp(&other.variant) { + Ordering::Equal => {} + ord => return ord, + } + + match self.major.cmp(&other.major) { + Ordering::Equal => {} + ord => return ord, + } + + match self.minor.cmp(&other.minor) { + Ordering::Equal => {} + ord => return ord, + } + + self.patch.cmp(&other.patch) + } +} diff --git a/vendor/smithay/src/backend/winit/input.rs b/vendor/smithay/src/backend/winit/input.rs new file mode 100644 index 0000000..c1a43c0 --- /dev/null +++ b/vendor/smithay/src/backend/winit/input.rs @@ -0,0 +1,557 @@ +use std::path::PathBuf; + +use winit::{ + dpi::PhysicalPosition, + event::{ElementState, MouseButton as WinitMouseButton, MouseScrollDelta}, +}; + +use crate::backend::input::{ + self, AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device, + DeviceCapability, Event, GestureBeginEvent, GestureEndEvent, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, InputBackend, InputTime, KeyState, + KeyboardKeyEvent, Keycode, PointerAxisEvent, PointerButtonEvent, PointerMotionAbsoluteEvent, + TouchCancelEvent, TouchDownEvent, TouchEvent, TouchMotionEvent, TouchSlot, TouchUpEvent, UnusedEvent, +}; + +/// Marker used to define the `InputBackend` types for the winit backend. +#[derive(Debug)] +pub struct WinitInput; + +/// Virtual input device used by the backend to associate input events +#[derive(PartialEq, Eq, Hash, Debug)] +pub struct WinitVirtualDevice; + +impl Device for WinitVirtualDevice { + fn id(&self) -> String { + String::from("winit") + } + + fn name(&self) -> String { + String::from("winit virtual input") + } + + fn has_capability(&self, capability: DeviceCapability) -> bool { + matches!( + capability, + DeviceCapability::Keyboard + | DeviceCapability::Pointer + | DeviceCapability::Touch + | DeviceCapability::Gesture + ) + } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option { + None + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`KeyboardKeyEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitKeyboardInputEvent { + pub(crate) time: InputTime, + pub(crate) key: u32, + pub(crate) count: u32, + pub(crate) state: ElementState, +} + +impl Event for WinitKeyboardInputEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl KeyboardKeyEvent for WinitKeyboardInputEvent { + fn key_code(&self) -> Keycode { + (self.key + 8).into() + } + + fn state(&self) -> KeyState { + self.state.into() + } + + fn count(&self) -> u32 { + self.count + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`PointerMotionAbsoluteEvent`] +#[derive(Debug, Clone)] +pub struct WinitMouseMovedEvent { + pub(crate) time: InputTime, + pub(crate) position: RelativePosition, + pub(crate) global_position: PhysicalPosition, +} + +impl Event for WinitMouseMovedEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl PointerMotionAbsoluteEvent for WinitMouseMovedEvent {} +impl AbsolutePositionEvent for WinitMouseMovedEvent { + fn x(&self) -> f64 { + self.global_position.x + } + + fn y(&self) -> f64 { + self.global_position.y + } + + fn x_transformed(&self, width: i32) -> f64 { + f64::max(self.position.x * width as f64, 0.0) + } + + fn y_transformed(&self, height: i32) -> f64 { + f64::max(self.position.y * height as f64, 0.0) + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`PointerAxisEvent`] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct WinitMouseWheelEvent { + pub(crate) time: InputTime, + pub(crate) delta: MouseScrollDelta, +} + +impl Event for WinitMouseWheelEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl PointerAxisEvent for WinitMouseWheelEvent { + fn source(&self) -> AxisSource { + match self.delta { + MouseScrollDelta::LineDelta(_, _) => AxisSource::Wheel, + MouseScrollDelta::PixelDelta(_) => AxisSource::Continuous, + _ => AxisSource::Continuous, + } + } + + fn amount(&self, axis: Axis) -> Option { + match (axis, self.delta) { + (Axis::Horizontal, MouseScrollDelta::PixelDelta(delta)) => Some(-delta.x), + (Axis::Vertical, MouseScrollDelta::PixelDelta(delta)) => Some(-delta.y), + _ => None, + } + } + + // TODO: Use high-res scroll where backend supports it + fn amount_v120(&self, axis: Axis) -> Option { + match (axis, self.delta) { + (Axis::Horizontal, MouseScrollDelta::LineDelta(x, _)) => Some(-x as f64 * 120.), + (Axis::Vertical, MouseScrollDelta::LineDelta(_, y)) => Some(-y as f64 * 120.), + _ => None, + } + } + + // TODO: Implement with Wayland if `wl_pointer` version >= 9 + fn relative_direction(&self, _axis: Axis) -> AxisRelativeDirection { + AxisRelativeDirection::Identical + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`PointerButtonEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitMouseInputEvent { + pub(crate) time: InputTime, + pub(crate) button: WinitMouseButton, + pub(crate) state: ElementState, + pub(crate) is_x11: bool, +} + +impl Event for WinitMouseInputEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl PointerButtonEvent for WinitMouseInputEvent { + fn button_code(&self) -> u32 { + match self.button { + WinitMouseButton::Left => 0x110, + WinitMouseButton::Right => 0x111, + WinitMouseButton::Middle => 0x112, + WinitMouseButton::Forward => 0x115, + WinitMouseButton::Back => 0x116, + _ => { + if self.is_x11 { + input::xorg_mouse_to_libinput(self.button as u32 + 1) + } else { + self.button as u32 + 1 + } + } + } + } + + fn state(&self) -> ButtonState { + self.state.into() + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`TouchDownEvent`] +#[derive(Debug, Clone)] +pub struct WinitTouchStartedEvent { + pub(crate) time: InputTime, + pub(crate) position: RelativePosition, + pub(crate) global_position: PhysicalPosition, + pub(crate) id: u64, +} + +impl Event for WinitTouchStartedEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl TouchDownEvent for WinitTouchStartedEvent {} + +impl TouchEvent for WinitTouchStartedEvent { + fn slot(&self) -> TouchSlot { + Some(self.id as u32).into() + } +} + +impl AbsolutePositionEvent for WinitTouchStartedEvent { + fn x(&self) -> f64 { + self.global_position.x + } + + fn y(&self) -> f64 { + self.global_position.y + } + + fn x_transformed(&self, width: i32) -> f64 { + f64::max(self.position.x * width as f64, 0.0) + } + + fn y_transformed(&self, height: i32) -> f64 { + f64::max(self.position.y * height as f64, 0.0) + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`TouchMotionEvent`] +#[derive(Debug, Clone)] +pub struct WinitTouchMovedEvent { + pub(crate) time: InputTime, + pub(crate) position: RelativePosition, + pub(crate) global_position: PhysicalPosition, + pub(crate) id: u64, +} + +impl Event for WinitTouchMovedEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl TouchMotionEvent for WinitTouchMovedEvent {} + +impl TouchEvent for WinitTouchMovedEvent { + fn slot(&self) -> TouchSlot { + Some(self.id as u32).into() + } +} + +impl AbsolutePositionEvent for WinitTouchMovedEvent { + fn x(&self) -> f64 { + self.global_position.x + } + + fn y(&self) -> f64 { + self.global_position.y + } + + fn x_transformed(&self, width: i32) -> f64 { + f64::max(self.position.x * width as f64, 0.0) + } + + fn y_transformed(&self, height: i32) -> f64 { + f64::max(self.position.y * height as f64, 0.0) + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`TouchUpEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitTouchEndedEvent { + pub(crate) time: InputTime, + pub(crate) id: u64, +} + +impl Event for WinitTouchEndedEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl TouchUpEvent for WinitTouchEndedEvent {} + +impl TouchEvent for WinitTouchEndedEvent { + fn slot(&self) -> TouchSlot { + Some(self.id as u32).into() + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`TouchCancelEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitTouchCancelledEvent { + pub(crate) time: InputTime, + pub(crate) id: u64, +} + +impl Event for WinitTouchCancelledEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl TouchCancelEvent for WinitTouchCancelledEvent {} + +impl TouchEvent for WinitTouchCancelledEvent { + fn slot(&self) -> TouchSlot { + Some(self.id as u32).into() + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`GesturePinchBeginEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitGesturePinchBeginEvent { + pub(crate) time: InputTime, +} + +impl Event for WinitGesturePinchBeginEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl GestureBeginEvent for WinitGesturePinchBeginEvent { + fn fingers(&self) -> u32 { + // Libinput only reports pinch gestures with 2 fingers, and winit-wayland also filters fingers != 2. + 2 + } +} + +impl GesturePinchBeginEvent for WinitGesturePinchBeginEvent {} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`GesturePinchUpdateEvent`] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct WinitGesturePinchUpdateEvent { + pub(crate) time: InputTime, + pub(crate) scale: f64, + pub(crate) rotation: f64, + pub(crate) delta_x: f64, + pub(crate) delta_y: f64, +} + +impl Event for WinitGesturePinchUpdateEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl GesturePinchUpdateEvent for WinitGesturePinchUpdateEvent { + fn scale(&self) -> f64 { + self.scale + } + + fn delta_x(&self) -> f64 { + self.delta_x + } + + fn delta_y(&self) -> f64 { + self.delta_y + } + + fn rotation(&self) -> f64 { + self.rotation + } +} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`GesturePinchEndEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitGesturePinchEndEvent { + pub(crate) time: InputTime, + pub(crate) cancelled: bool, +} + +impl Event for WinitGesturePinchEndEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl GestureEndEvent for WinitGesturePinchEndEvent { + fn cancelled(&self) -> bool { + self.cancelled + } +} + +impl GesturePinchEndEvent for WinitGesturePinchEndEvent {} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`GestureHoldBeginEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitGestureHoldBeginEvent { + pub(crate) time: InputTime, +} + +impl Event for WinitGestureHoldBeginEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl GestureBeginEvent for WinitGestureHoldBeginEvent { + fn fingers(&self) -> u32 { + // winit's HoldGesture carries no finger count; winit-wayland drops 1-finger holds, so every hold observed here has ≥ 2 fingers. + 2 + } +} + +impl GestureHoldBeginEvent for WinitGestureHoldBeginEvent {} + +/// Winit-Backend internal event wrapping `winit`'s types into a [`GestureHoldEndEvent`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WinitGestureHoldEndEvent { + pub(crate) time: InputTime, + pub(crate) cancelled: bool, +} + +impl Event for WinitGestureHoldEndEvent { + fn time(&self) -> InputTime { + self.time + } + + fn device(&self) -> WinitVirtualDevice { + WinitVirtualDevice + } +} + +impl GestureEndEvent for WinitGestureHoldEndEvent { + fn cancelled(&self) -> bool { + self.cancelled + } +} + +impl GestureHoldEndEvent for WinitGestureHoldEndEvent {} + +impl From for KeyState { + #[inline] + fn from(state: ElementState) -> Self { + match state { + ElementState::Pressed => KeyState::Pressed, + ElementState::Released => KeyState::Released, + } + } +} + +impl From for ButtonState { + #[inline] + fn from(state: ElementState) -> Self { + match state { + ElementState::Pressed => ButtonState::Pressed, + ElementState::Released => ButtonState::Released, + } + } +} + +/// Position relative to the source window, so each coordinate lays inside +/// the range from [0;1]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct RelativePosition { + /// Position of the `x` relative to the window. + x: f64, + /// Position of the `y` relative to the window. + y: f64, +} + +impl RelativePosition { + pub(crate) fn new(x: f64, y: f64) -> Self { + Self { x, y } + } +} + +impl InputBackend for WinitInput { + type Device = WinitVirtualDevice; + type KeyboardKeyEvent = WinitKeyboardInputEvent; + type PointerAxisEvent = WinitMouseWheelEvent; + type PointerButtonEvent = WinitMouseInputEvent; + type PointerMotionEvent = UnusedEvent; + type PointerMotionAbsoluteEvent = WinitMouseMovedEvent; + + type GestureSwipeBeginEvent = UnusedEvent; + type GestureSwipeUpdateEvent = UnusedEvent; + type GestureSwipeEndEvent = UnusedEvent; + type GesturePinchBeginEvent = WinitGesturePinchBeginEvent; + type GesturePinchUpdateEvent = WinitGesturePinchUpdateEvent; + type GesturePinchEndEvent = WinitGesturePinchEndEvent; + type GestureHoldBeginEvent = WinitGestureHoldBeginEvent; + type GestureHoldEndEvent = WinitGestureHoldEndEvent; + + type TouchDownEvent = WinitTouchStartedEvent; + type TouchUpEvent = WinitTouchEndedEvent; + type TouchMotionEvent = WinitTouchMovedEvent; + type TouchCancelEvent = WinitTouchCancelledEvent; + type TouchFrameEvent = UnusedEvent; + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; + + type SwitchToggleEvent = UnusedEvent; + + type SpecialEvent = UnusedEvent; +} diff --git a/vendor/smithay/src/backend/winit/mod.rs b/vendor/smithay/src/backend/winit/mod.rs new file mode 100644 index 0000000..45dd05a --- /dev/null +++ b/vendor/smithay/src/backend/winit/mod.rs @@ -0,0 +1,877 @@ +//! Implementation of backend traits for types provided by `winit` +//! +//! This module provides the appropriate implementations of the backend +//! interfaces for running a compositor as a Wayland or X11 client using [`winit`]. +//! +//! ## Usage +//! +//! The backend is initialized using one of the [`init`], [`init_from_attributes`] or +//! [`init_from_attributes_with_gl_attr`] functions, depending on the amount of control +//! you want on the initialization of the backend. These functions will provide you +//! with two objects: +//! +//! - a [`WinitGraphicsBackend`], which can give you an implementation of a [`Renderer`](crate::backend::renderer::Renderer) +//! (or even [`GlesRenderer`]) through its `renderer` method in addition to further +//! functionality to access and manage the created winit-window. +//! - a [`WinitEventLoop`], which dispatches some [`WinitEvent`] from the host graphics server. +//! +//! The other types in this module are the instances of the associated types of these +//! two traits for the winit backend. + +use std::io::Error as IoError; +use std::mem; +use std::sync::Arc; +use std::time::Duration; + +use calloop::generic::Generic; +use calloop::{EventSource, Interest, PostAction, Readiness, Token}; +use tracing::{debug, info, info_span, instrument, trace, warn}; +use wayland_egl as wegl; +use winit::dpi::LogicalPosition; +use winit::event::TouchPhase; +use winit::platform::scancode::PhysicalKeyExtScancode; +use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle}; +use winit::{ + application::ApplicationHandler, + dpi::LogicalSize, + event::{ButtonSource, ElementState, PointerKind, PointerSource, WindowEvent}, + event_loop::{ + ActiveEventLoop, EventLoop, + pump_events::{EventLoopExtPumpEvents, PumpStatus}, + }, + window::{Window as WinitWindow, WindowAttributes, WindowId}, +}; + +use crate::{ + backend::{ + egl::{ + EGLContext, EGLSurface, Error as EGLError, + context::{GlAttributes, PixelFormatRequirements}, + display::EGLDisplay, + native, + }, + input::{InputEvent, InputTime}, + renderer::{ + Bind, + gles::{GlesError, GlesRenderer}, + }, + }, + utils::{Physical, Rectangle, Size}, +}; + +mod input; + +pub use self::input::*; + +/// Create a new [`WinitGraphicsBackend`], which implements the +/// [`Renderer`](crate::backend::renderer::Renderer) trait and a corresponding [`WinitEventLoop`]. +pub fn init() -> Result<(WinitGraphicsBackend, WinitEventLoop), Error> +where + R: From + Bind, + crate::backend::SwapBuffersError: From, +{ + init_from_attributes( + WindowAttributes::default() + .with_surface_size(LogicalSize::new(1280.0, 800.0)) + .with_title("Smithay") + .with_visible(true), + ) +} + +/// Create a new [`WinitGraphicsBackend`], which implements the [`Renderer`](crate::backend::renderer::Renderer) +/// trait, from a given [`WindowAttributes`] struct and a corresponding +/// [`WinitEventLoop`]. +pub fn init_from_attributes( + attributes: WindowAttributes, +) -> Result<(WinitGraphicsBackend, WinitEventLoop), Error> +where + R: From + Bind, + crate::backend::SwapBuffersError: From, +{ + init_from_attributes_with_gl_attr( + attributes, + GlAttributes { + version: (3, 0), + profile: None, + debug: cfg!(debug_assertions), + vsync: false, + }, + ) +} + +/// Create a new [`WinitGraphicsBackend`], which implements the [`Renderer`](crate::backend::renderer::Renderer) +/// trait, from a given [`WindowAttributes`] struct, as well as given +/// [`GlAttributes`] for further customization of the rendering pipeline and a +/// corresponding [`WinitEventLoop`]. +pub fn init_from_attributes_with_gl_attr( + attributes: WindowAttributes, + gl_attributes: GlAttributes, +) -> Result<(WinitGraphicsBackend, WinitEventLoop), Error> +where + R: From + Bind, + crate::backend::SwapBuffersError: From, +{ + let span = info_span!("backend_winit", window = tracing::field::Empty); + let _guard = span.enter(); + info!("Initializing a winit backend"); + + let mut event_loop = EventLoop::builder().build().map_err(Error::EventLoopCreation)?; + + let mut window_event_loop_inner = WinitEventLoopInner { + window_attributes: attributes, + scale_factor: 1.0, + pinch_gesture_state: PinchGestureState::default(), + key_counter: 0, + // Will be initialized after window creation + window: None, + window_create_error: None, + is_x11: false, + }; + let mut initial_events = Vec::new(); + while window_event_loop_inner.window.is_none() { + event_loop.pump_app_events( + None, + &mut WinitEventLoopApp { + inner: &mut window_event_loop_inner, + callback: |evt| initial_events.push(evt), + }, + ); + if let Some(err) = window_event_loop_inner.window_create_error { + return Err(err); + } + } + let window = window_event_loop_inner.window.clone().unwrap(); + + window_event_loop_inner.is_x11 = matches!( + window.window_handle().map(|handle| handle.as_raw()), + Ok(RawWindowHandle::Xlib(_)) + ); + + span.record("window", window.id().into_raw()); + debug!("Window created"); + + let winit_graphics_backend = + WinitGraphicsBackend::new_with_gl_attr(window.clone(), &span, gl_attributes)?; + + drop(_guard); + + event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); + let event_loop = Generic::new(event_loop, Interest::READ, calloop::Mode::Level); + + Ok(( + winit_graphics_backend, + WinitEventLoop { + inner: window_event_loop_inner, + fake_token: None, + event_loop, + initial_events, + pending_events: Vec::new(), + span, + }, + )) +} + +/// Errors thrown by the `winit` backends +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failed to initialize an event loop. + #[error("Failed to initialize an event loop")] + EventLoopCreation(#[from] winit::error::EventLoopError), + /// Failed to initialize a window. + #[error("Failed to initialize a window")] + WindowCreation(#[from] winit::error::RequestError), + #[error("Failed to create a surface for the window")] + /// Surface creation error. + Surface(Box), + /// Context creation is not supported on the current window system + #[error("Context creation is not supported on the current window system")] + NotSupported, + /// EGL error. + #[error("EGL error: {0}")] + Egl(#[from] EGLError), + /// Renderer initialization failed. + #[error("Renderer creation failed: {0}")] + RendererCreationError(#[from] GlesError), +} + +/// Window with an active EGL Context created by `winit`. +#[derive(Debug)] +pub struct WinitGraphicsBackend { + renderer: R, + // The display isn't used past this point but must be kept alive. + _display: EGLDisplay, + egl_surface: EGLSurface, + window: Arc, + damage_tracking: bool, + bind_size: Option>, + span: tracing::Span, +} + +impl WinitGraphicsBackend +where + R: Bind, + crate::backend::SwapBuffersError: From, +{ + fn new_with_gl_attr( + window: Arc, + span: &tracing::Span, + gl_attributes: GlAttributes, + ) -> Result + where + R: From, + { + let (display, context, surface) = { + let display = unsafe { EGLDisplay::new(window.clone())? }; + + let context = + EGLContext::new_with_config(&display, gl_attributes, PixelFormatRequirements::_10_bit()) + .or_else(|_| { + EGLContext::new_with_config( + &display, + gl_attributes, + PixelFormatRequirements::_8_bit(), + ) + })?; + + let surface = match window.window_handle().map(|handle| handle.as_raw()) { + Ok(RawWindowHandle::Wayland(handle)) => { + debug!("Winit backend: Wayland"); + let size = window.surface_size(); + let surface = unsafe { + wegl::WlEglSurface::new_from_raw( + handle.surface.as_ptr() as *mut _, + size.width as i32, + size.height as i32, + ) + } + .map_err(|err| Error::Surface(err.into()))?; + unsafe { + EGLSurface::new( + &display, + context.pixel_format().unwrap(), + context.config_id(), + surface, + ) + .map_err(EGLError::CreationFailed)? + } + } + Ok(RawWindowHandle::Xlib(handle)) => { + debug!("Winit backend: X11"); + unsafe { + EGLSurface::new( + &display, + context.pixel_format().unwrap(), + context.config_id(), + native::XlibWindow(handle.window), + ) + .map_err(EGLError::CreationFailed)? + } + } + _ => panic!("only running on Wayland or with Xlib is supported"), + }; + + let _ = context.unbind(); + (display, context, surface) + }; + + let renderer = unsafe { GlesRenderer::new(context)?.into() }; + let damage_tracking = display.supports_damage(); + + Ok(WinitGraphicsBackend { + window: window.clone(), + span: span.clone(), + _display: display, + egl_surface: surface, + damage_tracking, + bind_size: None, + renderer, + }) + } + + /// Window size of the underlying window + pub fn window_size(&self) -> Size { + let (w, h): (i32, i32) = self.window.surface_size().into(); + (w, h).into() + } + + /// Scale factor of the underlying window. + pub fn scale_factor(&self) -> f64 { + self.window.scale_factor() + } + + /// Reference to the underlying window + pub fn window(&self) -> &dyn WinitWindow { + &*self.window + } + + /// Access the underlying renderer + pub fn renderer(&mut self) -> &mut R { + &mut self.renderer + } + + /// Bind the underlying window to the underlying renderer. + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn bind(&mut self) -> Result<(&mut R, R::Framebuffer<'_>), crate::backend::SwapBuffersError> { + // NOTE: we must resize before making the current context current, otherwise the back + // buffer will be latched. Some nvidia drivers may not like it, but a lot of wayland + // software does the order that way due to mesa latching back buffer on each + // `make_current`. + let window_size = self.window_size(); + if Some(window_size) != self.bind_size { + self.egl_surface.resize(window_size.w, window_size.h, 0, 0); + } + self.bind_size = Some(window_size); + + let fb = self.renderer.bind(&mut self.egl_surface)?; + + Ok((&mut self.renderer, fb)) + } + + /// Retrieve the underlying `EGLSurface` for advanced operations + /// + /// **Note:** Don't carelessly use this to manually bind the renderer to the surface, + /// `WinitGraphicsBackend::bind` transparently handles window resizes for you. + pub fn egl_surface(&self) -> &EGLSurface { + &self.egl_surface + } + + /// Retrieve the buffer age of the current backbuffer of the window. + /// + /// This will only return a meaningful value, if this `WinitGraphicsBackend` + /// is currently bound (by previously calling [`WinitGraphicsBackend::bind`]). + /// + /// Otherwise and on error this function returns `None`. + /// If you are using this value actively e.g. for damage-tracking you should + /// likely interpret an error just as if "0" was returned. + #[instrument(level = "trace", parent = &self.span, skip(self))] + pub fn buffer_age(&self) -> Option { + if self.damage_tracking { + self.egl_surface.buffer_age().map(|x| x as usize) + } else { + Some(0) + } + } + + /// Submits the back buffer to the window by swapping, requires the window to be previously + /// bound (see [`WinitGraphicsBackend::bind`]). + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn submit( + &mut self, + damage: Option<&[Rectangle]>, + ) -> Result<(), crate::backend::SwapBuffersError> { + let mut damage = match damage { + Some(damage) if self.damage_tracking && !damage.is_empty() => { + let bind_size = self + .bind_size + .expect("submitting without ever binding the renderer."); + let damage = damage + .iter() + .map(|rect| { + Rectangle::new( + (rect.loc.x, bind_size.h - rect.loc.y - rect.size.h).into(), + rect.size, + ) + }) + .collect::>(); + Some(damage) + } + _ => None, + }; + + // Request frame callback. + self.window.pre_present_notify(); + self.egl_surface.swap_buffers(damage.as_deref_mut())?; + Ok(()) + } +} + +#[derive(Debug)] +struct WinitEventLoopInner { + window_attributes: WindowAttributes, + window: Option>, + window_create_error: Option, + key_counter: u32, + is_x11: bool, + scale_factor: f64, + pinch_gesture_state: PinchGestureState, +} + +#[derive(Debug)] +struct PinchGestureState { + scale: f64, + rotation: f64, + pan: LogicalPosition, +} + +impl Default for PinchGestureState { + fn default() -> Self { + Self { + scale: 1.0, + rotation: 0.0, + pan: (0., 0.).into(), + } + } +} + +impl PinchGestureState { + fn reset(&mut self) { + self.scale = 1.0; + self.take_pending(); + } + + fn push_rotation(&mut self, delta_ccw_deg: f64) { + self.rotation -= delta_ccw_deg; + } + + fn push_pan(&mut self, delta_pan: LogicalPosition) { + self.pan.x += delta_pan.x; + self.pan.y += delta_pan.y; + } + + fn push_scale(&mut self, delta: f64) { + if delta.is_finite() { + self.scale += delta; + } + } + + fn take_pending(&mut self) -> (f64, LogicalPosition) { + let rotation = mem::take(&mut self.rotation); + let pan = mem::take(&mut self.pan); + + (rotation, pan) + } +} +/// Abstracted event loop of a [`WinitWindow`]. +/// +/// You can register it into `calloop` or call +/// [`dispatch_new_events`](WinitEventLoop::dispatch_new_events) periodically to receive any +/// events. +#[derive(Debug)] +pub struct WinitEventLoop { + inner: WinitEventLoopInner, + fake_token: Option, + initial_events: Vec, + pending_events: Vec, + event_loop: Generic, + span: tracing::Span, +} + +impl WinitEventLoop { + /// Processes new events of the underlying event loop and calls the provided callback. + /// + /// You need to periodically call this function to keep the underlying event loop and + /// [`WinitWindow`] active. Otherwise the window may not respond to user interaction. + /// + /// Returns an error if the [`WinitWindow`] the window has been closed. Calling + /// `dispatch_new_events` again after the [`WinitWindow`] has been closed is considered an + /// application error and unspecified behaviour may occur. + /// + /// The linked [`WinitGraphicsBackend`] will error with a lost context and should + /// not be used anymore as well. + #[instrument(level = "trace", parent = &self.span, skip_all)] + #[profiling::function] + pub fn dispatch_new_events(&mut self, mut callback: F) -> PumpStatus + where + F: FnMut(WinitEvent), + { + // On first call, handle events produced during window creation + for event in std::mem::take(&mut self.initial_events) { + callback(event); + } + + // SAFETY: we don't drop event loop ourselves. + let event_loop = unsafe { self.event_loop.get_mut() }; + + event_loop.pump_app_events( + Some(Duration::ZERO), + &mut WinitEventLoopApp { + inner: &mut self.inner, + callback, + }, + ) + } +} + +struct WinitEventLoopApp<'a, F: FnMut(WinitEvent)> { + inner: &'a mut WinitEventLoopInner, + callback: F, +} + +impl WinitEventLoopApp<'_, F> { + fn timestamp(&self) -> InputTime { + // TODO Get input time from X11 or Wayland? + InputTime::now() + } +} + +impl ApplicationHandler for WinitEventLoopApp<'_, F> { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + if self.inner.window.is_none() { + match event_loop.create_window(self.inner.window_attributes.clone()) { + Ok(window) => self.inner.window = Some(Arc::from(window)), + Err(err) => self.inner.window_create_error = Some(err.into()), + } + + (self.callback)(WinitEvent::Input(InputEvent::DeviceAdded { + device: WinitVirtualDevice, + })); + } + } + + fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) { + let Some(window) = self.inner.window.as_ref() else { + return; + }; + + match event { + WindowEvent::SurfaceResized(size) => { + trace!("Resizing window to {size:?}"); + let (w, h): (i32, i32) = size.into(); + + (self.callback)(WinitEvent::Resized { + size: (w, h).into(), + scale_factor: self.inner.scale_factor, + }); + } + WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + .. + } => { + trace!("Scale factor changed to {new_scale_factor}"); + self.inner.scale_factor = new_scale_factor; + let (w, h): (i32, i32) = window.surface_size().into(); + (self.callback)(WinitEvent::Resized { + size: (w, h).into(), + scale_factor: self.inner.scale_factor, + }); + } + WindowEvent::RedrawRequested => { + (self.callback)(WinitEvent::Redraw); + } + WindowEvent::CloseRequested => { + (self.callback)(WinitEvent::CloseRequested); + } + WindowEvent::Focused(focused) => { + (self.callback)(WinitEvent::Focus(focused)); + } + WindowEvent::KeyboardInput { + event, is_synthetic, .. + } if !is_synthetic && !event.repeat => { + match event.state { + ElementState::Pressed => self.inner.key_counter += 1, + ElementState::Released => { + self.inner.key_counter = self.inner.key_counter.saturating_sub(1); + } + }; + + let scancode = event.physical_key.to_scancode().unwrap_or(0); + let event = InputEvent::Keyboard { + event: WinitKeyboardInputEvent { + time: self.timestamp(), + key: scancode, + count: self.inner.key_counter, + state: event.state, + }, + }; + (self.callback)(WinitEvent::Input(event)); + } + WindowEvent::PointerMoved { position, source, .. } => { + let size = window.surface_size(); + let x = position.x / size.width as f64; + let y = position.y / size.height as f64; + let relative_position = RelativePosition::new(x, y); + + match source { + PointerSource::Mouse => { + let event = InputEvent::PointerMotionAbsolute { + event: WinitMouseMovedEvent { + time: self.timestamp(), + position: relative_position, + global_position: position, + }, + }; + (self.callback)(WinitEvent::Input(event)); + } + PointerSource::Touch { finger_id, force: _ } => { + let event = InputEvent::TouchMotion { + event: WinitTouchMovedEvent { + time: self.timestamp(), + position: relative_position, + global_position: position, + id: finger_id.into_raw() as u64, + }, + }; + (self.callback)(WinitEvent::Input(event)); + } + // TODO Handle tablet events + PointerSource::TabletTool { .. } => {} + _ => (), + } + } + WindowEvent::MouseWheel { delta, .. } => { + let event = InputEvent::PointerAxis { + event: WinitMouseWheelEvent { + time: self.timestamp(), + delta, + }, + }; + (self.callback)(WinitEvent::Input(event)); + } + WindowEvent::PointerButton { + state, + button, + position, + .. + } => { + match button { + ButtonSource::Mouse(button) => { + let event = InputEvent::PointerButton { + event: WinitMouseInputEvent { + time: self.timestamp(), + button, + state, + is_x11: self.inner.is_x11, + }, + }; + (self.callback)(WinitEvent::Input(event)); + } + ButtonSource::Touch { finger_id, force: _ } => match state { + ElementState::Pressed => { + let size = window.surface_size(); + let x = position.x / size.width as f64; + let y = position.y / size.width as f64; + let event = InputEvent::TouchDown { + event: WinitTouchStartedEvent { + time: self.timestamp(), + global_position: position, + position: RelativePosition::new(x, y), + id: finger_id.into_raw() as u64, + }, + }; + + (self.callback)(WinitEvent::Input(event)); + } + ElementState::Released => { + let size = window.surface_size(); + let x = position.x / size.width as f64; + let y = position.y / size.width as f64; + let event = InputEvent::TouchMotion { + event: WinitTouchMovedEvent { + time: self.timestamp(), + position: RelativePosition::new(x, y), + global_position: position, + id: finger_id.into_raw() as u64, + }, + }; + (self.callback)(WinitEvent::Input(event)); + + let event = InputEvent::TouchUp { + event: WinitTouchEndedEvent { + time: self.timestamp(), + id: finger_id.into_raw() as u64, + }, + }; + + (self.callback)(WinitEvent::Input(event)); + } + }, + // TODO Handle tablet events + ButtonSource::TabletTool { .. } => {} + _ => {} + } + } + WindowEvent::PointerLeft { + kind: PointerKind::Touch(finger_id), + .. + } => { + let event = InputEvent::TouchCancel { + event: WinitTouchCancelledEvent { + time: self.timestamp(), + id: finger_id.into_raw() as u64, + }, + }; + (self.callback)(WinitEvent::Input(event)); + } + WindowEvent::RotationGesture { delta, phase, .. } => match phase { + TouchPhase::Started => { + self.inner.pinch_gesture_state.take_pending(); + } + TouchPhase::Moved => self.inner.pinch_gesture_state.push_rotation(delta as f64), + _ => { + self.inner.pinch_gesture_state.take_pending(); + } + }, + WindowEvent::PanGesture { delta, phase, .. } => { + match phase { + TouchPhase::Started => { + self.inner.pinch_gesture_state.take_pending(); + } + TouchPhase::Moved => { + let logical_delta: LogicalPosition = delta.to_logical(self.inner.scale_factor); + self.inner.pinch_gesture_state.push_pan(logical_delta); + } + _ => { + self.inner.pinch_gesture_state.take_pending(); + } + }; + } + WindowEvent::HoldGesture { phase, .. } => { + let event = match phase { + TouchPhase::Started => Some(InputEvent::GestureHoldBegin { + event: WinitGestureHoldBeginEvent { + time: self.timestamp(), + }, + }), + TouchPhase::Moved => None, + TouchPhase::Ended => Some(InputEvent::GestureHoldEnd { + event: WinitGestureHoldEndEvent { + time: self.timestamp(), + cancelled: false, + }, + }), + TouchPhase::Cancelled => Some(InputEvent::GestureHoldEnd { + event: WinitGestureHoldEndEvent { + time: self.timestamp(), + cancelled: true, + }, + }), + }; + + if let Some(event) = event { + (self.callback)(WinitEvent::Input(event)); + } + } + WindowEvent::PinchGesture { delta, phase, .. } => { + let event = match phase { + TouchPhase::Started => { + self.inner.pinch_gesture_state.reset(); + InputEvent::GesturePinchBegin { + event: WinitGesturePinchBeginEvent { + time: self.timestamp(), + }, + } + } + TouchPhase::Moved => { + self.inner.pinch_gesture_state.push_scale(delta); + let (rotation, pan) = self.inner.pinch_gesture_state.take_pending(); + InputEvent::GesturePinchUpdate { + event: WinitGesturePinchUpdateEvent { + time: self.timestamp(), + scale: self.inner.pinch_gesture_state.scale, + rotation, + delta_x: pan.x, + delta_y: pan.y, + }, + } + } + TouchPhase::Ended => InputEvent::GesturePinchEnd { + event: WinitGesturePinchEndEvent { + time: self.timestamp(), + cancelled: false, + }, + }, + TouchPhase::Cancelled => InputEvent::GesturePinchEnd { + event: WinitGesturePinchEndEvent { + time: self.timestamp(), + cancelled: true, + }, + }, + }; + (self.callback)(WinitEvent::Input(event)); + } + _ => (), + } + } +} + +impl EventSource for WinitEventLoop { + type Event = WinitEvent; + type Metadata = (); + type Ret = (); + type Error = IoError; + + const NEEDS_EXTRA_LIFECYCLE_EVENTS: bool = true; + + fn before_sleep(&mut self) -> calloop::Result> { + let mut pending_events = std::mem::take(&mut self.pending_events); + let callback = |event| { + pending_events.push(event); + }; + // NOTE: drain winit's event loop before going to sleep, so we can + // wake up if other thread has woken up underlying winit loop, like other + // event queue got dispatched during that. + self.dispatch_new_events(callback); + self.pending_events = pending_events; + if self.pending_events.is_empty() { + Ok(None) + } else { + Ok(Some((Readiness::EMPTY, self.fake_token.unwrap()))) + } + } + + fn process_events( + &mut self, + _readiness: calloop::Readiness, + _token: calloop::Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let mut callback = |event| callback(event, &mut ()); + for event in self.pending_events.drain(..) { + callback(event); + } + Ok(match self.dispatch_new_events(callback) { + PumpStatus::Continue => PostAction::Continue, + PumpStatus::Exit(_) => PostAction::Remove, + }) + } + + fn register( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.fake_token = Some(token_factory.token()); + self.event_loop.register(poll, token_factory) + } + + fn reregister( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.event_loop.register(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { + self.event_loop.unregister(poll) + } +} + +/// Specific events generated by Winit +#[derive(Debug)] +pub enum WinitEvent { + /// The window has been resized + Resized { + /// The new physical size (in pixels) + size: Size, + /// The new scale factor + scale_factor: f64, + }, + + /// The focus state of the window changed + Focus(bool), + + /// An input event occurred. + Input(InputEvent), + + /// The user requested to close the window. + CloseRequested, + + /// A redraw was requested + Redraw, +} diff --git a/vendor/smithay/src/backend/x11/buffer.rs b/vendor/smithay/src/backend/x11/buffer.rs new file mode 100644 index 0000000..3788c94 --- /dev/null +++ b/vendor/smithay/src/backend/x11/buffer.rs @@ -0,0 +1,188 @@ +//! Utilities for importing buffers into X11. +//! +//! Buffers imported into X11 are represented as X pixmaps which are then presented to the window. +//! +//! At the moment only [`Dmabuf`] backed pixmaps are supported. +//! +//! ## Dmabuf pixmaps +//! +//! A [`Dmabuf`] backed pixmap is created using the [`DRI3`](x11rb::protocol::dri3) extension of +//! the X server. One of two code paths is used here. For more modern DRI3 (>= 1.2) implementations +//! multi-plane Dmabufs, may be used to create a pixmap. Otherwise the fallback code path +//! (available in >= 1.0) is used to create the pixmap. Although the Dmabuf may only have one plane. +//! +//! If you do need to modify any of the logic pertaining to the Dmabuf presentation, do ensure you +//! read the `dri3proto.txt` file (link in the non-public comments of the x11 mod.rs). +//! +//! ## Presentation to the window +//! +//! Presentation to the window is handled through the [`Present`](x11rb::protocol::present) +//! extension of the X server. Because we use direct rendering to present to the window, using +//! V-Sync from OpenGL or the equivalents in other rendering APIs will not work. This is where +//! the utility of the present extension is useful. When using the `present_pixmap` function, +//! the X server will notify when the frame has been presented to the window. The notification +//! of presentation usually occurs on a V-blank. +//! +//! If you do need to modify any of the logic pertaining to the using the present extension, do +//! ensure you read the `presentproto.txt` file (link in the non-public comments of the +//! x11 mod.rs). + +use std::sync::atomic::Ordering; + +use super::{PresentError, Window, X11Error}; +use drm_fourcc::DrmFourcc; +use x11rb::{ + connection::Connection, + protocol::{ + dri3::ConnectionExt as _, + present::{self, ConnectionExt}, + xproto::PixmapWrapper, + }, +}; + +use crate::backend::allocator::{Buffer, dmabuf::Dmabuf}; + +// Shm can be easily supported in the future using, xcb_shm_create_pixmap. + +pub trait PixmapWrapperExt +where + C: Connection, +{ + /// Creates a new Pixmap using the supplied Dmabuf. + /// + /// The returned Pixmap is freed when dropped. + fn with_dmabuf(connection: C, window: &Window, dmabuf: &Dmabuf) -> Result, X11Error>; + + /// Presents the pixmap to the window. + /// + /// The wrapper is consumed when this function is called. The return value will contain the + /// id of the pixmap. + /// + /// The pixmap will be automatically dropped when it bubbles up in the X11 event loop after the + /// X server has finished presentation with the buffer behind the pixmap. + fn present(self, connection: C, window: &Window) -> Result; +} + +impl PixmapWrapperExt for PixmapWrapper +where + C: Connection, +{ + #[profiling::function] + fn with_dmabuf(connection: C, window: &Window, dmabuf: &Dmabuf) -> Result, X11Error> { + if dmabuf.format().code != window.format() { + return Err(PresentError::IncorrectFormat(window.format()).into()); + } + + let mut fds = Vec::new(); + + // XCB closes the file descriptor after sending, so duplicate the file descriptors. + for handle in dmabuf.handles() { + let fd = rustix::io::fcntl_dupfd_cloexec( + handle, 3, // Set to 3 so the fd cannot become stdin, stdout or stderr + ) + .map_err(|e| PresentError::DupFailed(e.to_string()))?; + + fds.push(fd); + } + + // We need dri3 >= 1.2 in order to use the enhanced dri3_pixmap_from_buffers function. + let xid = if window.0.extensions.dri3 >= Some((1, 2)) { + if dmabuf.num_planes() > 4 { + return Err(PresentError::TooManyPlanes.into()); + } + + let xid = connection.generate_id()?; + let mut strides = dmabuf.strides(); + let mut offsets = dmabuf.offsets(); + + connection.dri3_pixmap_from_buffers( + xid, + window.id(), + dmabuf.width() as u16, + dmabuf.height() as u16, + strides.next().unwrap(), // there must be at least one plane and stride. + offsets.next().unwrap(), + // The other planes are optional, so unwrap_or to `NONE` if those planes are not available. + strides.next().unwrap_or(x11rb::NONE), + offsets.next().unwrap_or(x11rb::NONE), + strides.next().unwrap_or(x11rb::NONE), + offsets.next().unwrap_or(x11rb::NONE), + strides.next().unwrap_or(x11rb::NONE), + offsets.next().unwrap_or(x11rb::NONE), + window.depth(), + // In the future this could be made nicer. + match window.format() { + DrmFourcc::Argb8888 => 32, + DrmFourcc::Xrgb8888 => 24, + _ => unreachable!(), + }, + dmabuf.format().modifier.into(), + fds, + )?; + + xid + } else { + // Old codepath can only create a pixmap using one plane from a dmabuf. + if dmabuf.num_planes() != 1 { + return Err(PresentError::TooManyPlanes.into()); + } + + let xid = connection.generate_id()?; + let mut strides = dmabuf.strides(); + let stride = strides.next().unwrap(); + + connection.dri3_pixmap_from_buffer( + xid, + window.id(), + dmabuf.height() * stride, + dmabuf.width() as u16, + dmabuf.height() as u16, + stride as u16, + window.depth(), + // In the future this could be made nicer. + match window.format() { + DrmFourcc::Argb8888 => 32, + DrmFourcc::Xrgb8888 => 24, + _ => unreachable!(), + }, + fds.remove(0), + )?; + + xid + }; + + Ok(PixmapWrapper::for_pixmap(connection, xid)) + } + + #[profiling::function] + fn present(self, connection: C, window: &Window) -> Result { + let next_serial = window.0.next_serial.fetch_add(1, Ordering::SeqCst); + // We want to present as soon as possible, so wait 1ms so the X server will present when next convenient. + let msc = window.0.last_msc.load(Ordering::SeqCst) + 1; + + // options parameter does not take the enum but a u32. + const OPTIONS: present::Option = present::Option::NONE; + + connection.present_pixmap( + window.id(), + self.pixmap(), + next_serial, + x11rb::NONE, // Update the entire window + x11rb::NONE, // Update the entire window + 0, // No offsets + 0, + x11rb::NONE, // Let the X server pick the most suitable crtc + x11rb::NONE, // Do not wait to present + x11rb::NONE, // We will wait for the X server to tell us when it is done with the pixmap. + OPTIONS.into(), // No special presentation options. + msc, + 0, + 0, + &[], // We don't need to notify any other windows. + )?; + + // Pixmaps are reference counted on the X server. Because of reference counting we may + // drop the wrapper and the X server will free the pixmap when presentation has completed. + Ok(self.pixmap()) + } +} diff --git a/vendor/smithay/src/backend/x11/error.rs b/vendor/smithay/src/backend/x11/error.rs new file mode 100644 index 0000000..56f9a59 --- /dev/null +++ b/vendor/smithay/src/backend/x11/error.rs @@ -0,0 +1,154 @@ +use std::io; + +use rustix::io::Errno; +use x11rb::rust_connection::{ConnectError, ConnectionError, ReplyError, ReplyOrIdError}; + +use crate::backend::{allocator::dmabuf::AnyError, drm::CreateDrmNodeError}; + +use super::PresentError; + +/// An error emitted by the X11 backend during setup. +#[derive(Debug, thiserror::Error)] +pub enum X11Error { + /// Connecting to the X server failed. + #[error("Connecting to the X server failed")] + ConnectionFailed(#[from] ConnectError), + + /// Connection to X server was lost. + #[error("Connection to the X server was lost")] + ConnectionLost, + + /// A required X11 extension was not present or has the right version. + #[error("{0}")] + MissingExtension(#[from] MissingExtensionError), + + /// Some protocol error occurred during setup. + #[error("Some protocol error occurred during setup")] + Protocol(#[from] ReplyOrIdError), + + /// Creating the window failed. + #[error("Creating the window failed")] + CreateWindow(#[from] CreateWindowError), + + /// An X11 surface already exists for this window. + #[error("An X11 surface already exists for this window")] + SurfaceExists, + + /// An invalid window was used to create an X11 surface. + /// + /// This error will be risen if the window was destroyed or the window does not belong to the [`X11Handle`](super::X11Handle) + /// in use. + #[error("An invalid window was used to create an X11 surface")] + InvalidWindow, + + /// The X server is not capable of direct rendering. + #[error("The X server is not capable of direct rendering")] + CannotDirectRender, + + /// Failed to allocate buffers needed to present to the window. + #[error("Failed to allocate buffers needed to present to the window")] + Allocation(#[from] AllocateBuffersError), + + /// Error while presenting to a window. + #[error(transparent)] + Present(#[from] PresentError), +} + +impl From for X11Error { + #[inline] + fn from(err: ReplyError) -> Self { + Self::Protocol(err.into()) + } +} + +impl From for X11Error { + #[inline] + fn from(err: ConnectionError) -> Self { + Self::Protocol(err.into()) + } +} + +/// An error that occurs when a required X11 extension is not present. +#[derive(Debug, thiserror::Error)] +pub enum MissingExtensionError { + /// An extension was not found. + #[error("Extension \"{name}\" version {major}.{minor} was not found.")] + NotFound { + /// The name of the required extension. + name: &'static str, + /// The minimum required major version of extension. + major: u32, + /// The minimum required minor version of extension. + minor: u32, + }, + + /// An extension was present, but the version is too low. + #[error( + "Extension \"{name}\" version {required_major}.{required_minor} is required but only version {available_major}.{available_minor} is available." + )] + WrongVersion { + /// The name of the extension. + name: &'static str, + /// The minimum required major version of extension. + required_major: u32, + /// The minimum required minor version of extension. + required_minor: u32, + /// The major version of the extension available on the X server. + available_major: u32, + /// The minor version of the extension available on the X server. + available_minor: u32, + }, +} + +/// An error which may occur when creating an X11 window. +#[derive(Debug, thiserror::Error)] +pub enum CreateWindowError { + /// No depth fulfilling the pixel format requirements was found. + #[error("No depth fulfilling the requirements was found")] + NoDepth, + + /// No visual fulfilling the pixel format requirements was found. + #[error("No visual fulfilling the requirements was found")] + NoVisual, +} + +/// An error which may occur when allocating buffers for presentation to the window. +#[derive(Debug, thiserror::Error)] +pub enum AllocateBuffersError { + /// Failed to open the DRM device to allocate buffers. + #[error("Failed to open the DRM device to allocate buffers.")] + OpenDevice(#[from] io::Error), + + /// The device used to allocate buffers is not the correct drm node type. + #[error("The device used to allocate buffers is not the correct drm node type.")] + UnsupportedDrmNode, + + /// Allocating a new buffer failed + #[error(transparent)] + AllocationError(#[from] AnyError), + + /// No free slots + #[error("No free slots in the swapchain")] + NoFreeSlots, + + /// The window has been destroyed + #[error("The window has been destroyed")] + WindowDestroyed, +} + +impl From for AllocateBuffersError { + #[inline] + fn from(err: Errno) -> Self { + Self::OpenDevice(err.into()) + } +} + +impl From for AllocateBuffersError { + #[inline] + fn from(err: CreateDrmNodeError) -> Self { + match err { + CreateDrmNodeError::Io(err) => AllocateBuffersError::OpenDevice(err), + CreateDrmNodeError::NotDrmNode => AllocateBuffersError::UnsupportedDrmNode, + } + } +} diff --git a/vendor/smithay/src/backend/x11/extension.rs b/vendor/smithay/src/backend/x11/extension.rs new file mode 100644 index 0000000..bf6aa43 --- /dev/null +++ b/vendor/smithay/src/backend/x11/extension.rs @@ -0,0 +1,145 @@ +use tracing::{error, info}; + +use super::{MissingExtensionError, X11Error}; + +/// The extension macro. +/// +/// This macro generates a struct which checks for the presence of some X11 extensions and stores +/// the version supplied by the X server. +/// +/// ```ignore(cannot-test-this-because-non-exported-macro) +/// extensions! { +/// // The extension to check for. This should correspond to the name of the extension inside x11rb's `x11rb::protocol::xproto::` module path. +/// xfixes { +/// // The function used to query the available version of the extension. This will be inside the module path as explained above +/// xfixes_query_version, +/// // The minimum version of the extension that will be accepted. +/// minimum: (4, 0), +/// // The version of the extension to request. +/// request: (4, 0), +/// }, +/// } +/// +/// // The extensions may be checked then using the generated `Extensions` struct using the `check_extensions` function. +/// ``` +macro_rules! extensions { + ( + $( + $extension:ident { // Extension name for path lookup + $extension_fn:ident, // Function used to look up the version of the extension + required: $required:expr, + minimum: ($min_major:expr, $min_minor:expr), + request: ($req_major:expr, $req_minor:expr), + }, + )* + ) => { + #[allow(dead_code)] + #[derive(Debug, Copy, Clone)] + pub struct Extensions { + $( + #[doc = concat!(" The version of the `", stringify!($extension), "` extension.")] + pub $extension: Option<(u32, u32)>, + )* + } + + impl Extensions { + pub fn check_extensions(connection: &C) -> Result { + $( + let $extension = { + use x11rb::protocol::$extension::{ConnectionExt as _, X11_EXTENSION_NAME}; + + if connection.extension_information(X11_EXTENSION_NAME)?.is_some() { + let version = connection.$extension_fn($req_major, $req_minor)?.reply()?; + let major_version = u32::from(version.major_version); + let minor_version = u32::from(version.minor_version); + + #[allow(unused_comparisons)] // Macro comparisons + if major_version >= $req_major + || (major_version == $req_major && minor_version >= $req_minor) + { + info!( + "Loaded extension {} version {}.{}", + X11_EXTENSION_NAME, + major_version, + minor_version, + ); + + Some((major_version, minor_version)) + } else { + if $required { + error!( + "required extension {} version is too low (have {}.{}, expected {}.{})", + X11_EXTENSION_NAME, + major_version, + minor_version, + $req_major, + $req_minor, + ); + + return Err(MissingExtensionError::WrongVersion { + name: X11_EXTENSION_NAME, + required_major: $req_major, + required_minor: $req_minor, + available_major: major_version, + available_minor: minor_version, + }.into()); + } else { + None + } + } + } else { + if $required { + error!("required extension {} not found", X11_EXTENSION_NAME); + + return Err(MissingExtensionError::NotFound { + name: X11_EXTENSION_NAME, + major: $min_major, + minor: $min_minor, + } + .into()); + } else { + None + } + } + }; + )* + + Ok(Extensions { + $( + $extension, + )* + }) + } + } + }; +} + +extensions! { + present { + present_query_version, + required: true, + minimum: (1, 0), + request: (1, 0), + }, + + xfixes { + xfixes_query_version, + required: true, + minimum: (4, 0), + request: (4, 0), + }, + + dri3 { + dri3_query_version, + required: true, + minimum: (1, 0), + request: (1, 2), + }, + + xinput { + xinput_xi_query_version, + required: true, + minimum: (2, 0), + request: (2, 4), + }, +} diff --git a/vendor/smithay/src/backend/x11/input.rs b/vendor/smithay/src/backend/x11/input.rs new file mode 100644 index 0000000..aeb21c1 --- /dev/null +++ b/vendor/smithay/src/backend/x11/input.rs @@ -0,0 +1,262 @@ +//! Input backend implementation for the X11 backend. + +use super::{Window, WindowTemporary, window_inner::WindowInner}; +use crate::{ + backend::input::{ + self, AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device, + DeviceCapability, InputBackend, InputTime, KeyState, KeyboardKeyEvent, Keycode, PointerAxisEvent, + PointerButtonEvent, PointerMotionAbsoluteEvent, UnusedEvent, + }, + utils::{Logical, Size}, +}; +use std::sync::Weak; + +/// Marker used to define the `InputBackend` types for the X11 backend. +#[derive(Debug)] +pub struct X11Input; + +/// Virtual input device used by the backend to associate input events. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct X11VirtualDevice; + +impl Device for X11VirtualDevice { + fn id(&self) -> String { + "x11".to_owned() + } + + fn name(&self) -> String { + "x11 virtual input".to_owned() + } + + fn has_capability(&self, capability: DeviceCapability) -> bool { + matches!( + capability, + DeviceCapability::Keyboard | DeviceCapability::Pointer | DeviceCapability::Touch + ) + } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option { + None + } +} + +/// X11-Backend internal event wrapping `X11`'s types into a [`KeyboardKeyEvent`]. +#[derive(Debug, Clone)] +pub struct X11KeyboardInputEvent { + pub(crate) time: u32, + pub(crate) key: Keycode, + pub(crate) count: u32, + pub(crate) state: KeyState, + pub(crate) window: Weak, +} + +impl X11KeyboardInputEvent { + /// Returns a temporary reference to the window belonging to this event. + /// + /// Returns None if the window is not alive anymore. + pub fn window(&self) -> Option + '_> { + self.window.upgrade().map(Window).map(WindowTemporary) + } +} + +impl input::Event for X11KeyboardInputEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> X11VirtualDevice { + X11VirtualDevice + } +} + +impl KeyboardKeyEvent for X11KeyboardInputEvent { + fn key_code(&self) -> Keycode { + self.key + } + + fn state(&self) -> KeyState { + self.state + } + + fn count(&self) -> u32 { + self.count + } +} + +/// X11-Backend internal event wrapping `X11`'s types into a [`PointerAxisEvent`] +#[derive(Debug, Clone)] +pub struct X11MouseWheelEvent { + pub(crate) time: u32, + pub(crate) axis: Axis, + pub(crate) amount: f64, + pub(crate) window: Weak, +} + +impl X11MouseWheelEvent { + /// Returns a temporary reference to the window belonging to this event. + /// + /// Returns None if the window is not alive anymore. + pub fn window(&self) -> Option + '_> { + self.window.upgrade().map(Window).map(WindowTemporary) + } +} + +impl input::Event for X11MouseWheelEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> X11VirtualDevice { + X11VirtualDevice + } +} + +impl PointerAxisEvent for X11MouseWheelEvent { + fn amount(&self, _axis: Axis) -> Option { + None + } + + fn amount_v120(&self, axis: Axis) -> Option { + if self.axis == axis { + match axis { + Axis::Vertical => Some(-self.amount * 120.), + Axis::Horizontal => Some(self.amount * 120.), + } + } else { + Some(0.0) + } + } + + fn source(&self) -> AxisSource { + // X11 seems to act within the scope of individual ratchets of a scroll wheel. + AxisSource::Wheel + } + + fn relative_direction(&self, _axis: Axis) -> AxisRelativeDirection { + AxisRelativeDirection::Identical + } +} + +/// X11-Backend internal event wrapping `X11`'s types into a [`PointerButtonEvent`] +#[derive(Debug, Clone)] +pub struct X11MouseInputEvent { + pub(crate) time: u32, + pub(crate) raw: u32, + pub(crate) state: ButtonState, + pub(crate) window: Weak, +} + +impl X11MouseInputEvent { + /// Returns a temporary reference to the window belonging to this event. + /// + /// Returns None if the window is not alive anymore. + pub fn window(&self) -> Option + '_> { + self.window.upgrade().map(Window).map(WindowTemporary) + } +} + +impl input::Event for X11MouseInputEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> X11VirtualDevice { + X11VirtualDevice + } +} + +impl PointerButtonEvent for X11MouseInputEvent { + fn button_code(&self) -> u32 { + input::xorg_mouse_to_libinput(self.raw) + } + + fn state(&self) -> ButtonState { + self.state + } +} + +/// X11-Backend internal event wrapping `X11`'s types into a [`PointerMotionAbsoluteEvent`] +#[derive(Debug, Clone)] +pub struct X11MouseMovedEvent { + pub(crate) time: u32, + pub(crate) x: f64, + pub(crate) y: f64, + pub(crate) size: Size, + pub(crate) window: Weak, +} + +impl X11MouseMovedEvent { + /// Returns a temporary reference to the window belonging to this event. + /// + /// Returns None if the window is not alive anymore. + pub fn window(&self) -> Option + '_> { + self.window.upgrade().map(Window).map(WindowTemporary) + } +} + +impl input::Event for X11MouseMovedEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> X11VirtualDevice { + X11VirtualDevice + } +} + +impl PointerMotionAbsoluteEvent for X11MouseMovedEvent {} +impl AbsolutePositionEvent for X11MouseMovedEvent { + fn x(&self) -> f64 { + self.x + } + + fn y(&self) -> f64 { + self.y + } + + fn x_transformed(&self, width: i32) -> f64 { + f64::max(self.x * width as f64 / self.size.w as f64, 0.0) + } + + fn y_transformed(&self, height: i32) -> f64 { + f64::max(self.y * height as f64 / self.size.h as f64, 0.0) + } +} + +impl InputBackend for X11Input { + type Device = X11VirtualDevice; + type KeyboardKeyEvent = X11KeyboardInputEvent; + type PointerAxisEvent = X11MouseWheelEvent; + type PointerButtonEvent = X11MouseInputEvent; + + type PointerMotionEvent = UnusedEvent; + + type PointerMotionAbsoluteEvent = X11MouseMovedEvent; + + type GestureSwipeBeginEvent = UnusedEvent; + type GestureSwipeUpdateEvent = UnusedEvent; + type GestureSwipeEndEvent = UnusedEvent; + type GesturePinchBeginEvent = UnusedEvent; + type GesturePinchUpdateEvent = UnusedEvent; + type GesturePinchEndEvent = UnusedEvent; + type GestureHoldBeginEvent = UnusedEvent; + type GestureHoldEndEvent = UnusedEvent; + + type TouchDownEvent = UnusedEvent; + type TouchUpEvent = UnusedEvent; + type TouchMotionEvent = UnusedEvent; + type TouchCancelEvent = UnusedEvent; + type TouchFrameEvent = UnusedEvent; + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; + + type SwitchToggleEvent = UnusedEvent; + + type SpecialEvent = UnusedEvent; +} diff --git a/vendor/smithay/src/backend/x11/mod.rs b/vendor/smithay/src/backend/x11/mod.rs new file mode 100644 index 0000000..ada4b16 --- /dev/null +++ b/vendor/smithay/src/backend/x11/mod.rs @@ -0,0 +1,1083 @@ +//! Implementation of the backend types using X11. +//! +//! This backend provides the appropriate backend implementations to run a Wayland compositor as an +//! X11 client. +//! +//! The backend is initialized using [`X11Backend::new`](self::X11Backend::new). +//! +//! ## Example usage +//! +//! ```rust,no_run +//! # use std::{sync::{Arc, Mutex}, error::Error}; +//! # use smithay::backend::x11::{X11Backend, X11Surface, WindowBuilder}; +//! use smithay::backend::allocator::dmabuf::DmabufAllocator; +//! use smithay::backend::allocator::gbm::{GbmAllocator, GbmDevice, GbmBufferFlags}; +//! use smithay::backend::egl::{EGLDisplay, EGLContext}; +//! use smithay::utils::DeviceFd; +//! use std::collections::HashSet; +//! +//! # struct CompositorState; +//! fn init_x11_backend( +//! handle: calloop::LoopHandle, +//! ) -> Result<(), Box> { +//! // Create the backend +//! let backend = X11Backend::new()?; +//! +//! // Get a handle from the backend to interface with the X server +//! let x_handle = backend.handle(); +//! // Create a window +//! let window = WindowBuilder::new() +//! .title("Wayland inside X11") +//! .build(&x_handle) +//! .expect("Could not create window"); +//! +//! // To render to a window, we need to create an X11 surface. +//! +//! // Get the DRM node used by the X server for direct rendering. +//! let (_drm_node, fd) = x_handle.drm_node()?; +//! // Create the gbm device for allocating buffers +//! let device = GbmDevice::new(DeviceFd::from(fd))?; +//! // Initialize EGL to retrieve the support modifier list +//! let egl = unsafe { EGLDisplay::new(device.clone()).expect("Failed to create EGLDisplay") }; +//! let context = EGLContext::new(&egl).expect("Failed to create EGLContext"); +//! let modifiers = context.dmabuf_render_formats().iter().map(|format| format.modifier).collect::>(); +//! +//! // Finally create the X11 surface, you will use this to obtain buffers that will be presented to the +//! // window. +//! let surface = x_handle.create_surface(&window, DmabufAllocator(GbmAllocator::new(device, GbmBufferFlags::RENDERING)), modifiers.into_iter()); +//! +//! // Insert the backend into the event loop to receive events. +//! handle.insert_source(backend, |event, _window, state| { +//! // Process events from the X server that apply to the window. +//! })?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! ## EGL +//! +//! When using [`EGL`](crate::backend::egl), an [`X11Surface`] may be used to create an [`EGLDisplay`]. + +/* +A note for future contributors and maintainers: + +Do take a look at some useful reading in order to understand this backend more deeply: + +DRI3 protocol documentation: https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/blob/master/dri3proto.txt + +Present protocol documentation: https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/blob/master/presentproto.txt +*/ + +mod buffer; +mod error; +#[macro_use] +mod extension; +mod input; +mod surface; +mod window_inner; + +use crate::{ + backend::{ + allocator::{Allocator, Swapchain}, + drm::{CreateDrmNodeError, DrmNode, NodeType}, + egl::{EGLDevice, EGLDisplay, Error as EGLError, native::X11DefaultDisplay}, + input::{Axis, ButtonState, InputEvent, KeyState, Keycode}, + }, + utils::{Logical, Size, x11rb::X11Source}, +}; +use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; +use drm::node::path_to_type; +use drm_fourcc::{DrmFourcc, DrmModifier}; +/* +use nix::{ + fcntl::{self, OFlag}, + sys::stat::Mode, +}; +*/ +use rustix::fs::{Mode, OFlags}; +use std::{ + collections::HashMap, + io, + os::unix::io::OwnedFd, + sync::{ + Arc, Mutex, Weak, + atomic::{AtomicU32, Ordering}, + mpsc, + }, +}; +use tracing::{debug_span, error, info, instrument, warn}; +use x11rb::{ + atom_manager, + connection::Connection, + protocol::{ + self as x11, ErrorKind, + dri3::ConnectionExt as _, + xinput, + xproto::{ColormapAlloc, ConnectionExt, CreateWindowAux, VisualClass, WindowClass, WindowWrapper}, + }, + rust_connection::{ReplyError, RustConnection}, +}; + +use self::{extension::Extensions, window_inner::WindowInner}; + +pub use self::error::*; +pub use self::input::*; +pub use self::surface::*; + +use super::allocator::dmabuf::{AnyError, Dmabuf}; + +/// An event emitted by the X11 backend. +#[derive(Debug)] +pub enum X11Event { + /// The X server has required the compositor to redraw the contents of window. + Refresh { + /// XID of the window + window_id: u32, + }, + + /// The focus state of the window changed. + Focus { + /// The new focus state + focused: bool, + /// The XID of the window + window_id: u32, + }, + + /// An input event occurred. + Input { + /// The received event. + event: InputEvent, + /// The XID of the window. + /// + /// If the XID is None, this means that no specific window was targeted for this, and all + /// of them should react to this change. + window_id: Option, + }, + + /// The window was resized. + Resized { + /// The new size of the window + new_size: Size, + /// XID of the window + window_id: u32, + }, + + /// The last buffer presented to the window has been displayed. + /// + /// When this event is scheduled, the next frame may be rendered. + PresentCompleted { + /// XID of the window + window_id: u32, + }, + + /// The window has received a request to be closed. + CloseRequested { + /// XID of the window + window_id: u32, + }, +} + +/// Represents an active connection to the X to manage events on the Window provided by the backend. +#[derive(Debug)] +pub struct X11Backend { + connection: Arc, + source: X11Source, + inner: Arc>, + span: tracing::Span, +} + +impl X11Backend { + /// Initializes the X11 backend by connecting to the X server. + pub fn new() -> Result { + let span = debug_span!("backend_x11"); + let _guard = span.enter(); + + info!("Connecting to the X server"); + + let (connection, screen_number) = RustConnection::connect(None)?; + let connection = Arc::new(connection); + info!(screen = screen_number, "Connected"); + span.record("screen", screen_number); + + let extensions = Extensions::check_extensions(&*connection)?; + + let screen = &connection.setup().roots[screen_number]; + + let depth = screen + .allowed_depths + .iter() + .find(|depth| depth.depth == 32) // Prefer 32-bit color + .or_else(|| screen.allowed_depths.iter().find(|depth| depth.depth == 24)) // 24-bit fallback for Xrgb8888 + .cloned() + .ok_or(CreateWindowError::NoDepth)?; + + // Next find a visual using the supported depth + let visual_id = depth + .visuals + .iter() + // Ensure the visual is little endian to comply with the format needed with X/ARGB8888 + .filter(|visual| visual.red_mask == 0xff0000) + .find(|visual| visual.class == VisualClass::TRUE_COLOR) + .ok_or(CreateWindowError::NoVisual)? + .visual_id; + + let format = match depth.depth { + 24 => DrmFourcc::Xrgb8888, + 32 => DrmFourcc::Argb8888, + _ => unreachable!(), + }; + info!(?depth, visual_id, %format, "Window parameters selected"); + + // Make a colormap + let colormap = connection.generate_id()?; + connection.create_colormap(ColormapAlloc::NONE, colormap, screen.root, visual_id)?; + + let atoms = Atoms::new(&*connection)?.reply()?; + + // We need to give the X11Source a window we have created, we cannot send the close event to the root + // window (0). To handle this, we will create a window we never map or provide to users to the backend + // can be sent a message for shutdown. + + let close_window = WindowWrapper::create_window( + &*connection, + x11rb::COPY_DEPTH_FROM_PARENT, + screen.root, + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_OUTPUT, + x11rb::COPY_FROM_PARENT, + &CreateWindowAux::new(), + )? + .into_window(); + + let source = X11Source::new(connection.clone(), close_window, atoms._SMITHAY_X11_BACKEND_CLOSE); + + let inner = X11Inner { + connection: connection.clone(), + screen_number, + windows: HashMap::new(), + key_counter: Arc::new(AtomicU32::new(0)), + window_format: format, + extensions, + colormap, + atoms, + depth, + visual_id, + devices: false, + }; + + drop(_guard); + Ok(X11Backend { + connection, + source, + inner: Arc::new(Mutex::new(inner)), + span, + }) + } + + /// Returns a handle to the X11 backend. + pub fn handle(&self) -> X11Handle { + X11Handle { + connection: self.connection.clone(), + inner: self.inner.clone(), + span: self.span.clone(), + } + } +} + +#[derive(Debug, thiserror::Error)] +enum EGLInitError { + #[error(transparent)] + EGL(#[from] EGLError), + #[error(transparent)] + IO(#[from] io::Error), +} + +/// A handle to the X11 backend. +/// +/// This is the primary object used to interface with the backend. +#[derive(Debug)] +pub struct X11Handle { + connection: Arc, + inner: Arc>, + span: tracing::Span, +} + +impl X11Handle { + /// Returns the default screen number of the X server. + pub fn screen(&self) -> usize { + self.inner.lock().unwrap().screen_number + } + + /// Returns the underlying connection to the X server. + pub fn connection(&self) -> Arc { + self.connection.clone() + } + + /// Returns the format of the window. + pub fn format(&self) -> DrmFourcc { + self.inner.lock().unwrap().window_format + } + + /// Returns the DRM node the X server uses for direct rendering. + /// + /// The DRM node may be used to create a [`gbm::Device`] to allocate buffers. + #[instrument(parent = &self.span, skip(self), ret, err)] + pub fn drm_node(&self) -> Result<(DrmNode, OwnedFd), X11Error> { + // Kernel documentation explains why we should prefer the node to be a render node: + // https://kernel.readthedocs.io/en/latest/gpu/drm-uapi.html + // + // > Render nodes solely serve render clients, that is, no modesetting or privileged ioctls + // > can be issued on render nodes. Only non-global rendering commands are allowed. If a + // > driver supports render nodes, it must advertise it via the DRIVER_RENDER DRM driver + // > capability. If not supported, the primary node must be used for render clients together + // > with the legacy drmAuth authentication procedure. + // + // Since giving the X11 backend the ability to do modesetting is a big nono, we try to only + // ever create a gbm device from a render node. + // + // Of course if the DRM device does not support render nodes, no DRIVER_RENDER capability, then + // fall back to the primary node. + + // We cannot fallback on the egl_init method, because there is no way for us to authenticate a primary node. + // dri3 does not work for closed-source drivers, but *may* give us a authenticated fd as a fallback. + // As a result we try to use egl for a cleaner, better supported approach at first and only if that fails use dri3. + let inner = self.inner.lock().unwrap(); + + egl_init(&inner).or_else(|err| { + warn!( + "Failed to init X11 surface via egl, falling back to dri3: {}", + err + ); + dri3_init(&inner) + }) + } + + /// Creates a surface that allocates and presents buffers to the window. + /// + /// This will fail if the window has already been used to create a surface. + #[instrument(parent = &self.span, skip(self, allocator, modifiers))] + pub fn create_surface + 'static>( + &self, + window: &Window, + allocator: A, + modifiers: impl Iterator, + ) -> Result { + let has_resize = { window.0.resize.lock().unwrap().is_some() }; + + if has_resize { + return Err(X11Error::SurfaceExists); + } + + let inner = self.inner.clone(); + let inner_guard = inner.lock().unwrap(); + + // Fail if the window is not managed by this backend or is destroyed + if !inner_guard.windows.contains_key(&window.id()) { + return Err(X11Error::InvalidWindow); + } + + let mut modifiers = modifiers.collect::>(); + // older dri3 versions do only support buffers with one plane. + // we need to make sure, we don't accidentally allocate buffers with more. + if window.0.extensions.dri3 < Some((1, 2)) { + modifiers.retain(|modi| modi == &DrmModifier::Invalid || modi == &DrmModifier::Linear); + } + + let format = window.0.format; + let size = window.size(); + let swapchain = Swapchain::new( + Box::new(allocator) as Box + 'static>, + size.w as u32, + size.h as u32, + format, + modifiers, + ); + + let (sender, recv) = mpsc::channel(); + + { + let mut resize = window.0.resize.lock().unwrap(); + *resize = Some(sender); + } + + Ok(X11Surface { + connection: Arc::downgrade(&inner_guard.connection), + window: Arc::downgrade(&window.0), + swapchain, + format, + width: size.w, + height: size.h, + buffer: None, + resize: recv, + span: self.span.clone(), + }) + } + + /// Get a temporary reference to a window by its XID + pub fn window_ref_from_id(&self, id: u32) -> Option + '_> { + X11Inner::window_ref_from_id(&self.inner, &id) + .and_then(|w| w.upgrade()) + .map(Window) + .map(WindowTemporary) + } +} + +/// Builder used to construct a window. +#[derive(Debug)] +pub struct WindowBuilder<'a> { + name: Option<&'a str>, + size: Option>, +} + +impl<'a> WindowBuilder<'a> { + #[allow(clippy::new_without_default)] + /// Returns a new builder. + pub fn new() -> WindowBuilder<'a> { + WindowBuilder { + name: None, + size: None, + } + } + + /// Sets the title of the window that will be created by the builder. + pub fn title(self, name: &'a str) -> Self { + Self { + name: Some(name), + ..self + } + } + + /// Sets the size of the window that will be created. + /// + /// There is no guarantee the size specified here will be the actual size of the window when it is + /// presented. + pub fn size(self, size: Size) -> Self { + Self { + size: Some(size), + ..self + } + } + + /// Creates a window using the options specified in the builder. + pub fn build(self, handle: &X11Handle) -> Result { + let _guard = handle.span.enter(); + let connection = handle.connection(); + + let inner = &mut *handle.inner.lock().unwrap(); + + let window = Arc::new(WindowInner::new( + Arc::downgrade(&connection), + &connection.setup().roots[inner.screen_number], + self.size.unwrap_or_else(|| (1280, 800).into()), + self.name.unwrap_or("Smithay"), + inner.window_format, + inner.atoms, + inner.depth.clone(), + inner.visual_id, + inner.colormap, + inner.extensions, + )?); + + let downgrade = Arc::downgrade(&window); + inner.windows.insert(window.id, downgrade); + + Ok(Window(window)) + } +} + +/// An X11 window. +/// +/// Dropping an instance of the window will destroy it. +#[derive(Debug)] +pub struct Window(Arc); + +impl Window { + /// Sets the title of the window. + pub fn set_title(&self, title: &str) { + self.0.set_title(title); + } + + /// Maps the window, making it visible. + pub fn map(&self) { + self.0.map(); + } + + /// Unmaps the window, making it invisible. + pub fn unmap(&self) { + self.0.unmap(); + } + + /// Returns the size of this window. + /// + /// If the window has been destroyed, the size is `0 x 0`. + pub fn size(&self) -> Size { + self.0.size() + } + + /// Changes the visibility of the cursor within the confines of the window. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + pub fn set_cursor_visible(&self, visible: bool) { + self.0.set_cursor_visible(visible); + } + + /// Returns the XID of the window. + pub fn id(&self) -> u32 { + self.0.id + } + + /// Returns the depth id of this window. + pub fn depth(&self) -> u8 { + self.0.depth.depth + } + + /// Returns the format expected by the window. + pub fn format(&self) -> DrmFourcc { + self.0.format + } +} + +impl PartialEq for Window { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +struct WindowTemporary(Window); + +impl AsRef for WindowTemporary { + #[inline] + fn as_ref(&self) -> &Window { + &self.0 + } +} + +impl EventSource for X11Backend { + type Event = X11Event; + type Metadata = (); + type Ret = (); + type Error = X11Error; + + #[profiling::function] + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let connection = self.connection.clone(); + let inner = self.inner.clone(); + let _guard = self.span.enter(); + + let post_action = self + .source + .process_events(readiness, token, |event, _| match event { + calloop::channel::Event::Msg(event) => { + X11Inner::process_event(&inner, event, &mut callback); + } + calloop::channel::Event::Closed => {} + }) + .map_err(|_| X11Error::ConnectionLost)?; + + // Flush the connection so changes to the window state during callbacks can be emitted. + let _ = connection.flush(); + + Ok(post_action) + } + + fn register(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.source.register(poll, token_factory) + } + + fn reregister(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.source.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.source.unregister(poll) + } +} + +atom_manager! { + pub(crate) Atoms: AtomCollectionCookie { + WM_PROTOCOLS, + WM_DELETE_WINDOW, + _NET_WM_NAME, + UTF8_STRING, + _SMITHAY_X11_BACKEND_CLOSE, + } +} + +#[derive(Debug)] +pub(crate) struct X11Inner { + connection: Arc, + screen_number: usize, + windows: HashMap>, + key_counter: Arc, + window_format: DrmFourcc, + extensions: Extensions, + colormap: u32, + atoms: Atoms, + depth: x11::xproto::Depth, + visual_id: u32, + devices: bool, +} + +impl X11Inner { + fn window_ref_from_id(inner: &Arc>, id: &u32) -> Option> { + let mut inner = inner.lock().unwrap(); + inner.windows.retain(|_, weak| weak.strong_count() != 0); + inner.windows.get(id).cloned() + } + + #[profiling::function] + fn process_event(inner: &Arc>, event: x11::Event, callback: &mut F) + where + F: FnMut(X11Event, &mut ()), + { + { + let mut inner = inner.lock().unwrap(); + if !inner.windows.is_empty() && !inner.devices { + callback( + Input { + event: InputEvent::DeviceAdded { + device: X11VirtualDevice, + }, + window_id: None, + }, + &mut (), + ); + inner.devices = true; + } else if inner.windows.is_empty() && inner.devices { + callback( + Input { + event: InputEvent::DeviceRemoved { + device: X11VirtualDevice, + }, + window_id: None, + }, + &mut (), + ); + inner.devices = false; + } + } + + use self::X11Event::{Focus, Input}; + + // If X11 is deadlocking somewhere here, make sure you drop your mutex guards. + + match event { + x11::Event::XinputFocusIn(focus_in) => { + callback( + Focus { + focused: true, + window_id: focus_in.event, + }, + &mut (), + ); + } + + x11::Event::XinputFocusOut(focus_out) => { + callback( + Focus { + focused: false, + window_id: focus_out.event, + }, + &mut (), + ); + } + + x11::Event::XinputButtonPress(button_press) => { + if let Some(window) = X11Inner::window_ref_from_id(inner, &button_press.event) { + // X11 decided to associate scroll wheel with a button, 4, 5, 6 and 7 for + // up, down, right and left. For scrolling, a press event is emitted and a + // release is them immediately followed for scrolling. This means we can + // ignore release for scrolling. + + // Ideally we would use `ButtonIndex` from XCB, however it does not cover 6 and 7 + // for horizontal scroll and does not work nicely in match statements, so we + // use magic constants here: + // + // 1 => MouseButton::Left + // 2 => MouseButton::Middle + // 3 => MouseButton::Right + // 4 => Axis::Vertical +1.0 + // 5 => Axis::Vertical -1.0 + // 6 => Axis::Horizontal -1.0 + // 7 => Axis::Horizontal +1.0 + // Others => ?? + + // Scrolling + if button_press.detail >= 4 && button_press.detail <= 7 { + callback( + Input { + event: InputEvent::PointerAxis { + event: X11MouseWheelEvent { + time: button_press.time, + axis: match button_press.detail { + // Up | Down + 4 | 5 => Axis::Vertical, + + // Right | Left + 6 | 7 => Axis::Horizontal, + + _ => unreachable!(), + }, + amount: match button_press.detail { + // Up | Right + 4 | 7 => 1.0, + + // Down | Left + 5 | 6 => -1.0, + + _ => unreachable!(), + }, + window, + }, + }, + window_id: Some(button_press.event), + }, + &mut (), + ) + } else { + callback( + Input { + event: InputEvent::PointerButton { + event: X11MouseInputEvent { + time: button_press.time, + raw: button_press.detail, + state: ButtonState::Pressed, + window, + }, + }, + window_id: Some(button_press.event), + }, + &mut (), + ) + } + } + } + + x11::Event::XinputButtonRelease(button_release) => { + // Ignore release tick because this event is always sent immediately after the press + // tick for scrolling and the backend will dispatch release event automatically during + // the press event. + if button_release.detail >= 4 && button_release.detail <= 7 { + return; + } + + if let Some(window) = X11Inner::window_ref_from_id(inner, &button_release.event) { + callback( + Input { + event: InputEvent::PointerButton { + event: X11MouseInputEvent { + time: button_release.time, + raw: button_release.detail, + state: ButtonState::Released, + window, + }, + }, + window_id: Some(button_release.event), + }, + &mut (), + ); + } + } + + x11::Event::XinputKeyPress(key_press) => { + if key_press.flags.contains(xinput::KeyEventFlags::KEY_REPEAT) { + return; + } + + if let Some(window) = X11Inner::window_ref_from_id(inner, &key_press.event) { + // Do not hold the lock. + let count = { inner.lock().unwrap().key_counter.fetch_add(1, Ordering::SeqCst) + 1 }; + + callback( + Input { + event: InputEvent::Keyboard { + event: X11KeyboardInputEvent { + time: key_press.time, + key: Keycode::from(key_press.detail), + count, + state: KeyState::Pressed, + window, + }, + }, + window_id: Some(key_press.event), + }, + &mut (), + ) + } + } + + x11::Event::XinputKeyRelease(key_release) => { + if let Some(window) = X11Inner::window_ref_from_id(inner, &key_release.event) { + let count = { + let key_counter = inner.lock().unwrap().key_counter.clone(); + + // atomic u32 has no checked_sub, so load and store to do the same. + let mut count = key_counter.load(Ordering::SeqCst); + count = count.saturating_sub(1); + key_counter.store(count, Ordering::SeqCst); + + count + }; + + callback( + Input { + event: InputEvent::Keyboard { + event: X11KeyboardInputEvent { + time: key_release.time, + key: Keycode::from(key_release.detail), + count, + state: KeyState::Released, + window, + }, + }, + window_id: Some(key_release.event), + }, + &mut (), + ); + } + } + + x11::Event::XinputMotion(motion_notify) => { + if let Some(window) = + X11Inner::window_ref_from_id(inner, &motion_notify.event).and_then(|w| w.upgrade()) + { + // Use event_x/y since those are relative the the window receiving events. + let x = fixed_point_to_float(motion_notify.event_x); + let y = fixed_point_to_float(motion_notify.event_y); + + let window_size = { *window.size.lock().unwrap() }; + + callback( + Input { + event: InputEvent::PointerMotionAbsolute { + event: X11MouseMovedEvent { + time: motion_notify.time, + x, + y, + size: window_size, + window: Arc::downgrade(&window), + }, + }, + window_id: Some(motion_notify.event), + }, + &mut (), + ) + } + } + + x11::Event::ConfigureNotify(configure_notify) => { + if let Some(window) = + X11Inner::window_ref_from_id(inner, &configure_notify.window).and_then(|w| w.upgrade()) + { + let previous_size = { *window.size.lock().unwrap() }; + + // Did the size of the window change? + let configure_notify_size: Size = + (configure_notify.width, configure_notify.height).into(); + + if configure_notify_size != previous_size { + // Intentionally drop the lock on the size mutex incase a user + // requests a resize or does something which causes a resize + // inside the callback. + { + let mut resize_guard = window.size.lock().unwrap(); + *resize_guard = configure_notify_size; + } + + (callback)( + X11Event::Resized { + new_size: configure_notify_size, + window_id: configure_notify.window, + }, + &mut (), + ); + + if let Some(resize_sender) = window.resize.lock().unwrap().as_ref() { + let _ = resize_sender.send(configure_notify_size); + } + } + } + } + + x11::Event::XinputEnter(enter_notify) => { + if let Some(window) = + X11Inner::window_ref_from_id(inner, &enter_notify.event).and_then(|w| w.upgrade()) + { + window.cursor_enter(); + } + } + + x11::Event::XinputLeave(leave_notify) => { + if let Some(window) = + X11Inner::window_ref_from_id(inner, &leave_notify.event).and_then(|w| w.upgrade()) + { + window.cursor_leave(); + } + } + + x11::Event::ClientMessage(client_message) => { + if let Some(window) = + X11Inner::window_ref_from_id(inner, &client_message.window).and_then(|w| w.upgrade()) + { + if client_message.data.as_data32()[0] == window.atoms.WM_DELETE_WINDOW + // Destroy the window? + { + (callback)( + X11Event::CloseRequested { + window_id: client_message.window, + }, + &mut (), + ); + } + } + } + + x11::Event::Expose(expose) => { + if expose.count == 0 { + (callback)( + X11Event::Refresh { + window_id: expose.window, + }, + &mut (), + ); + } + } + + x11::Event::PresentCompleteNotify(complete_notify) => { + if let Some(window) = + X11Inner::window_ref_from_id(inner, &complete_notify.window).and_then(|w| w.upgrade()) + { + window.last_msc.store(complete_notify.msc, Ordering::SeqCst); + + (callback)( + X11Event::PresentCompleted { + window_id: complete_notify.window, + }, + &mut (), + ); + } + } + + x11::Event::PresentIdleNotify(_) => { + // Pixmap is reference counted in the X server, so we do not need to take and drop. + } + + x11::Event::Error(e) => { + error!("X11 protocol error: {:?}", e); + } + + _ => (), + } + } +} + +fn fixed_point_to_float(value: i32) -> f64 { + let int = value >> 16; + let frac = value & 0xffff; + int as f64 + frac as f64 / u16::MAX as f64 +} + +fn egl_init(_: &X11Inner) -> Result<(DrmNode, OwnedFd), EGLInitError> { + let display = unsafe { EGLDisplay::new(X11DefaultDisplay)? }; + let device = EGLDevice::device_for_display(&display)?; + let path = path_to_type(device.drm_device_path()?, NodeType::Render)?; + let node = DrmNode::from_path(&path) + .map_err(|err| match err { + CreateDrmNodeError::Io(err) => err, + _ => unreachable!(), + }) + .map_err(EGLInitError::IO)?; + let fd = rustix::fs::open(&path, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty()) + .map_err(Into::::into) + .map_err(EGLInitError::IO)?; + Ok((node, fd)) +} + +fn dri3_init(x11: &X11Inner) -> Result<(DrmNode, OwnedFd), X11Error> { + let connection = &x11.connection; + + // Determine which drm-device the Display is using. + let screen = &connection.setup().roots[x11.screen_number]; + // provider being NONE tells the X server to use the RandR provider. + let dri3 = match connection.dri3_open(screen.root, x11rb::NONE)?.reply() { + Ok(reply) => reply, + Err(err) => { + return Err(if let ReplyError::X11Error(ref protocol_error) = err { + match protocol_error.error_kind { + // Implementation is risen when the renderer is not capable of X server is not capable + // of rendering at all. + ErrorKind::Implementation => X11Error::CannotDirectRender, + // Match may occur when the node cannot be authenticated for the client. + ErrorKind::Match => X11Error::CannotDirectRender, + _ => err.into(), + } + } else { + err.into() + }); + } + }; + let device_fd = dri3.device_fd; + + let dri_node = DrmNode::from_file(&device_fd).map_err(Into::::into)?; + if dri_node.ty() != NodeType::Render { + // Try to get the render node. + match dri_node.node_with_type(NodeType::Render) { + Some(Ok(node)) => { + match node + .dev_path() + .map(|path| rustix::fs::open(path, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty())) + { + Some(Ok(fd)) => return Ok((node, fd)), + Some(Err(err)) => { + warn!( + "Could not create render node from existing DRM node ({:?}): {}, falling back to primary node", + dri_node.dev_path().as_ref().map(|x| x.display()), + err + ); + } + None => { + warn!( + "Could not create render node from existing DRM node ({:?}), falling back to primary node", + dri_node.dev_path().as_ref().map(|x| x.display()) + ); + } + } + } + Some(Err(err)) => { + warn!( + "Could not create render node from existing DRM node ({:?}): {}, falling back to primary node", + dri_node.dev_path().as_ref().map(|x| x.display()), + err + ); + } + None => { + warn!( + "No render node available for DRM node ({:?}), falling back to primary node", + dri_node.dev_path().as_ref().map(|x| x.display()) + ); + } + }; + } + + let fd_flags = rustix::io::fcntl_getfd(&device_fd).map_err(AllocateBuffersError::from)?; + + // Enable the close-on-exec flag. + rustix::io::fcntl_setfd(&device_fd, fd_flags | rustix::io::FdFlags::CLOEXEC) + .map_err(AllocateBuffersError::from)?; + + Ok((dri_node, device_fd)) +} diff --git a/vendor/smithay/src/backend/x11/surface.rs b/vendor/smithay/src/backend/x11/surface.rs new file mode 100644 index 0000000..65bac0b --- /dev/null +++ b/vendor/smithay/src/backend/x11/surface.rs @@ -0,0 +1,146 @@ +use std::{ + mem, + sync::{Weak, mpsc::Receiver}, +}; + +use drm_fourcc::DrmFourcc; +use tracing::instrument; +use x11rb::{connection::Connection, protocol::xproto::PixmapWrapper, rust_connection::RustConnection}; + +use crate::{ + backend::{ + allocator::{ + Allocator, Slot, Swapchain, + dmabuf::{AnyError, Dmabuf}, + }, + x11::{AllocateBuffersError, Window, buffer::PixmapWrapperExt, window_inner::WindowInner}, + }, + utils::{Logical, Size}, +}; + +use super::{WindowTemporary, X11Error}; + +/// An error that may occur when presenting. +#[derive(Debug, thiserror::Error)] +pub enum PresentError { + /// The dmabuf being presented has too many planes. + #[error("The Dmabuf had too many planes")] + TooManyPlanes, + + /// Duplicating the dmabuf handles failed. + #[error("Duplicating the file descriptors for the dmabuf handles failed")] + DupFailed(String), + + /// The format dmabuf presented does not match the format of the window. + #[error("Buffer had incorrect format, expected: {0}")] + IncorrectFormat(DrmFourcc), +} + +/// An X11 surface which uses GBM to allocate and present buffers. +#[derive(Debug)] +pub struct X11Surface { + pub(crate) connection: Weak, + pub(crate) window: Weak, + pub(crate) resize: Receiver>, + pub(crate) swapchain: Swapchain + 'static>>, + pub(crate) format: DrmFourcc, + pub(crate) width: u16, + pub(crate) height: u16, + pub(crate) buffer: Option>, + pub(crate) span: tracing::Span, +} + +impl X11Surface { + /// Returns the window the surface presents to. + /// + /// This will return [`None`] if the window has been destroyed. + pub fn window(&self) -> Option + '_> { + self.window.upgrade().map(Window).map(WindowTemporary) + } + + /// Returns the format of the buffers the surface accepts. + pub fn format(&self) -> DrmFourcc { + self.format + } + + /// Returns the next buffer that will be presented to the Window and its age. + /// + /// You may bind this buffer to a renderer to render. + /// This function will return the same buffer until [`submit`](Self::submit) is called + /// or [`reset_buffers`](Self::reset_buffers) is used to reset the buffers. + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn buffer(&mut self) -> Result<(Dmabuf, u8), AllocateBuffersError> { + if let Some(new_size) = self.resize.try_iter().last() { + self.resize(new_size); + } + + if self.buffer.is_none() { + self.buffer = Some( + self.swapchain + .acquire() + .map_err(Into::::into)? + .ok_or(AllocateBuffersError::NoFreeSlots)?, + ); + } + + let slot = self.buffer.as_ref().unwrap(); + let age = slot.age(); + Ok(((*slot).clone(), age)) + } + + /// Consume and submit the buffer to the window. + #[instrument(level = "trace", parent = &self.span, skip(self))] + #[profiling::function] + pub fn submit(&mut self) -> Result<(), X11Error> { + if let Some(connection) = self.connection.upgrade() { + // Get a new buffer + let mut next = self + .swapchain + .acquire() + .map_err(Into::::into)? + .ok_or(AllocateBuffersError::NoFreeSlots)?; + + // Swap the buffers + if let Some(current) = self.buffer.as_mut() { + mem::swap(&mut next, current); + } + + { + let window = self.window().ok_or(AllocateBuffersError::WindowDestroyed)?; + let pixmap = PixmapWrapper::with_dmabuf(&*connection, window.as_ref(), &next)?; + + // Now present the current buffer + let _ = pixmap.present(&*connection, window.as_ref())?; + } + self.swapchain.submitted(&next); + + // Flush the connection after presenting to the window to ensure we don't run out of buffer space in the X11 connection. + let _ = connection.flush(); + } + Ok(()) + } + + /// Resets the internal buffers. + #[instrument(level = "trace", parent = &self.span, skip(self))] + pub fn reset_buffers(&mut self) { + self.swapchain.reset_buffers(); + self.buffer = None; + } + + /// Reset the age for all internal buffers. + /// + /// This can be used to clear the damage history. + #[instrument(level = "trace", parent = &self.span, skip(self))] + pub fn reset_buffer_ages(&mut self) { + self.swapchain.reset_buffer_ages(); + } + + fn resize(&mut self, size: Size) { + self.swapchain.resize(size.w as u32, size.h as u32); + self.buffer = None; + + self.width = size.w; + self.height = size.h; + } +} diff --git a/vendor/smithay/src/backend/x11/window_inner.rs b/vendor/smithay/src/backend/x11/window_inner.rs new file mode 100644 index 0000000..18ac17d --- /dev/null +++ b/vendor/smithay/src/backend/x11/window_inner.rs @@ -0,0 +1,316 @@ +/** +A note for future contributors and maintainers: + +When editing this file, grab the nearest copy of the ICCCM. Following the ICCCM is paramount to +X11 clients behaving properly and preventing scenarios such as windows not being resized in tiling +window managers. + +Pay particular attention to "Section 4: Client to Window Manager Communication" + +A link to the ICCCM Section 4: https://tronche.com/gui/x/icccm/sec-4.html +*/ +use crate::utils::{Logical, Size}; + +use super::{Atoms, Window, X11Error, extension::Extensions}; +use drm_fourcc::DrmFourcc; +use portable_atomic::{AtomicU32, AtomicU64}; +use std::sync::{Arc, Mutex, Weak, mpsc::Sender}; +use x11rb::{ + connection::Connection, + protocol::{ + present::{self, ConnectionExt as _}, + xfixes::ConnectionExt as _, + xinput::{self, ConnectionExt as _}, + xproto::{ + self as x11, AtomEnum, ConnectionExt, CreateWindowAux, Depth, EventMask, PropMode, Screen, + UnmapNotifyEvent, WindowClass, + }, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, +}; + +impl From> for Window { + #[inline] + fn from(inner: Arc) -> Self { + Window(inner) + } +} + +#[derive(Debug)] +pub struct CursorState { + pub inside_window: bool, + pub visible: bool, +} + +impl Default for CursorState { + #[inline] + fn default() -> Self { + CursorState { + inside_window: false, + visible: true, + } + } +} + +#[derive(Debug)] +pub(crate) struct WindowInner { + pub connection: Weak, + pub id: x11::Window, + root: x11::Window, + pub atoms: Atoms, + pub cursor_state: Arc>, + pub size: Mutex>, + /// Channel used to send resize notifications to the surface that presents to this window. + /// + /// This value will be [`None`] if no surface is bound to the window. + pub resize: Mutex>>>, + pub next_serial: AtomicU32, + pub last_msc: Arc, + pub format: DrmFourcc, + pub depth: Depth, + pub extensions: Extensions, +} + +impl WindowInner { + #[allow(clippy::too_many_arguments)] + pub fn new( + connection: Weak, + screen: &Screen, + size: Size, + title: &str, + format: DrmFourcc, + atoms: Atoms, + depth: Depth, + visual_id: u32, + colormap: u32, + extensions: Extensions, + ) -> Result { + let weak = connection; + let connection = weak.upgrade().unwrap(); + + // Generate the xid for the window + let window = connection.generate_id()?; + + // The event mask never include `EventMask::RESIZE_REDIRECT`. + // + // The reason is twofold: + // - We are not a window manager + // - Makes our window impossible to resize. + // + // On the resizing aspect, KWin and some other WMs would allow resizing, but those + // compositors rely on putting this window in another window for drawing decorations, + // so visibly in KWin it would look like using the RESIZE_REDIRECT event mask would work, + // but a tiling window manager would be sad and the tiling window manager devs mad because + // this window would refuse to listen to the tiling WM. + // + // For resizing we use ConfigureNotify events from the STRUCTURE_NOTIFY event mask. + + let window_aux = CreateWindowAux::new() + .event_mask( + EventMask::EXPOSURE // Be told when the window is exposed + | EventMask::STRUCTURE_NOTIFY + | EventMask::EXPOSURE + | EventMask::NO_EVENT, + ) + // Border pixel and color map need to be set if our depth may differ from the root depth. + .border_pixel(screen.black_pixel) + .colormap(colormap); + + let _ = connection.create_window( + depth.depth, + window, + screen.root, + 0, + 0, + size.w, + size.h, + 0, + WindowClass::INPUT_OUTPUT, + visual_id, + &window_aux, + )?; + + connection.xinput_xi_select_events( + window, + &[xinput::EventMask { + deviceid: 1, // AllMasterDevices + mask: vec![ + xinput::XIEventMask::KEY_PRESS + | xinput::XIEventMask::KEY_RELEASE + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::MOTION + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT, + ], + }], + )?; + + // We only ever need one event id since we will only ever have one event context. + let present_event_id = connection.generate_id()?; + connection.present_select_input( + present_event_id, + window, + present::EventMask::COMPLETE_NOTIFY | present::EventMask::IDLE_NOTIFY, + )?; + + // Send requests to change window properties while we wait for the window creation request to complete. + let window = WindowInner { + connection: weak, + id: window, + root: screen.root, + atoms, + cursor_state: Arc::new(Mutex::new(CursorState::default())), + size: Mutex::new(size), + next_serial: AtomicU32::new(0), + last_msc: Arc::new(AtomicU64::new(0)), + format, + depth, + extensions, + resize: Mutex::new(None), + }; + + // Enable WM_DELETE_WINDOW so our client is not disconnected upon our toplevel window being destroyed. + connection.change_property32( + PropMode::REPLACE, + window.id, + atoms.WM_PROTOCOLS, + AtomEnum::ATOM, + &[atoms.WM_DELETE_WINDOW], + )?; + + // WM class cannot be safely changed later. + let _ = connection.change_property8( + PropMode::REPLACE, + window.id, + AtomEnum::WM_CLASS, + AtomEnum::STRING, + b"Smithay\0Wayland_Compositor\0", + )?; + + window.set_title(title); + window.map(); + + // Flush requests to server so window is displayed. + connection.flush()?; + + Ok(window) + } + + pub fn map(&self) { + if let Some(connection) = self.connection.upgrade() { + let _ = connection.map_window(self.id); + } + } + + pub fn unmap(&self) { + if let Some(connection) = self.connection.upgrade() { + // ICCCM - Changing Window State + // + // Normal -> Withdrawn - The client should unmap the window and follow it with a synthetic + // UnmapNotify event as described later in this section. + let _ = connection.unmap_window(self.id); + + // Send a synthetic UnmapNotify event to make the ICCCM happy + let _ = connection.send_event( + false, + self.id, + EventMask::STRUCTURE_NOTIFY | EventMask::SUBSTRUCTURE_NOTIFY, + UnmapNotifyEvent { + response_type: x11rb::protocol::xproto::UNMAP_NOTIFY_EVENT, + sequence: 0, // Ignored by X server + event: self.root, + window: self.id, + from_configure: false, + }, + ); + } + } + + pub fn size(&self) -> Size { + *self.size.lock().unwrap() + } + + pub fn set_title(&self, title: &str) { + if let Some(connection) = self.connection.upgrade() { + // _NET_WM_NAME should be preferred by window managers, but set both properties. + let _ = connection.change_property8( + PropMode::REPLACE, + self.id, + AtomEnum::WM_NAME, + AtomEnum::STRING, + title.as_bytes(), + ); + + let _ = connection.change_property8( + PropMode::REPLACE, + self.id, + self.atoms._NET_WM_NAME, + self.atoms.UTF8_STRING, + title.as_bytes(), + ); + } + } + + pub fn set_cursor_visible(&self, visible: bool) { + if let Some(connection) = self.connection.upgrade() { + let mut state = self.cursor_state.lock().unwrap(); + let changed = state.visible != visible; + + if changed && state.inside_window { + state.visible = visible; + self.update_cursor(&*connection, state.visible); + } + } + } + + pub fn cursor_enter(&self) { + if let Some(connection) = self.connection.upgrade() { + let mut state = self.cursor_state.lock().unwrap(); + state.inside_window = true; + self.update_cursor(&*connection, state.visible); + } + } + + pub fn cursor_leave(&self) { + if let Some(connection) = self.connection.upgrade() { + let mut state = self.cursor_state.lock().unwrap(); + state.inside_window = false; + self.update_cursor(&*connection, true); + } + } + + fn update_cursor(&self, connection: &C, visible: bool) { + let _ = match visible { + // This generates a Match error if we did not call Show/HideCursor before. Ignore that error. + true => connection + .xfixes_show_cursor(self.id) + .map(|cookie| cookie.ignore_error()), + false => connection + .xfixes_hide_cursor(self.id) + .map(|cookie| cookie.ignore_error()), + }; + } +} + +impl PartialEq for WindowInner { + fn eq(&self, other: &Self) -> bool { + match (self.connection.upgrade(), other.connection.upgrade()) { + (Some(conn1), Some(conn2)) => Arc::ptr_eq(&conn1, &conn2) && self.id == other.id, + + // A window with no connection to the X server cannot be validated. + _ => false, + } + } +} + +impl Drop for WindowInner { + fn drop(&mut self) { + if let Some(connection) = self.connection.upgrade() { + let _ = connection.destroy_window(self.id); + } + } +} diff --git a/vendor/smithay/src/desktop/mod.rs b/vendor/smithay/src/desktop/mod.rs new file mode 100644 index 0000000..16dccc4 --- /dev/null +++ b/vendor/smithay/src/desktop/mod.rs @@ -0,0 +1,68 @@ +//! Desktop management helpers +//! +//! This module contains helpers to organize and interact with desktop-style shells. +//! +//! It is therefore a lot more opinionated than for example the [xdg-shell handler](crate::wayland::shell::xdg::XdgShellHandler) +//! and tightly integrates with some protocols (e.g. xdg-shell). +//! +//! The usage of this module is therefor entirely optional and depending on your use-case you might also only want +//! to use a limited set of the helpers provided. +//! +//! ## Helpers +//! +//! ### [`Window`] +//! +//! A window represents what is typically understood by the end-user as a single application window. +//! +//! Currently it abstracts over xdg-shell toplevels and Xwayland surfaces. +//! It provides a bunch of methods to calculate and retrieve its size, manage itself, attach additional user_data +//! as well as a [drawing function](`crate::backend::renderer::element::AsRenderElements::render_elements`) to ease rendering it's related surfaces. +//! +//! Note that a [`Window`] on it's own has no position. For that it needs to be placed inside a [`Space`]. +//! +//! ### [`Space`] +//! +//! A space represents a two-dimensional plane of undefined dimensions. +//! [`Window`]s (and other types implementing [`SpaceElement`](space::SpaceElement)) and [`Output`](crate::output::Output)s can be mapped onto it. +//! +//! Elements get a position and stacking order through mapping. Outputs become views of a part of the [`Space`] +//! and can be rendered via [`render_output`](crate::desktop::space::render_output). +//! +//! ### Layer Shell +//! +//! A [`LayerSurface`] represents a surface as provided by e.g. the layer-shell protocol. +//! It provides similar helper methods as a [`Window`] does to toplevel surfaces. +//! +//! Each [`Output`](crate::output::Output) can be associated a [`LayerMap`] by calling [`layer_map_for_output`], +//! which [`LayerSurface`]s can be mapped upon. Associated layer maps are automatically rendered by [`render_output`](crate::desktop::space::render_output), +//! but a [draw function](`crate::backend::renderer::element::AsRenderElements::render_elements`) is also provided for manual layer-surface management. +//! +//! ### Popups +//! +//! Provides a [`PopupManager`], which can be used to automatically keep track of popups and their +//! relations to one-another. Popups are then automatically rendered with their matching toplevel surfaces, +//! when either [`crate::backend::renderer::element::AsRenderElements::render_elements`] or [`render_output`](crate::desktop::space::render_output) is called. +//! +//! ## Remarks +//! +//! Note that the desktop abstractions are concerned with easing rendering different clients and therefore need to be able +//! to manage client buffers to do so. If you plan to use the provided drawing functions, you need to use +//! [`on_commit_buffer_handler`](crate::backend::renderer::utils::on_commit_buffer_handler). + +pub mod space; +pub use self::space::Space; + +#[cfg(feature = "wayland_frontend")] +pub use self::wayland::{ + layer::{LayerMap, LayerSurface, layer_map_for_output}, + popup::*, + utils, + window::*, +}; +#[cfg(feature = "wayland_frontend")] +mod wayland { + pub(crate) mod layer; + pub mod popup; + pub mod utils; + pub mod window; +} diff --git a/vendor/smithay/src/desktop/space/element/mod.rs b/vendor/smithay/src/desktop/space/element/mod.rs new file mode 100644 index 0000000..6c15f95 --- /dev/null +++ b/vendor/smithay/src/desktop/space/element/mod.rs @@ -0,0 +1,546 @@ +use std::hash::Hash; + +#[cfg(feature = "wayland_frontend")] +use crate::{ + backend::renderer::{ImportAll, element::surface::WaylandSurfaceRenderElement}, + desktop::LayerSurface, + wayland::shell::wlr_layer::Layer, +}; +use crate::{ + backend::renderer::{ + Renderer, Texture, + element::{AsRenderElements, Wrap}, + }, + output::Output, + utils::{IsAlive, Logical, Physical, Point, Rectangle, Scale}, +}; + +#[cfg(feature = "wayland_frontend")] +mod wayland; +#[cfg(feature = "wayland_frontend")] +pub use self::wayland::SurfaceTree; + +/// Indicates default values for some zindexs inside smithay +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum RenderZindex { + /// WlrLayer::Background default zindex + Background = 10, + /// WlrLayer::Bottom default zindex + Bottom = 20, + /// Default zindex for Windows + Shell = 30, + /// WlrLayer::Top default zindex + Top = 40, + /// Default Layer for RenderElements + Overlay = 60, +} + +impl From for u8 { + #[inline] + fn from(idx: RenderZindex) -> u8 { + idx as u8 + } +} + +impl From for Option { + #[inline] + fn from(idx: RenderZindex) -> Option { + Some(idx as u8) + } +} + +/// Element mappable onto a [`Space`](super::Space) +pub trait SpaceElement: IsAlive { + /// Returns the geometry of this element. + /// + /// Defaults to be equal to it's bounding box. + fn geometry(&self) -> Rectangle { + self.bbox() + } + /// Returns the bounding box of this element + fn bbox(&self) -> Rectangle; + /// Returns whenever a given point inside this element will be able to receive input + fn is_in_input_region(&self, point: &Point) -> bool; + /// Gets the z-index of this element + fn z_index(&self) -> u8 { + RenderZindex::Overlay as u8 + } + + /// Set the rendered state to activated, if applicable to this element + fn set_activate(&self, activated: bool); + /// The element is displayed on a given output. + /// + /// Maybe called for an already entered output, + /// if the overlap changes + fn output_enter(&self, output: &Output, overlap: Rectangle); + /// The element left a given output + fn output_leave(&self, output: &Output); + /// Periodically called to update internal state, if necessary + fn refresh(&self) {} +} + +impl SpaceElement for &T { + fn geometry(&self) -> Rectangle { + SpaceElement::geometry(*self) + } + fn bbox(&self) -> Rectangle { + SpaceElement::bbox(*self) + } + fn is_in_input_region(&self, point: &Point) -> bool { + SpaceElement::is_in_input_region(*self, point) + } + fn z_index(&self) -> u8 { + SpaceElement::z_index(*self) + } + + fn set_activate(&self, activated: bool) { + SpaceElement::set_activate(*self, activated) + } + fn output_enter(&self, output: &Output, overlap: Rectangle) { + SpaceElement::output_enter(*self, output, overlap) + } + fn output_leave(&self, output: &Output) { + SpaceElement::output_leave(*self, output) + } + fn refresh(&self) { + SpaceElement::refresh(*self) + } +} + +#[derive(Debug)] +pub(super) enum SpaceElements<'a, E> { + #[cfg(feature = "wayland_frontend")] + Layer { + surface: LayerSurface, + output_location: Point, + }, + Element(&'a InnerElement), +} + +impl SpaceElements<'_, E> +where + E: SpaceElement, +{ + pub(super) fn z_index(&self) -> u8 { + match self { + #[cfg(feature = "wayland_frontend")] + SpaceElements::Layer { surface, .. } => { + let layer = match surface.layer() { + Layer::Background => RenderZindex::Background, + Layer::Bottom => RenderZindex::Bottom, + Layer::Top => RenderZindex::Top, + Layer::Overlay => RenderZindex::Overlay, + }; + layer as u8 + } + SpaceElements::Element(inner) => inner.element.z_index(), + } + } + + pub(super) fn bbox(&self) -> Rectangle { + match self { + #[cfg(feature = "wayland_frontend")] + SpaceElements::Layer { + surface, + output_location, + } => { + let mut bbox = surface.bbox(); + bbox.loc += *output_location; + bbox + } + SpaceElements::Element(inner) => inner.bbox(), + } + } + + pub(super) fn render_location(&self) -> Point { + match self { + #[cfg(feature = "wayland_frontend")] + SpaceElements::Layer { .. } => self.bbox().loc, + SpaceElements::Element(inner) => inner.render_location(), + } + } +} + +impl< + 'a, + #[cfg(feature = "wayland_frontend")] R: Renderer + ImportAll, + #[cfg(not(feature = "wayland_frontend"))] R: Renderer, + E: AsRenderElements, +> AsRenderElements for SpaceElements<'a, E> +where + R::TextureId: Clone + Texture + 'static, + >::RenderElement: 'a, + SpaceRenderElements>::RenderElement>: + From>::RenderElement>>, +{ + type RenderElement = SpaceRenderElements>::RenderElement>; + + #[profiling::function] + fn render_elements>( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec { + match &self { + #[cfg(feature = "wayland_frontend")] + SpaceElements::Layer { surface, .. } => AsRenderElements::::render_elements::< + WaylandSurfaceRenderElement, + >(surface, renderer, location, scale, alpha) + .into_iter() + .map(SpaceRenderElements::Surface) + .map(C::from) + .collect(), + SpaceElements::Element(element) => element + .element + .render_elements::>::RenderElement>>( + renderer, location, scale, alpha, + ) + .into_iter() + .map(SpaceRenderElements::Element) + .map(C::from) + .collect(), + } + } +} + +// Clippy: `large_enum_variant` is easily triggered here, as the `_GenericCatcher` is always zero-size. +// Thus we disable the lint. + +#[macro_export] +#[doc(hidden)] +macro_rules! space_elements_internal { + (@enum $(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime, $custom:ident>; $($(#[$meta:meta])* $body:ident=$field:ty),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$lt, $custom> { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident<$custom:ident>; $($(#[$meta:meta])* $body:ident=$field:ty),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$custom> { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime>; $($(#[$meta:meta])* $body:ident=$field:ty),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name<$lt> { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + (@enum $(#[$attr:meta])* $vis:vis $name:ident; $($(#[$meta:meta])* $body:ident=$field:ty),* $(,)?) => { + $(#[$attr])* + #[allow(clippy::large_enum_variant)] + $vis enum $name { + $( + $( + #[$meta] + )* + $body($field) + ),*, + #[doc(hidden)] + _GenericCatcher(std::convert::Infallible), + } + }; + (@call $name:ident; $($x:ident),*) => { + $crate::desktop::space::SpaceElement::$name($($x),*) + }; + (@alive $($(#[$meta:meta])* $body:ident=$field:ty),* $(,)?) => { + #[inline] + fn alive(&self) -> bool { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::utils::IsAlive::alive(x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + }; + (@body $($(#[$meta:meta])* $body:ident=$field:ty),* $(,)?) => { + fn geometry(&self) -> $crate::utils::Rectangle { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call geometry; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + fn bbox(&self) -> $crate::utils::Rectangle { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call bbox; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + fn is_in_input_region(&self, point: &$crate::utils::Point) -> bool { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call is_in_input_region; x, point) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn z_index(&self) -> u8 { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call z_index; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + + fn set_activate(&self, activated: bool) { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call set_activate; x, activated) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + fn output_enter(&self, output: &$crate::output::Output, overlap: $crate::utils::Rectangle) { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call output_enter; x, output, overlap) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + fn output_leave(&self, output: &$crate::output::Output) { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call output_leave; x, output) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + fn refresh(&self) { + match self { + $( + #[allow(unused_doc_comments)] + $( + #[$meta] + )* + Self::$body(x) => $crate::space_elements_internal!(@call refresh; x) + ),*, + Self::_GenericCatcher(_) => unreachable!(), + } + } + }; + (@impl $name:ident<$lt:lifetime>; $($tail:tt)*) => { + impl<$lt> $crate::desktop::space::SpaceElement for $name<$lt> + { + $crate::space_elements_internal!(@body $($tail)*); + } + impl<$lt> $crate::utils::IsAlive for $name<$lt> + { + $crate::space_elements_internal!(@alive $($tail)*); + } + }; + (@impl $name:ident<$lt:lifetime, $custom:ident>; $($tail:tt)*) => { + impl<$lt, $custom> $crate::desktop::space::SpaceElement for $name<$lt, $custom> + where + $custom: $crate::desktop::space::SpaceElement, + { + $crate::space_elements_internal!(@body $($tail)*); + } + impl<$lt, $custom> $crate::utils::IsAlive for $name<$lt, $custom> + where + $custom: $crate::utils::IsAlive, + { + $crate::space_elements_internal!(@alive $($tail)*); + } + }; + (@impl $name:ident<$custom:ident>; $($tail:tt)*) => { + impl<$custom> $crate::desktop::space::SpaceElement for $name<$custom> + where + $custom: $crate::desktop::space::SpaceElement, + { + $crate::space_elements_internal!(@body $($tail)*); + } + impl<$custom> $crate::utils::IsAlive for $name<$custom> + where + $custom: $crate::utils::IsAlive, + { + $crate::space_elements_internal!(@alive $($tail)*); + } + }; + (@impl $name:ident; $($tail:tt)*) => { + impl $crate::desktop::space::SpaceElement for $name + { + $crate::space_elements_internal!(@body $($tail)*); + } + impl $crate::utils::IsAlive for $name + { + $crate::space_elements_internal!(@alive $($tail)*); + } + }; +} + +/// Aggregate multiple types implementing [`SpaceElement`] into a single enum type to be used +/// with a [`Space`][super::Space]. +/// +/// # Example +/// +/// ``` +/// use smithay::desktop::space::space_elements; +/// # use smithay::{desktop::space::SpaceElement, output::Output, utils::{Point, Rectangle, Logical, IsAlive}}; +/// # struct Window; +/// # impl SpaceElement for Window { +/// # fn bbox(&self) -> Rectangle { unimplemented!() } +/// # fn is_in_input_region(&self, point: &Point) -> bool { unimplemented!() } +/// # fn set_activate(&self, activated: bool) {} +/// # fn output_enter(&self, output: &Output, overlap: Rectangle) {} +/// # fn output_leave(&self, output: &Output) {} +/// # } +/// # impl IsAlive for Window { +/// # fn alive(&self) -> bool { unimplemented!() } +/// # } +/// +/// space_elements! { +/// /// Name of the type +/// pub MySpaceElements<'a, C>; // can be generic if necessary +/// Window=Window, // variant name = type +/// Custom=&'a C, // also supports references or usage of generic types +/// } +/// ``` +#[macro_export] +macro_rules! space_elements { + ($(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime, $custom:ident>; $($tail:tt)*) => { + $crate::space_elements_internal!(@enum $(#[$attr])* $vis $name<$lt, $custom>; $($tail)*); + $crate::space_elements_internal!(@impl $name<$lt, $custom>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<$custom:ident>; $($tail:tt)*) => { + $crate::space_elements_internal!(@enum $(#[$attr])* $vis $name<$custom>; $($tail)*); + $crate::space_elements_internal!(@impl $name<$custom>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident<$lt:lifetime>; $($tail:tt)*) => { + $crate::space_elements_internal!(@enum $(#[$attr])* $vis $name<$lt>; $($tail)*); + $crate::space_elements_internal!(@impl $name<$lt>; $($tail)*); + }; + ($(#[$attr:meta])* $vis:vis $name:ident; $($tail:tt)*) => { + $crate::space_elements_internal!(@enum $(#[$attr])* $vis $name; $($tail)*); + $crate::space_elements_internal!(@impl $name; $($tail)*); + }; +} + +pub use space_elements; + +use super::{InnerElement, SpaceRenderElements}; + +#[cfg(test)] +#[allow(dead_code)] +mod tests { + use crate::{ + desktop::space::SpaceElement, + output::Output, + utils::{IsAlive, Logical, Point, Rectangle}, + }; + + pub struct TestElement; + impl SpaceElement for TestElement { + fn bbox(&self) -> Rectangle { + unimplemented!() + } + fn is_in_input_region(&self, _point: &Point) -> bool { + unimplemented!() + } + fn set_activate(&self, _activated: bool) {} + fn output_enter(&self, _output: &Output, _overlap: Rectangle) {} + fn output_leave(&self, _output: &Output) {} + } + impl IsAlive for TestElement { + fn alive(&self) -> bool { + unimplemented!() + } + } + + space_elements! { + /// Some test space elements + pub TestSpaceElements; + /// A complete surface tree + Window=TestElement, + } + + space_elements! { + /// Some test space elements + pub TestSpaceElements2<'a>; + /// A complete surface tree + Window=&'a TestElement, + } + + space_elements! { + /// Some test space elements + pub TestSpaceElements5<'a, C>; + /// A complete surface tree + Window=TestElement, + Custom=&'a C, + } + + space_elements! { + /// Some test space elements + pub TestSpaceElements7; + /// A complete surface tree + Window=TestElement, + Custom=C, + } +} diff --git a/vendor/smithay/src/desktop/space/element/wayland.rs b/vendor/smithay/src/desktop/space/element/wayland.rs new file mode 100644 index 0000000..c1ef260 --- /dev/null +++ b/vendor/smithay/src/desktop/space/element/wayland.rs @@ -0,0 +1,56 @@ +use crate::{ + backend::renderer::{ + ImportAll, Renderer, + element::{AsRenderElements, Kind, surface::WaylandSurfaceRenderElement}, + }, + utils::{IsAlive, Physical, Point, Scale}, +}; +use wayland_server::protocol::wl_surface::WlSurface; + +/// A custom surface tree +#[derive(Debug)] +pub struct SurfaceTree { + surface: WlSurface, +} + +impl SurfaceTree { + /// Create a surface tree from a surface + pub fn from_surface(surface: &WlSurface) -> Self { + SurfaceTree { + surface: surface.clone(), + } + } +} + +impl IsAlive for SurfaceTree { + #[inline] + fn alive(&self) -> bool { + self.surface.alive() + } +} + +impl AsRenderElements for SurfaceTree +where + R: Renderer + ImportAll, + R::TextureId: Clone + 'static, +{ + type RenderElement = WaylandSurfaceRenderElement; + + #[profiling::function] + fn render_elements>>( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec { + crate::backend::renderer::element::surface::render_elements_from_surface_tree( + renderer, + &self.surface, + location, + scale, + alpha, + Kind::Unspecified, + ) + } +} diff --git a/vendor/smithay/src/desktop/space/mod.rs b/vendor/smithay/src/desktop/space/mod.rs new file mode 100644 index 0000000..199f5a8 --- /dev/null +++ b/vendor/smithay/src/desktop/space/mod.rs @@ -0,0 +1,801 @@ +//! This module contains the [`Space`] helper class as well has related +//! rendering helpers to add custom elements or different clients to a space. + +use crate::{ + backend::renderer::{ + Color32F, Renderer, Texture, + damage::{Error as OutputDamageTrackerError, OutputDamageTracker, RenderOutputResult}, + element::{AsRenderElements, RenderElement, Wrap}, + }, + output::{Output, OutputModeSource, OutputNoMode}, + utils::{IsAlive, Logical, Point, Rectangle, Scale, Transform}, +}; +#[cfg(feature = "wayland_frontend")] +use crate::{ + backend::renderer::{ImportAll, element::surface::WaylandSurfaceRenderElement}, + desktop::{LayerSurface, WindowSurfaceType, layer_map_for_output}, + wayland::shell::wlr_layer::Layer, +}; +use std::{collections::HashMap, fmt}; +use tracing::{debug, debug_span, instrument}; +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_surface::WlSurface; + +mod element; +mod output; +mod utils; + +#[cfg(feature = "wayland_frontend")] +pub(crate) mod wayland; + +pub use self::element::*; +use self::output::*; +pub use self::utils::*; + +crate::utils::ids::id_gen!(space_id); + +#[derive(Debug)] +struct InnerElement { + element: E, + location: Point, + outputs: HashMap>, +} + +/// Represents two dimensional plane to map windows and outputs upon. +/// +/// Space is generic over the types of elements mapped onto it. +/// The simplest usecase is a `Space`, but other types can be used +/// by implementing [`SpaceElement`]. Multiple types might be quickly aggregated into +/// an enum by using the [`space_elements!`]-macro. +#[derive(Debug)] +pub struct Space { + pub(super) id: usize, + // in z-order, back to front + elements: Vec>, + outputs: Vec, + span: tracing::Span, +} + +impl PartialEq for Space { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Drop for Space { + #[inline] + fn drop(&mut self) { + space_id::remove(self.id); + } +} + +impl Default for Space { + #[inline] + fn default() -> Self { + let id = space_id::next(); + let span = debug_span!("desktop_space", id); + + Self { + id, + elements: Default::default(), + outputs: Default::default(), + span, + } + } +} + +impl Space { + /// Gets the id of this space + pub fn id(&self) -> usize { + self.id + } + + /// Map a [`SpaceElement`] and move it to top of the stack + /// + /// This can safely be called on an already mapped window + /// to update its location inside the space. + /// + /// If activate is true it will set the new windows state + /// to be activate and removes that state from every + /// other mapped window. + pub fn map_element

(&mut self, element: E, location: P, activate: bool) + where + P: Into>, + { + let inner = self.create_inner_element(element, location); + self.insert_elem(self.elements.len(), inner, activate); + } + + /// Map a [`SpaceElement`] above `reference_element` in the stack + /// + /// This can safely be called on an already mapped window + /// to update its location inside the space. + /// + /// This function does nothing if `reference_element` is not mapped. + /// + /// Both elements must have the same z-index, otherwise they will not be placed together. + /// + /// If activate is true it will set the new windows state + /// to be activate and removes that state from every + /// other mapped window. + pub fn map_element_above

(&mut self, element: E, location: P, reference_element: &E, activate: bool) + where + P: Into>, + { + if let Some(reference_pos) = self + .elements + .iter() + .position(|inner| &inner.element == reference_element) + { + let (inner, index) = + if let Some(pos) = self.elements.iter().position(|inner| inner.element == element) { + let inner = self.elements.remove(pos); + let index = if pos > reference_pos { + reference_pos + 1 + } else { + reference_pos + }; + (inner, index) + } else { + (self.create_inner_element(element, location), reference_pos + 1) + }; + + self.insert_elem(index, inner, activate); + } + } + + fn create_inner_element

(&mut self, element: E, location: P) -> InnerElement + where + P: Into>, + { + #[allow(clippy::mutable_key_type)] + let outputs = if let Some(pos) = self.elements.iter().position(|inner| inner.element == element) { + self.elements.remove(pos).outputs + } else { + HashMap::new() + }; + + InnerElement { + element, + location: location.into(), + outputs, + } + } + + /// Moves an already mapped [`SpaceElement`] to top of the stack + /// + /// This function does nothing for unmapped windows. + /// + /// If activate is true it will set the new windows state + /// to be activate and removes that state from every + /// other mapped window. + pub fn raise_element(&mut self, element: &E, activate: bool) { + if let Some(pos) = self.elements.iter().position(|inner| &inner.element == element) { + let inner = self.elements.remove(pos); + self.insert_elem(self.elements.len(), inner, activate); + } + } + + /// Moves an already-mapped [`SpaceElement`] to be above `reference_element` + /// + /// This function does nothing if one or both windows are unmapped. + /// + /// Both elements must have the same z-index, otherwise they will not be placed together. + /// + /// If activate is true it will set the new windows state + /// to be activate and removes that state from every + /// other mapped window. + pub fn raise_element_above(&mut self, element: &E, reference_element: &E, activate: bool) { + if let Some(pos) = self.elements.iter().position(|inner| &inner.element == element) { + if let Some(reference_pos) = self + .elements + .iter() + .position(|inner| &inner.element == reference_element) + { + let index = if pos > reference_pos { + reference_pos + 1 + } else { + reference_pos + }; + + let inner = self.elements.remove(pos); + self.insert_elem(index, inner, activate); + } + } + } + + /// Moves an already mapped [`SpaceElement`] to the bottom of the stack + /// + /// This function does nothing for unmapped windows. + pub fn lower_element(&mut self, element: &E) { + if let Some(pos) = self.elements.iter().position(|inner| &inner.element == element) { + let inner = self.elements.remove(pos); + self.insert_elem(0, inner, false); + } + } + + fn insert_elem(&mut self, index: usize, elem: InnerElement, activate: bool) { + if activate { + elem.element.set_activate(true); + for e in self.elements.iter() { + e.element.set_activate(false); + } + } + + self.elements.insert(index, elem); + self.elements.sort_by_key(|e1| e1.element.z_index()); + } + + /// Changes the location of an already-mapped [`SpaceElement`] in this space + /// + /// This function does nothing for unmapped windows. + /// + /// The element's stacing order and active state are not changed. + pub fn relocate_element

(&mut self, element: &E, location: P) + where + P: Into>, + { + if let Some(inner) = self.elements.iter_mut().find(|inner| &inner.element == element) { + inner.location = location.into(); + } + } + + /// Unmap a [`SpaceElement`] from this space. + /// + /// This function does nothing for already unmapped windows + pub fn unmap_elem(&mut self, element: &E) { + if let Some(pos) = self.elements.iter().position(|inner| &inner.element == element) { + let elem = self.elements.remove(pos); + for output in elem.outputs.keys() { + elem.element.output_leave(output); + } + } + } + + /// Iterate elements in z-order back to front + pub fn elements(&self) -> impl DoubleEndedIterator + ExactSizeIterator { + self.elements.iter().map(|e| &e.element) + } + + /// Iterate elements on a specific output in z-order back to front + pub fn elements_for_output<'output>( + &'output self, + output: &'output Output, + ) -> impl DoubleEndedIterator + use<'output, E> { + self.elements + .iter() + .filter(|e| e.outputs.contains_key(output)) + .map(|e| &e.element) + } + + /// Finds the topmost element under this point if any and returns it + /// together with the location of this element relative to this space. + /// + /// This is equivalent to iterating the elements in the space from + /// top to bottom and testing if the point is within the elements + /// input region and returning the first matching one. + /// + /// Note that [`SpaceElement::is_in_input_region`] expects the point + /// to be relative to the elements origin. + pub fn element_under>>(&self, point: P) -> Option<(&E, Point)> { + let point = point.into(); + self.elements + .iter() + .rev() + .filter(|e| e.bbox().to_f64().contains(point)) + .find_map(|e| { + // we need to offset the point to the location where the surface is actually drawn + let render_location = e.render_location(); + if e.element.is_in_input_region(&(point - render_location.to_f64())) { + Some((&e.element, render_location)) + } else { + None + } + }) + } + + /// Get a reference to the outputs under a given point + pub fn output_under>>(&self, point: P) -> impl Iterator { + let point = point.into(); + self.outputs.iter().rev().filter(move |o| { + let bbox = self.output_geometry(o); + bbox.map(|bbox| bbox.to_f64().contains(point)).unwrap_or(false) + }) + } + + /// Returns the layer surface matching a given surface, if any + /// + /// `surface_type` can be used to limit the types of surfaces queried for equality. + #[cfg(feature = "wayland_frontend")] + pub fn layer_for_surface( + &self, + surface: &WlSurface, + surface_type: WindowSurfaceType, + ) -> Option { + self.outputs.iter().find_map(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(surface, surface_type).cloned() + }) + } + + /// Returns the location of a [`SpaceElement`] inside the Space. + pub fn element_location(&self, elem: &E) -> Option> { + self.elements + .iter() + .find(|e| &e.element == elem) + .map(|e| e.location) + } + + /// Returns the bounding box of a [`SpaceElement`] including its relative position inside the Space. + pub fn element_bbox(&self, elem: &E) -> Option> { + self.elements + .iter() + .find(|e| &e.element == elem) + .map(|e| e.bbox()) + } + + /// Returns the geometry of a [`SpaceElement`] including its relative position inside the Space. + /// + /// This area is usually defined as the contents of the window, excluding decorations. + pub fn element_geometry(&self, elem: &E) -> Option> { + self.elements + .iter() + .find(|e| &e.element == elem) + .map(|e| e.geometry()) + } + + /// Maps an [`Output`] inside the space. + /// + /// Can be safely called on an already mapped + /// [`Output`] to update its location. + /// + /// *Note:* Remapping an output does reset it's damage memory. + pub fn map_output>>(&mut self, output: &Output, location: P) { + let location = location.into(); + set_output_location(self.id, output, location); + if !self.outputs.contains(output) { + debug!(parent: &self.span, output = output.name(), "Mapping output at {:?}", location); + self.outputs.push(output.clone()); + } + } + + /// Iterate over all mapped [`Output`]s of this space. + pub fn outputs(&self) -> impl Iterator { + self.outputs.iter() + } + + /// Unmap an [`Output`] from this space. + /// + /// Does nothing if the output was not previously mapped. + pub fn unmap_output(&mut self, output: &Output) { + if !self.outputs.contains(output) { + return; + } + debug!(parent: &self.span, output = output.name(), "Unmapping output"); + set_output_location(self.id, output, None); + self.outputs.retain(|o| o != output); + } + + /// Returns the geometry of the output including it's relative position inside the space. + /// + /// The size is matching the amount of logical pixels of the space visible on the output + /// given its current mode and scale. + pub fn output_geometry(&self, o: &Output) -> Option> { + if !self.outputs.contains(o) { + return None; + } + + let transform: Transform = o.current_transform(); + let location = output_location(self.id, o); + o.current_mode().map(|mode| { + Rectangle::new( + location, + transform + .transform_size(mode.size) + .to_f64() + .to_logical(o.current_scale().fractional_scale()) + .to_i32_ceil(), + ) + }) + } + + /// Returns all [`Output`]s a [`SpaceElement`] overlaps with. + pub fn outputs_for_element(&self, elem: &E) -> Vec { + if !self.elements.iter().any(|e| &e.element == elem) { + return Vec::new(); + } + + self.elements + .iter() + .find(|e| &e.element == elem) + .into_iter() + .flat_map(|e| &e.outputs) + .map(|(o, _)| o) + .cloned() + .collect() + } + + /// Refresh some internal values and update client state, + /// meaning this will handle output enter and leave events + /// for mapped outputs and windows based on their position. + /// + /// Needs to be called periodically, at best before every + /// wayland socket flush. + #[profiling::function] + pub fn refresh(&mut self) { + self.elements.retain(|e| e.alive()); + + let outputs = self + .outputs + .iter() + .cloned() + .map(|o| { + let geo = self.output_geometry(&o).unwrap_or_else(Rectangle::zero); + (o, geo) + }) + .collect::>(); + for e in &mut self.elements { + e.outputs.retain(|output, _| { + if !outputs.iter().any(|(o, _)| o == output) { + e.element.output_leave(output); + false + } else { + true + } + }); + + let bbox = e.bbox(); + for (output, output_geometry) in &outputs { + // Check if the bounding box of the toplevel intersects with the output + if let Some(mut overlap) = output_geometry.intersection(bbox) { + // output_enter expects the overlap to be relative to the element + overlap.loc -= bbox.loc; + let old = e.outputs.insert(output.clone(), overlap); + if old.is_none() || matches!(old, Some(old_overlap) if old_overlap != overlap) { + e.element.output_enter(output, overlap); + } + } else if e.outputs.remove(output).is_some() { + e.element.output_leave(output); + } + } + } + + self.elements.iter().for_each(|e| e.element.refresh()); + for (output, _) in outputs { + output.cleanup(); + } + } + + /// Retrieve the render elements for a given region of the space. + /// + /// *Note:* Because this is not rendering a specific output, + /// this will not contain layer surfaces. + /// Use [`Space::render_elements_for_output`], if you care about this. + #[instrument(level = "trace", skip(self, renderer, scale), parent = &self.span)] + #[profiling::function] + pub fn render_elements_for_region<'a, R: Renderer, S: Into>>( + &'a self, + renderer: &mut R, + region: &Rectangle, + scale: S, + alpha: f32, + ) -> Vec<>::RenderElement> + where + R::TextureId: Texture + 'static, + E: AsRenderElements, + >::RenderElement: 'a, + { + let scale = scale.into(); + + self.elements + .iter() + .rev() + .filter(|e| { + let geometry = e.bbox(); + region.overlaps(geometry) + }) + .flat_map(|e| { + let location = e.render_location() - region.loc; + e.element + .render_elements::<>::RenderElement>( + renderer, + location.to_physical_precise_round(scale), + scale, + alpha, + ) + }) + .collect::>() + } + + /// Retrieve the render elements for an output + #[instrument(level = "trace", skip(self, renderer), parent = &self.span)] + #[profiling::function] + pub fn render_elements_for_output< + 'a, + #[cfg(feature = "wayland_frontend")] R: Renderer + ImportAll, + #[cfg(not(feature = "wayland_frontend"))] R: Renderer, + >( + &'a self, + renderer: &mut R, + output: &Output, + alpha: f32, + ) -> Result>::RenderElement>>, OutputError> + where + R::TextureId: Clone + Texture + 'static, + E: AsRenderElements, + >::RenderElement: 'a, + SpaceRenderElements>::RenderElement>: + From>::RenderElement>>, + { + if !self.outputs.contains(output) { + return Err(OutputError::Unmapped); + } + + let output_scale = output.current_scale().fractional_scale(); + // The unwrap is safe or we would have returned OutputError::Unmapped already + let output_geo = self.output_geometry(output).unwrap(); + + let mut space_elements: Vec> = + self.elements.iter().rev().map(SpaceElements::Element).collect(); + + #[cfg(feature = "wayland_frontend")] + { + let layer_map = layer_map_for_output(output); + space_elements.extend(layer_map.layers().rev().cloned().map(|l| SpaceElements::Layer { + surface: l, + output_location: output_geo.loc, + })); + } + + space_elements.sort_by_key(|e| std::cmp::Reverse(e.z_index())); + + Ok(space_elements + .into_iter() + .filter(|e| { + let geometry = e.bbox(); + output_geo.overlaps(geometry) + }) + .flat_map(|e| { + let location = e.render_location() - output_geo.loc; + e.render_elements::>::RenderElement>>( + renderer, + location.to_physical_precise_round(output_scale), + Scale::from(output_scale), + alpha, + ) + }) + .collect::>()) + } +} + +/// Errors thrown by [`Space::elements_for_output`] +#[derive(thiserror::Error, Debug)] +pub enum OutputError { + /// The given [`Output`] has no set mode + #[error(transparent)] + NoMode(#[from] OutputNoMode), + /// The given [`Output`] is not mapped to this [`Space`]. + #[error("Output was not mapped to this space")] + Unmapped, +} + +impl IsAlive for InnerElement { + #[inline] + fn alive(&self) -> bool { + self.element.alive() + } +} + +impl InnerElement { + // the inner geometry of the element in space coordinates + fn geometry(&self) -> Rectangle { + let mut geo = self.element.geometry(); + geo.loc = self.location; + geo + } + + // the bounding box of the element in space coordinates + fn bbox(&self) -> Rectangle { + let mut bbox = self.element.bbox(); + bbox.loc += self.location - self.element.geometry().loc; + bbox + } + + fn render_location(&self) -> Point { + self.location - self.element.geometry().loc + } +} + +#[cfg(feature = "wayland_frontend")] +crate::backend::renderer::element::render_elements! { + /// Defines the render elements used internally by a [`Space`] + /// + /// Use them in place of `E` in `space_render_elements` or + /// `render_output` if you do not need custom render elements + pub SpaceRenderElements where + R: ImportAll; + /// A single wayland surface + Surface=WaylandSurfaceRenderElement, + /// A single texture + Element=Wrap, +} +#[cfg(not(feature = "wayland_frontend"))] +crate::backend::renderer::element::render_elements! { + /// Defines the render elements used internally by a [`Space`] + /// + /// Use them in place of `E` in `space_render_elements` or + /// `render_output` if you do not need custom render elements + pub SpaceRenderElements; + /// A single texture + Element=Wrap, +} + +impl< + #[cfg(feature = "wayland_frontend")] R: Renderer + ImportAll, + #[cfg(not(feature = "wayland_frontend"))] R: Renderer, + E: RenderElement + std::fmt::Debug, +> std::fmt::Debug for SpaceRenderElements +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #[cfg(feature = "wayland_frontend")] + Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(), + Self::Element(arg0) => f.debug_tuple("Element").field(arg0).finish(), + Self::_GenericCatcher(_) => unreachable!(), + } + } +} + +#[cfg(feature = "wayland_frontend")] +crate::backend::renderer::element::render_elements! { + OutputRenderElements<'a, R, E, C> where + R: ImportAll; + Space=SpaceRenderElements, + Custom=&'a C, +} +#[cfg(not(feature = "wayland_frontend"))] +crate::backend::renderer::element::render_elements! { + OutputRenderElements<'a, R, E, C>; + Space=SpaceRenderElements, + Custom=&'a C, +} + +/// Get the render elements for a specific output +/// +/// If multiple spaces are given their elements will be stacked +/// the same way. +/// +/// *Note*: If the `wayland_frontend`-feature is enabled +/// this will include layer-shell surfaces added to this +/// outputs [`LayerMap`](crate::desktop::LayerMap). +#[instrument(level = "trace", skip(spaces, renderer))] +#[profiling::function] +pub fn space_render_elements< + 'a, + #[cfg(feature = "wayland_frontend")] R: Renderer + ImportAll, + #[cfg(not(feature = "wayland_frontend"))] R: Renderer, + E: SpaceElement + PartialEq + AsRenderElements + 'a, + S: IntoIterator>, +>( + renderer: &mut R, + spaces: S, + output: &Output, + alpha: f32, +) -> Result>::RenderElement>>, OutputNoMode> +where + R::TextureId: Clone + Texture + 'static, + >::RenderElement: 'a, + SpaceRenderElements>::RenderElement>: + From>::RenderElement>>, +{ + let mut render_elements = Vec::new(); + let output_scale = output.current_scale().fractional_scale(); + + #[cfg(feature = "wayland_frontend")] + let layer_map = layer_map_for_output(output); + #[cfg(feature = "wayland_frontend")] + let lower = { + let (lower, upper): (Vec<&LayerSurface>, Vec<&LayerSurface>) = layer_map + .layers() + .rev() + .partition(|s| matches!(s.layer(), Layer::Background | Layer::Bottom)); + + render_elements.extend( + upper + .into_iter() + .filter_map(|surface| layer_map.layer_geometry(surface).map(|geo| (geo.loc, surface))) + .flat_map(|(loc, surface)| { + AsRenderElements::::render_elements::>( + surface, + renderer, + loc.to_physical_precise_round(output_scale), + Scale::from(output_scale), + alpha, + ) + .into_iter() + .map(SpaceRenderElements::Surface) + }), + ); + + lower + }; + + for space in spaces { + let _guard = space.span.enter(); + if let Some(output_geo) = space.output_geometry(output) { + render_elements.extend( + space + .render_elements_for_region(renderer, &output_geo, output_scale, alpha) + .into_iter() + .map(|e| SpaceRenderElements::Element(Wrap::from(e))), + ); + } + } + + #[cfg(feature = "wayland_frontend")] + render_elements.extend( + lower + .into_iter() + .filter_map(|surface| layer_map.layer_geometry(surface).map(|geo| (geo.loc, surface))) + .flat_map(|(loc, surface)| { + AsRenderElements::::render_elements::>( + surface, + renderer, + loc.to_physical_precise_round(output_scale), + Scale::from(output_scale), + alpha, + ) + .into_iter() + .map(SpaceRenderElements::Surface) + }), + ); + + Ok(render_elements) +} + +/// Render a output +/// +/// If multiple spaces are given their elements will be stacked +/// the same way. +#[allow(clippy::too_many_arguments)] +#[profiling::function] +pub fn render_output< + 'a, + 'd, + #[cfg(feature = "wayland_frontend")] R: Renderer + ImportAll, + #[cfg(not(feature = "wayland_frontend"))] R: Renderer, + C: RenderElement, + E: SpaceElement + PartialEq + AsRenderElements + 'a, + S: IntoIterator>, +>( + output: &Output, + renderer: &mut R, + framebuffer: &mut R::Framebuffer<'_>, + alpha: f32, + age: usize, + spaces: S, + custom_elements: &'a [C], + damage_tracker: &'d mut OutputDamageTracker, + clear_color: impl Into, +) -> Result, OutputDamageTrackerError> +where + R::TextureId: Clone + Texture + 'static, + >::RenderElement: 'a, + SpaceRenderElements>::RenderElement>: + From>::RenderElement>>, +{ + if let OutputModeSource::Auto(renderer_output) = damage_tracker.mode() { + assert!(renderer_output == output); + } + + let space_render_elements = space_render_elements(renderer, spaces, output, alpha)?; + + let mut render_elements: Vec>::RenderElement, C>> = + Vec::with_capacity(custom_elements.len() + space_render_elements.len()); + + render_elements.extend(custom_elements.iter().map(OutputRenderElements::Custom)); + render_elements.extend(space_render_elements.into_iter().map(OutputRenderElements::Space)); + + damage_tracker.render_output(renderer, framebuffer, age, &render_elements, clear_color) +} diff --git a/vendor/smithay/src/desktop/space/output.rs b/vendor/smithay/src/desktop/space/output.rs new file mode 100644 index 0000000..1c44313 --- /dev/null +++ b/vendor/smithay/src/desktop/space/output.rs @@ -0,0 +1,40 @@ +use crate::{ + output::Output, + utils::{Logical, Point}, +}; + +use std::{collections::HashMap, sync::Mutex}; + +type OutputUserdata = Mutex>>; + +pub fn set_output_location(space: usize, o: &Output, new_loc: impl Into>>) { + let userdata = o.user_data(); + userdata.insert_if_missing_threadsafe(OutputUserdata::default); + + match new_loc.into() { + Some(loc) => userdata + .get::() + .unwrap() + .lock() + .unwrap() + .insert(space, loc), + None => userdata + .get::() + .unwrap() + .lock() + .unwrap() + .remove(&space), + }; +} + +pub fn output_location(space: usize, o: &Output) -> Point { + let userdata = o.user_data(); + userdata.insert_if_missing_threadsafe(OutputUserdata::default); + *userdata + .get::() + .unwrap() + .lock() + .unwrap() + .entry(space) + .or_default() +} diff --git a/vendor/smithay/src/desktop/space/utils.rs b/vendor/smithay/src/desktop/space/utils.rs new file mode 100644 index 0000000..56815b1 --- /dev/null +++ b/vendor/smithay/src/desktop/space/utils.rs @@ -0,0 +1,80 @@ +use crate::{ + backend::renderer::{ + Renderer, + element::{ + AsRenderElements, + utils::{ + ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, RelocateRenderElement, + RescaleRenderElement, constrain_as_render_elements, + }, + }, + }, + utils::{Logical, Point, Rectangle, Scale}, +}; + +use super::SpaceElement; + +/// Defines the reference size for the constrain behavior. +#[derive(Debug, Copy, Clone)] +pub enum ConstrainReference { + /// Use the bounding box as the reference + BoundingBox, + /// Use the geometry as the reference + Geometry, +} + +/// Defines the behavior for [`constrain_space_element`] +#[derive(Debug, Copy, Clone)] +pub struct ConstrainBehavior { + /// Defines what should be used as the reference for calculating the scale + pub reference: ConstrainReference, + /// Defines how the element should be scaled + pub behavior: ConstrainScaleBehavior, + /// Defines the alignment of the element inside of the constrain + pub align: ConstrainAlign, +} + +/// Constrain the render elements of a [`SpaceElement`] +/// +/// see [`constrain_as_render_elements`] +#[profiling::function] +pub fn constrain_space_element<'e, R, E, C, L, S>( + renderer: &mut R, + element: &'e E, + location: L, + alpha: f32, + scale: S, + constrain: Rectangle, + behavior: ConstrainBehavior, +) -> impl Iterator + use +where + R: Renderer, + E: SpaceElement + AsRenderElements, + C: From< + CropRenderElement< + RelocateRenderElement>::RenderElement>>, + >, + >, + L: Into>, + S: Into>, +{ + let location = location.into(); + let scale = scale.into(); + + let scale_reference = match behavior.reference { + ConstrainReference::BoundingBox => element.bbox(), + ConstrainReference::Geometry => element.geometry(), + }; + + constrain_as_render_elements( + element, + renderer, + (location - scale_reference.loc).to_physical_precise_round(scale), + alpha, + constrain.to_physical_precise_round(scale), + scale_reference.to_physical_precise_round(scale), + behavior.behavior, + behavior.align, + scale, + ) +} diff --git a/vendor/smithay/src/desktop/space/wayland/layer.rs b/vendor/smithay/src/desktop/space/wayland/layer.rs new file mode 100644 index 0000000..05808b6 --- /dev/null +++ b/vendor/smithay/src/desktop/space/wayland/layer.rs @@ -0,0 +1,61 @@ +use crate::{ + backend::renderer::{ + ImportAll, Renderer, + element::{ + AsRenderElements, Kind, + surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, + }, + }, + desktop::{LayerSurface, PopupManager}, + utils::{Physical, Point, Scale}, +}; + +impl AsRenderElements for LayerSurface +where + R: Renderer + ImportAll, + R::TextureId: Clone + 'static, +{ + type RenderElement = WaylandSurfaceRenderElement; + + #[profiling::function] + fn render_elements>>( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec { + let surface = self.wl_surface(); + + let mut render_elements: Vec = Vec::new(); + let popup_render_elements = + PopupManager::popups_for_surface(surface).flat_map(|(popup, popup_offset)| { + let offset = (popup_offset - popup.geometry().loc) + .to_f64() + .to_physical(scale) + .to_i32_round(); + + render_elements_from_surface_tree( + renderer, + popup.wl_surface(), + location + offset, + scale, + alpha, + Kind::Unspecified, + ) + }); + + render_elements.extend(popup_render_elements); + + render_elements.extend(render_elements_from_surface_tree( + renderer, + surface, + location, + scale, + alpha, + Kind::Unspecified, + )); + + render_elements + } +} diff --git a/vendor/smithay/src/desktop/space/wayland/mod.rs b/vendor/smithay/src/desktop/space/wayland/mod.rs new file mode 100644 index 0000000..4b746f1 --- /dev/null +++ b/vendor/smithay/src/desktop/space/wayland/mod.rs @@ -0,0 +1,87 @@ +use std::{cell::RefCell, collections::HashMap}; + +use tracing::instrument; +use wayland_server::protocol::wl_surface::WlSurface; + +use crate::{ + backend::renderer::utils::RendererSurfaceStateUserData, + output::{Output, WeakOutput}, + utils::{Logical, Point, Rectangle}, + wayland::compositor::{TraversalAction, with_surface_tree_downward}, +}; + +mod layer; +mod window; +#[cfg(feature = "xwayland")] +mod x11; + +/// Updates the output overlap for a surface tree. +/// +/// Surfaces in the tree will receive output enter and leave events as necessary according to their +/// computed overlap. +#[instrument(level = "trace", skip(output), fields(output = output.name()))] +#[profiling::function] +pub fn output_update(output: &Output, output_overlap: Option>, surface: &WlSurface) { + with_surface_tree_downward( + surface, + (Point::from((0, 0)), false), + |_, states, (location, parent_unmapped)| { + let mut location = *location; + let data = states.data_map.get::(); + + // If the parent is unmapped we still have to traverse + // our children to send a leave events + if *parent_unmapped { + TraversalAction::DoChildren((location, true)) + } else if let Some(surface_view) = data.and_then(|d| d.lock().unwrap().surface_view) { + location += surface_view.offset; + TraversalAction::DoChildren((location, false)) + } else { + // If we are unmapped we still have to traverse + // our children to send leave events + TraversalAction::DoChildren((location, true)) + } + }, + |wl_surface, states, (location, parent_unmapped)| { + let mut location = *location; + + if *parent_unmapped { + // The parent is unmapped, just send a leave event + // if we were previously mapped and exit early + output.leave(wl_surface); + return; + } + + let Some(output_overlap) = output_overlap else { + // There's no overlap, send a leave event. + output.leave(wl_surface); + return; + }; + + let data = states.data_map.get::(); + + if let Some(surface_view) = data.and_then(|d| d.lock().unwrap().surface_view) { + location += surface_view.offset; + let surface_rectangle = Rectangle::new(location, surface_view.dst); + if output_overlap.overlaps(surface_rectangle) { + // We found a matching output, check if we already sent enter + output.enter(wl_surface); + } else { + // Surface does not match output, if we sent enter earlier + // we should now send leave + output.leave(wl_surface); + } + } else { + // Maybe the surface got unmapped, send leave on output + output.leave(wl_surface); + } + }, + |_, _, _| true, + ); +} + +#[derive(Debug, Default)] +struct WindowOutputState { + output_overlap: HashMap>, +} +type WindowOutputUserData = RefCell; diff --git a/vendor/smithay/src/desktop/space/wayland/window.rs b/vendor/smithay/src/desktop/space/wayland/window.rs new file mode 100644 index 0000000..2bf6844 --- /dev/null +++ b/vendor/smithay/src/desktop/space/wayland/window.rs @@ -0,0 +1,138 @@ +use crate::{ + backend::renderer::{ + ImportAll, Renderer, + element::{ + AsRenderElements, Kind, + surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, + }, + }, + desktop::{PopupManager, Window, WindowSurface, WindowSurfaceType, space::SpaceElement}, + output::Output, + utils::{Logical, Physical, Point, Rectangle, Scale}, + wayland::seat::WaylandFocus, +}; + +use super::{WindowOutputUserData, output_update}; + +impl SpaceElement for Window { + fn geometry(&self) -> Rectangle { + self.geometry() + } + + fn bbox(&self) -> Rectangle { + self.bbox_with_popups() + } + + fn is_in_input_region(&self, point: &Point) -> bool { + self.surface_under(*point, WindowSurfaceType::ALL).is_some() + } + + fn z_index(&self) -> u8 { + self.0.z_index.load(std::sync::atomic::Ordering::SeqCst) + } + + fn set_activate(&self, activated: bool) { + self.set_activated(activated); + } + + #[profiling::function] + fn output_enter(&self, output: &Output, overlap: Rectangle) { + self.user_data().insert_if_missing(WindowOutputUserData::default); + { + let mut state = self + .user_data() + .get::() + .unwrap() + .borrow_mut(); + state.output_overlap.insert(output.downgrade(), overlap); + state.output_overlap.retain(|weak, _| weak.is_alive()); + } + self.refresh() + } + + #[profiling::function] + fn output_leave(&self, output: &Output) { + if let Some(state) = self.user_data().get::() { + state.borrow_mut().output_overlap.retain(|weak, _| weak != output); + } + + if let Some(surface) = self.wl_surface() { + output_update(output, None, &surface); + for (popup, _) in PopupManager::popups_for_surface(&surface) { + output_update(output, None, popup.wl_surface()); + } + } + } + + #[profiling::function] + fn refresh(&self) { + self.user_data().insert_if_missing(WindowOutputUserData::default); + let state = self.user_data().get::().unwrap().borrow(); + + if let Some(surface) = self.wl_surface() { + for (weak, overlap) in state.output_overlap.iter() { + if let Some(output) = weak.upgrade() { + output_update(&output, Some(*overlap), &surface); + for (popup, location) in PopupManager::popups_for_surface(&surface) { + let mut overlap = *overlap; + overlap.loc -= location; + output_update(&output, Some(overlap), popup.wl_surface()); + } + } + } + } + } +} + +impl AsRenderElements for Window +where + R: Renderer + ImportAll, + R::TextureId: Clone + 'static, +{ + type RenderElement = WaylandSurfaceRenderElement; + + #[profiling::function] + fn render_elements>>( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec { + match self.underlying_surface() { + WindowSurface::Wayland(s) => { + let mut render_elements: Vec = Vec::new(); + let surface = s.wl_surface(); + let popup_render_elements = + PopupManager::popups_for_surface(surface).flat_map(|(popup, popup_offset)| { + let offset = (self.geometry().loc + popup_offset - popup.geometry().loc) + .to_physical_precise_round(scale); + + render_elements_from_surface_tree( + renderer, + popup.wl_surface(), + location + offset, + scale, + alpha, + Kind::Unspecified, + ) + }); + + render_elements.extend(popup_render_elements); + + render_elements.extend(render_elements_from_surface_tree( + renderer, + surface, + location, + scale, + alpha, + Kind::Unspecified, + )); + + render_elements + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => AsRenderElements::render_elements(s, renderer, location, scale, alpha), + } + } +} diff --git a/vendor/smithay/src/desktop/space/wayland/x11.rs b/vendor/smithay/src/desktop/space/wayland/x11.rs new file mode 100644 index 0000000..87266c5 --- /dev/null +++ b/vendor/smithay/src/desktop/space/wayland/x11.rs @@ -0,0 +1,104 @@ +use crate::{ + backend::renderer::{ + ImportAll, Renderer, + element::{ + Kind, + surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, + }, + }, + desktop::{WindowSurfaceType, space::SpaceElement}, + utils::{Logical, Physical, Point, Rectangle, Scale}, + xwayland::X11Surface, +}; + +use super::{WindowOutputUserData, output_update}; + +impl SpaceElement for X11Surface { + fn bbox(&self) -> Rectangle { + X11Surface::bbox(self) + } + + fn geometry(&self) -> Rectangle { + X11Surface::geometry(self) + } + + fn is_in_input_region(&self, point: &Point) -> bool { + X11Surface::surface_under(self, *point, (0, 0), WindowSurfaceType::all()).is_some() + } + + fn set_activate(&self, activated: bool) { + let _ = self.set_activated(activated); + } + + fn output_enter(&self, output: &crate::output::Output, overlap: Rectangle) { + self.user_data().insert_if_missing(WindowOutputUserData::default); + { + let mut state = self + .user_data() + .get::() + .unwrap() + .borrow_mut(); + state.output_overlap.insert(output.downgrade(), overlap); + state.output_overlap.retain(|weak, _| weak.is_alive()); + } + self.refresh() + } + + fn output_leave(&self, output: &crate::output::Output) { + if let Some(state) = self.user_data().get::() { + state.borrow_mut().output_overlap.retain(|weak, _| weak != output); + } + + let Some(surface) = X11Surface::wl_surface(self) else { + return; + }; + output_update(output, None, &surface); + } + + fn refresh(&self) { + self.user_data().insert_if_missing(WindowOutputUserData::default); + let wo_state = self.user_data().get::().unwrap().borrow(); + + let Some(surface) = X11Surface::wl_surface(self) else { + return; + }; + for (weak, overlap) in wo_state.output_overlap.iter() { + if let Some(output) = weak.upgrade() { + output_update(&output, Some(*overlap), &surface); + } + } + } + + fn z_index(&self) -> u8 { + if self.is_override_redirect() { + crate::desktop::space::RenderZindex::Overlay as u8 + } else { + crate::desktop::space::RenderZindex::Shell as u8 + } + } +} + +impl crate::backend::renderer::element::AsRenderElements for X11Surface +where + R: Renderer + ImportAll, + R::TextureId: Clone + 'static, +{ + type RenderElement = WaylandSurfaceRenderElement; + + #[profiling::function] + fn render_elements>>( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + mut alpha: f32, + ) -> Vec { + let Some(surface) = X11Surface::wl_surface(self) else { + return Vec::new(); + }; + if let Some(opacity) = self.state.lock().unwrap().opacity { + alpha *= (opacity as f32) / (u32::MAX as f32); + } + render_elements_from_surface_tree(renderer, &surface, location, scale, alpha, Kind::Unspecified) + } +} diff --git a/vendor/smithay/src/desktop/wayland/layer.rs b/vendor/smithay/src/desktop/wayland/layer.rs new file mode 100644 index 0000000..25756d4 --- /dev/null +++ b/vendor/smithay/src/desktop/wayland/layer.rs @@ -0,0 +1,784 @@ +use crate::{ + backend::renderer::utils::RendererSurfaceStateUserData, + desktop::{PopupManager, utils::*}, + output::{Output, WeakOutput}, + utils::{IsAlive, Logical, Point, Rectangle, Size, user_data::UserDataMap}, + wayland::{ + compositor::{SurfaceData, TraversalAction, with_states, with_surface_tree_downward}, + dmabuf::DmabufFeedback, + seat::WaylandFocus, + shell::wlr_layer::{ + Anchor, ExclusiveZone, KeyboardInteractivity, Layer as WlrLayer, LayerSurface as WlrLayerSurface, + LayerSurfaceCachedState, LayerSurfaceData, + }, + }, +}; +use indexmap::IndexSet; +use tracing::{debug_span, trace}; +use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback; +use wayland_server::protocol::wl_surface::{self, WlSurface}; + +use std::{ + borrow::Cow, + hash::{Hash, Hasher}, + num::Saturating, + sync::{Arc, Mutex, MutexGuard}, + time::Duration, +}; + +use crate::desktop::WindowSurfaceType; + +crate::utils::ids::id_gen!(layer_id); + +/// Map of [`LayerSurface`]s on an [`Output`] +#[derive(Debug)] +pub struct LayerMap { + layers: IndexSet, + output: WeakOutput, + zone: Rectangle, +} + +/// Retrieve a [`LayerMap`] for a given [`Output`]. +/// +/// If none existed before a new empty [`LayerMap`] is attached +/// to the output and returned on subsequent calls. +/// +/// Note: This function internally uses a [`Mutex`] per +/// [`Output`] as exposed by its return type. Therefore +/// trying to hold on to multiple references of a [`LayerMap`] +/// of the same output using this function *will* result in a deadlock. +pub fn layer_map_for_output(o: &Output) -> MutexGuard<'_, LayerMap> { + let userdata = o.user_data(); + userdata.insert_if_missing_threadsafe(|| { + Mutex::new(LayerMap { + layers: IndexSet::new(), + output: o.downgrade(), + zone: Rectangle::from_size( + o.current_mode() + .map(|mode| { + let logical_size = mode + .size + .to_f64() + .to_logical(o.current_scale().fractional_scale()) + .to_i32_round(); + o.current_transform().transform_size(logical_size) + }) + .unwrap_or_else(|| (0, 0).into()), + ), + }) + }); + userdata.get::>().unwrap().lock().unwrap() +} + +#[derive(Debug, thiserror::Error)] +pub enum LayerError { + #[error("Layer is already mapped to a different map")] + AlreadyMapped, +} + +impl LayerMap { + /// Map a [`LayerSurface`] to this [`LayerMap`]. + pub fn map_layer(&mut self, layer: &LayerSurface) -> Result<(), LayerError> { + if !self.layers.contains(layer) { + if layer + .0 + .userdata + .get::() + .map(|s| s.lock().unwrap().location.is_some()) + .unwrap_or(false) + { + return Err(LayerError::AlreadyMapped); + } + + self.layers.insert(layer.clone()); + self.arrange(); + } + Ok(()) + } + + /// Remove a [`LayerSurface`] from this [`LayerMap`]. + pub fn unmap_layer(&mut self, layer: &LayerSurface) { + if self.layers.shift_remove(layer) { + let _ = layer + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .location + .take(); + self.arrange(); + } + if let (Some(output), surface) = (self.output(), layer.wl_surface()) { + with_surface_tree_downward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |wl_surface, _, _| { + output.leave(wl_surface); + }, + |_, _, _| true, + ); + for (popup, _) in PopupManager::popups_for_surface(surface) { + let surface = popup.wl_surface(); + with_surface_tree_downward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |wl_surface, _, _| { + output.leave(wl_surface); + }, + |_, _, _| true, + ) + } + } + } + + /// Return the area of this output, that is not exclusive to any [`LayerSurface`]s. + pub fn non_exclusive_zone(&self) -> Rectangle { + self.zone + } + + /// Returns the geometry of a given mapped [`LayerSurface`]. + /// + /// If the surface was not previously mapped onto this layer map, + /// this function return `None`. + pub fn layer_geometry(&self, layer: &LayerSurface) -> Option> { + if !self.layers.contains(layer) { + return None; + } + let mut geo = layer.geometry(); + let state = layer_state(layer); + geo.loc += state.location.unwrap_or_default(); + Some(geo) + } + + /// Returns a [`LayerSurface`] under a given point and on a given layer, if any. + pub fn layer_under>>( + &self, + layer: WlrLayer, + point: P, + ) -> Option<&LayerSurface> { + let point = point.into(); + self.layers_on(layer).rev().find(|l| { + let bbox_with_popups = { + let mut bbox = l.bbox_with_popups(); + let state = layer_state(l); + bbox.loc += state.location.unwrap_or_default(); + bbox + }; + bbox_with_popups.to_f64().contains(point) + }) + } + + /// Iterator over all [`LayerSurface`]s currently mapped. + pub fn layers(&self) -> impl DoubleEndedIterator { + self.layers.iter() + } + + /// Iterator over all [`LayerSurface`]s currently mapped on a given layer. + pub fn layers_on(&self, layer: WlrLayer) -> impl DoubleEndedIterator { + self.layers.iter().filter(move |l| l.layer() == layer) + } + + /// Returns the [`LayerSurface`] matching a given [`WlSurface`], if any. + /// + /// `surface_type` can be used to limit the types of surfaces queried for equality. + pub fn layer_for_surface( + &self, + surface: &WlSurface, + surface_type: WindowSurfaceType, + ) -> Option<&LayerSurface> { + use std::sync::atomic::{AtomicBool, Ordering}; + + self.layers.iter().find(|layer| { + if surface_type.contains(WindowSurfaceType::POPUP) { + for (popup, _) in PopupManager::popups_for_surface(layer.wl_surface()) { + let toplevel = popup.wl_surface(); + let found = AtomicBool::new(false); + with_surface_tree_downward( + toplevel, + surface, + |_, _, search| { + if surface_type.contains(WindowSurfaceType::SUBSURFACE) { + TraversalAction::DoChildren(search) + } else { + TraversalAction::SkipChildren + } + }, + |s, _, search| { + found.fetch_or(s == *search, Ordering::SeqCst); + }, + |_, _, _| !found.load(Ordering::SeqCst), + ); + if found.load(Ordering::SeqCst) { + return true; + } + } + } + + if surface_type.contains(WindowSurfaceType::TOPLEVEL) { + let toplevel = layer.wl_surface(); + let found = AtomicBool::new(false); + with_surface_tree_downward( + toplevel, + surface, + |_, _, search| { + if surface_type.contains(WindowSurfaceType::SUBSURFACE) { + TraversalAction::DoChildren(search) + } else { + TraversalAction::SkipChildren + } + }, + |s, _, search| { + found.fetch_or(s == *search, Ordering::SeqCst); + }, + |_, _, _| !found.load(Ordering::SeqCst), + ); + return found.load(Ordering::SeqCst); + } + + false + }) + } + + /// Force re-arranging the layer surfaces, e.g. when the output size changes. + /// + /// Note: Mapping or unmapping a layer surface will automatically cause a re-arrangement. + /// + /// Return whenever any position or size changes of existing surfaces were necessary. + pub fn arrange(&mut self) -> bool { + let mut changed = false; + if let Some(output) = self.output() { + let span = debug_span!("layer_map", output = output.name()); + let _guard = span.enter(); + + let output_rect = Rectangle::from_size( + output + .current_mode() + .map(|mode| { + let logical_size = mode + .size + .to_f64() + .to_logical(output.current_scale().fractional_scale()) + .to_i32_round(); + output.current_transform().transform_size(logical_size) + }) + .unwrap_or_else(|| (0, 0).into()), + ); + let zone = output_rect; + let mut zone: Rectangle<_, Logical> = Rectangle::new( + Point::new(Saturating(zone.loc.x), Saturating(zone.loc.y)), + Size::new(Saturating(zone.size.w), Saturating(zone.size.h)), + ); + trace!("Arranging layers into {:?}", output_rect.size); + + let exclusive_surfaces = self + .layers + .iter() + .filter(|l| matches!(l.effective_exclusive_zone(), ExclusiveZone::Exclusive(_))); + let non_exclusive_surfaces = self + .layers + .iter() + .filter(|l| !matches!(l.effective_exclusive_zone(), ExclusiveZone::Exclusive(_))); + for layer in exclusive_surfaces.chain(non_exclusive_surfaces) { + let surface = layer.wl_surface(); + + with_surface_tree_downward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |wl_surface, _, _| { + output.enter(wl_surface); + }, + |_, _, _| true, + ); + for (popup, _) in PopupManager::popups_for_surface(surface) { + let surface = popup.wl_surface(); + with_surface_tree_downward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |wl_surface, _, _| { + output.enter(wl_surface); + }, + |_, _, _| true, + ) + } + + let data = layer.cached_state(); + + let mut source = match data.exclusive_zone { + ExclusiveZone::Exclusive(_) | ExclusiveZone::Neutral => zone, + ExclusiveZone::DontCare => Rectangle::new( + Point::new(Saturating(output_rect.loc.x), Saturating(output_rect.loc.y)), + Size::new(Saturating(output_rect.size.w), Saturating(output_rect.size.h)), + ), + }; + + // adjust the copy rect to account for the margins + if data.anchor.contains(Anchor::LEFT) { + source.size.w -= data.margin.left + } + if data.anchor.contains(Anchor::RIGHT) { + source.size.w -= data.margin.right + } + if data.anchor.contains(Anchor::TOP) { + source.size.h -= data.margin.top + } + if data.anchor.contains(Anchor::BOTTOM) { + source.size.h -= data.margin.bottom + } + + let mut size: Size<_, Logical> = Size::new(Saturating(data.size.w), Saturating(data.size.h)); + size.w = size.w.min(source.size.w); + size.h = size.h.min(source.size.h); + if size.w.0 == 0 { + size.w.0 = source.size.w.0 / 2; + } + if size.h.0 == 0 { + size.h.0 = source.size.h.0 / 2; + } + if data.anchor.anchored_horizontally() { + size.w = source.size.w; + } + if data.anchor.anchored_vertically() { + size.h = source.size.h; + } + + let x = if data.anchor.contains(Anchor::LEFT) { + source.loc.x + Saturating(data.margin.left) + } else if data.anchor.contains(Anchor::RIGHT) { + source.loc.x + (source.size.w - size.w) + } else { + source.loc.x + Saturating((source.size.w.0 / 2) - (size.w.0 / 2)) + }; + + let y = if data.anchor.contains(Anchor::TOP) { + source.loc.y + Saturating(data.margin.top) + } else if data.anchor.contains(Anchor::BOTTOM) { + source.loc.y + (source.size.h - size.h) + } else { + source.loc.y + Saturating((source.size.h.0 / 2) - (size.h.0 / 2)) + }; + + let location: Point = (x.0, y.0).into(); + + if let ExclusiveZone::Exclusive(amount) = data.exclusive_zone { + let amount = Saturating(amount as i32); + + match effective_exclusive_edge(&data) { + Some(Anchor::TOP) => { + let sum = amount + Saturating(data.margin.top); + zone.loc.y += sum; + zone.size.h -= sum; + } + Some(Anchor::BOTTOM) => { + zone.size.h -= amount + Saturating(data.margin.bottom); + } + Some(Anchor::LEFT) => { + let sum = amount + Saturating(data.margin.left); + zone.loc.x += sum; + zone.size.w -= sum; + } + Some(Anchor::RIGHT) => { + zone.size.w -= amount + Saturating(data.margin.right); + } + // Exclusive edge is always exactly one edge + Some(_) => unreachable!(), + None => {} + } + } + + let size = Size::new(size.w.0.max(0), size.h.0.max(0)); + trace!("Setting layer to pos {:?} and size {:?}", location, size); + let size_changed = layer.0.surface.with_pending_state(|state| { + state.size.replace(size).map(|old| old != size).unwrap_or(true) + }); + changed = changed || size_changed; + let initial_configure_sent = with_states(surface, |states| { + states + .data_map + .get::() + .map(|data| data.lock().unwrap().initial_configure_sent) + }) + .unwrap_or_default(); + + // arrange should never automatically send an configure + // event if the surface has not been configured already. + // The spec mandates that the initial configure has to be + // send in response of the initial commit of the surface. + // That also guarantees that the client is able set a size + // before committing the surface. By not respecting that + // we would send a wrong size to the client and also violate + // the spec by sending a configure event before a prior commit. + if size_changed && initial_configure_sent { + layer.0.surface.send_pending_configure(); + } + + { + let mut layer_state = layer_state(layer); + if layer_state.location != Some(location) { + layer_state.location = Some(location); + changed = true; + } + } + } + + let zone = Rectangle::new( + Point::new(zone.loc.x.0, zone.loc.y.0), + Size::new(zone.size.w.0.max(0), zone.size.h.0.max(0)), + ); + trace!("Remaining zone {:?}", zone); + self.zone = zone; + } + + changed + } + + fn output(&self) -> Option { + self.output.upgrade() + } + + /// Cleanup some internally used resources. + /// + /// This function needs to be called periodically (though not necessarily frequently) + /// to be able cleanup internally used resources. + pub fn cleanup(&mut self) { + if self.layers.iter().any(|l| !l.alive()) { + self.layers.retain(|layer| layer.alive()); + self.arrange(); + } + } + + /// Returns layers count + #[allow(clippy::len_without_is_empty)] //we don't need is_empty on that struct for now, mark as allow + pub fn len(&self) -> usize { + self.layers.len() + } +} + +fn effective_exclusive_edge(data: &LayerSurfaceCachedState) -> Option { + data.exclusive_edge + .or_else(|| implied_exclusive_edge_for_anchor(data.anchor)) +} + +fn implied_exclusive_edge_for_anchor(anchor: Anchor) -> Option { + match anchor.bits().count_ones() { + 0 | 2 | 4 => None, + 1 => Some(anchor), + 3 => Some(match anchor.complement() { + Anchor::TOP => Anchor::BOTTOM, + Anchor::BOTTOM => Anchor::TOP, + Anchor::LEFT => Anchor::RIGHT, + Anchor::RIGHT => Anchor::LEFT, + _ => unreachable!(), + }), + _ => unreachable!(), + } +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct LayerState { + pub location: Option>, +} + +type LayerUserdata = Mutex; +pub fn layer_state(layer: &LayerSurface) -> MutexGuard<'_, LayerState> { + let userdata = layer.user_data(); + userdata.insert_if_missing_threadsafe(LayerUserdata::default); + userdata.get::().unwrap().lock().unwrap() +} + +/// A [`LayerSurface`] represents a single layer surface as given by the wlr-layer-shell protocol. +#[derive(Debug, Clone)] +pub struct LayerSurface(pub(crate) Arc); + +impl PartialEq for LayerSurface { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.id == other.0.id + } +} + +impl Eq for LayerSurface {} + +impl Hash for LayerSurface { + #[inline] + fn hash(&self, state: &mut H) { + self.0.id.hash(state); + } +} + +#[derive(Debug)] +pub(crate) struct LayerSurfaceInner { + pub(crate) id: usize, + surface: WlrLayerSurface, + namespace: String, + userdata: UserDataMap, +} + +impl Drop for LayerSurfaceInner { + #[inline] + fn drop(&mut self) { + layer_id::remove(self.id); + } +} + +impl IsAlive for LayerSurface { + #[inline] + fn alive(&self) -> bool { + self.0.surface.alive() + } +} + +impl LayerSurface { + /// Create a new [`LayerSurface`] from a given [`WlrLayerSurface`] and its namespace. + pub fn new(surface: WlrLayerSurface, namespace: String) -> LayerSurface { + LayerSurface(Arc::new(LayerSurfaceInner { + id: layer_id::next(), + surface, + namespace, + userdata: UserDataMap::new(), + })) + } + + /// Returns the underlying [`WlrLayerSurface`] + pub fn layer_surface(&self) -> &WlrLayerSurface { + &self.0.surface + } + + /// Returns the underlying [`WlSurface`] + #[inline] + pub fn wl_surface(&self) -> &WlSurface { + self.0.surface.wl_surface() + } + + /// Returns a clone of the cached protocol state + pub fn cached_state(&self) -> LayerSurfaceCachedState { + self.0.surface.with_cached_state(Clone::clone) + } + + /// Returns true, if the surface has indicated, that it is able to process keyboard events. + pub fn can_receive_keyboard_focus(&self) -> bool { + self.0 + .surface + .with_cached_state(|state| match state.keyboard_interactivity { + KeyboardInteractivity::Exclusive | KeyboardInteractivity::OnDemand => true, + KeyboardInteractivity::None => false, + }) + } + + /// Returns the layer this surface resides on, if any yet. + pub fn layer(&self) -> WlrLayer { + self.0.surface.with_cached_state(|state| state.layer) + } + + /// Returns the namespace of this surface + pub fn namespace(&self) -> &str { + &self.0.namespace + } + + /// Returns the bounding box over this layer surface excluding its subsurfaces. + pub fn geometry(&self) -> Rectangle { + with_states(self.0.surface.wl_surface(), |states| { + let data = states.data_map.get::(); + data.and_then(|d| d.lock().unwrap().view()).map(|view| Rectangle { + loc: view.offset, + size: view.dst, + }) + }) + .unwrap_or_default() + } + + /// Returns the bounding box over this layer surface and its subsurfaces. + pub fn bbox(&self) -> Rectangle { + bbox_from_surface_tree(self.0.surface.wl_surface(), (0, 0)) + } + + /// Returns the bounding box over this layer surface, it subsurfaces as well as any popups. + /// + /// Note: You need to use a [`PopupManager`] to track popups, otherwise the bounding box + /// will not include the popups. + pub fn bbox_with_popups(&self) -> Rectangle { + let mut bounding_box = self.bbox(); + let surface = self.0.surface.wl_surface(); + for (popup, location) in PopupManager::popups_for_surface(surface) { + let offset = location - popup.geometry().loc; + bounding_box = bounding_box.merge(bbox_from_surface_tree(popup.wl_surface(), offset)); + } + + bounding_box + } + + /// Finds the topmost surface under this point if any and returns it together with the location of this + /// surface. + /// + /// - `point` needs to be relative to (0,0) of the layer surface. + pub fn surface_under>>( + &self, + point: P, + surface_type: WindowSurfaceType, + ) -> Option<(WlSurface, Point)> { + let point = point.into(); + let surface = self.wl_surface(); + if surface_type.contains(WindowSurfaceType::POPUP) { + for (popup, location) in PopupManager::popups_for_surface(surface) { + let surface = popup.wl_surface(); + let offset = location - popup.geometry().loc; + if let Some(result) = under_from_surface_tree(surface, point, offset, surface_type) { + return Some(result); + } + } + } + + if surface_type.contains(WindowSurfaceType::TOPLEVEL) { + return under_from_surface_tree(surface, point, (0, 0), surface_type); + } + + None + } + + /// Sends the frame callback to all the subsurfaces in this layer that requested it + /// + /// See [`send_frames_surface_tree`] for more information + pub fn send_frame( + &self, + output: &Output, + time: T, + throttle: Option, + primary_scan_out_output: F, + ) where + T: Into, + F: FnMut(&WlSurface, &SurfaceData) -> Option + Copy, + { + let time = time.into(); + let surface = self.0.surface.wl_surface(); + + send_frames_surface_tree(surface, output, time, throttle, primary_scan_out_output); + for (popup, _) in PopupManager::popups_for_surface(surface) { + let surface = popup.wl_surface(); + send_frames_surface_tree(surface, output, time, throttle, primary_scan_out_output); + } + } + + /// Sends the dmabuf feedback to all the subsurfaces in this window that requested it + /// + /// See [`send_dmabuf_feedback_surface_tree`] for more information + pub fn send_dmabuf_feedback<'a, P, F>( + &self, + output: &Output, + primary_scan_out_output: P, + select_dmabuf_feedback: F, + ) where + P: FnMut(&wl_surface::WlSurface, &SurfaceData) -> Option + Copy, + F: Fn(&wl_surface::WlSurface, &SurfaceData) -> &'a DmabufFeedback + Copy, + { + let surface = self.0.surface.wl_surface(); + send_dmabuf_feedback_surface_tree(surface, output, primary_scan_out_output, select_dmabuf_feedback); + for (popup, _) in PopupManager::popups_for_surface(surface) { + let surface = popup.wl_surface(); + send_dmabuf_feedback_surface_tree( + surface, + output, + primary_scan_out_output, + select_dmabuf_feedback, + ); + } + } + + /// Takes the [`PresentationFeedbackCallback`](crate::wayland::presentation::PresentationFeedbackCallback)s from all subsurfaces in this layer + /// + /// see [`take_presentation_feedback_surface_tree`] for more information + pub fn take_presentation_feedback( + &self, + output_feedback: &mut OutputPresentationFeedback, + primary_scan_out_output: F1, + presentation_feedback_flags: F2, + ) where + F1: FnMut(&WlSurface, &SurfaceData) -> Option + Copy, + F2: FnMut(&WlSurface, &SurfaceData) -> wp_presentation_feedback::Kind + Copy, + { + let surface = self.0.surface.wl_surface(); + take_presentation_feedback_surface_tree( + surface, + output_feedback, + primary_scan_out_output, + presentation_feedback_flags, + ); + for (popup, _) in PopupManager::popups_for_surface(surface) { + let surface = popup.wl_surface(); + take_presentation_feedback_surface_tree( + surface, + output_feedback, + primary_scan_out_output, + presentation_feedback_flags, + ); + } + } + + /// Run a closure on all surfaces in this layer (including it's popups, if [`PopupManager`] is used) + pub fn with_surfaces(&self, mut processor: F) + where + F: FnMut(&WlSurface, &SurfaceData), + { + let surface = self.0.surface.wl_surface(); + + with_surfaces_surface_tree(surface, &mut processor); + for (popup, _) in PopupManager::popups_for_surface(surface) { + let surface = popup.wl_surface(); + with_surfaces_surface_tree(surface, &mut processor); + } + } + + /// Returns a [`UserDataMap`] to allow associating arbitrary data with this surface. + pub fn user_data(&self) -> &UserDataMap { + &self.0.userdata + } + + /// Returns the exclusive zone set for the layer; except if an exclusive zone is set + /// but the exclusive edge is not set and cannot be inferred, then return `Neutral`. + fn effective_exclusive_zone(&self) -> ExclusiveZone { + with_states(self.wl_surface(), |states| { + let mut cached_state = states.cached_state.get::(); + let data = cached_state.current(); + if matches!(data.exclusive_zone, ExclusiveZone::Exclusive(_)) + && effective_exclusive_edge(data).is_none() + { + ExclusiveZone::Neutral + } else { + data.exclusive_zone + } + }) + } +} + +impl WaylandFocus for LayerSurface { + #[inline] + fn wl_surface(&self) -> Option> { + Some(Cow::Borrowed(self.0.surface.wl_surface())) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_implied_exclusive_edge() { + assert_eq!(implied_exclusive_edge_for_anchor(Anchor::empty()), None); + assert_eq!(implied_exclusive_edge_for_anchor(Anchor::all()), None); + assert_eq!( + implied_exclusive_edge_for_anchor(Anchor::BOTTOM | Anchor::LEFT), + None + ); + assert_eq!( + implied_exclusive_edge_for_anchor(Anchor::BOTTOM), + Some(Anchor::BOTTOM) + ); + assert_eq!( + implied_exclusive_edge_for_anchor(Anchor::LEFT | Anchor::TOP | Anchor::RIGHT), + Some(Anchor::TOP) + ); + } +} diff --git a/vendor/smithay/src/desktop/wayland/popup/grab.rs b/vendor/smithay/src/desktop/wayland/popup/grab.rs new file mode 100644 index 0000000..cc9eb9c --- /dev/null +++ b/vendor/smithay/src/desktop/wayland/popup/grab.rs @@ -0,0 +1,710 @@ +use std::{ + fmt, + sync::{Arc, Mutex}, +}; + +use wayland_server::{Resource, protocol::wl_surface::WlSurface}; + +use crate::{ + backend::input::{ButtonState, InputTime, KeyState, Keycode}, + input::{ + SeatHandler, + keyboard::{ + GrabStartData as KeyboardGrabStartData, KeyboardGrab, KeyboardHandle, KeyboardInnerHandle, + ModifiersState, + }, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, + }, + }, + utils::{DeadResource, IsAlive, Logical, Point, SERIAL_COUNTER, Serial}, + wayland::seat::WaylandFocus, +}; + +use thiserror::Error; + +use super::{PopupKind, PopupManager}; + +/// Defines the possible errors that +/// can be returned from [`PopupManager::grab_popup`] +#[derive(Debug, Error)] +pub enum PopupGrabError { + /// This resource has been destroyed and can no longer be used. + #[error(transparent)] + DeadResource(#[from] DeadResource), + /// The client tried to grab a popup after it's parent has been dismissed + #[error("the parent of the popup has been already dismissed")] + ParentDismissed, + /// The client tried to grab a popup after it has been mapped + #[error("tried to grab after being mapped")] + InvalidGrab, + /// The client tried to grab a popup which is not the topmost + #[error("popup was not created on the topmost popup")] + NotTheTopmostPopup, +} + +/// Defines the possibly strategies +/// for the [`PopupGrab::ungrab`] operation +#[derive(Debug, Copy, Clone)] +pub enum PopupUngrabStrategy { + /// Only ungrab the topmost popup + Topmost, + /// Ungrab all active popups + All, +} + +#[derive(Debug, Default)] +struct PopupGrabInternal { + serial: Option, + active_grabs: Vec, + dismissed_grabs: Vec, +} + +impl PopupGrabInternal { + fn has_any_grabs(&self) -> bool { + !self.active_grabs.is_empty() || !self.dismissed_grabs.is_empty() + } + + fn has_active_grabs(&self) -> bool { + !self.active_grabs.is_empty() + } + + fn current_grab(&self) -> Option<&WlSurface> { + self.active_grabs + .iter() + .rev() + .find(|p| p.alive()) + .map(|p| p.wl_surface()) + } + + fn is_dismissed(&self, surface: &WlSurface) -> bool { + self.dismissed_grabs.iter().any(|p| p.wl_surface() == surface) + } + + fn append_grab(&mut self, popup: &PopupKind) { + self.active_grabs.push(popup.clone()); + } + + fn cleanup(&mut self) { + let mut i = 0; + while i < self.active_grabs.len() { + if !self.active_grabs[i].alive() { + let grab = self.active_grabs.remove(i); + self.dismissed_grabs.push(grab); + } else { + i += 1; + } + } + + self.dismissed_grabs.retain(|p| p.alive()); + } +} + +#[derive(Debug, Clone, Default)] +pub(super) struct PopupGrabInner { + internal: Arc>, +} + +impl PopupGrabInner { + pub(super) fn has_any_grabs(&self) -> bool { + let guard = self.internal.lock().unwrap(); + guard.has_any_grabs() + } + + pub(super) fn has_active_grabs(&self) -> bool { + let guard = self.internal.lock().unwrap(); + guard.has_active_grabs() + } + + fn current_grab(&self) -> Option { + let guard = self.internal.lock().unwrap(); + guard.active_grabs.iter().rev().find(|p| p.alive()).cloned() + } + + pub(super) fn cleanup(&self) { + let mut guard = self.internal.lock().unwrap(); + guard.cleanup(); + } + + pub(super) fn grab(&self, popup: &PopupKind, serial: Serial) -> Result, PopupGrabError> { + let parent = popup.parent().ok_or(DeadResource)?; + + self.cleanup(); + + let mut guard = self.internal.lock().unwrap(); + + if let Some(grab) = guard.current_grab() { + if grab != &parent { + // If the parent is a grabbing popup which has already been dismissed, this popup will be immediately dismissed. + if guard.is_dismissed(&parent) { + return Err(PopupGrabError::ParentDismissed); + } + + // If the parent is a popup that did not take an explicit grab, an error will be raised. + return Err(PopupGrabError::NotTheTopmostPopup); + } + } + + guard.append_grab(popup); + + Ok(guard.serial.replace(serial)) + } + + fn ungrab(&self, root: &WlSurface, strategy: PopupUngrabStrategy) -> Option { + let mut guard = self.internal.lock().unwrap(); + let dismissed = match strategy { + PopupUngrabStrategy::Topmost => { + if let Some(grab) = guard.active_grabs.pop() { + let dismissed = PopupManager::dismiss_popup(root, &grab); + + if dismissed.is_ok() { + guard.dismissed_grabs.push(grab); + } + + dismissed + } else { + Ok(()) + } + } + PopupUngrabStrategy::All => { + let grabs = std::mem::take(&mut guard.active_grabs); + + if let Some(grab) = grabs.first() { + let dismissed = PopupManager::dismiss_popup(root, grab); + + if dismissed.is_ok() { + guard.dismissed_grabs.push(grab.clone()); + guard.dismissed_grabs.extend(grabs); + } + + dismissed + } else { + Ok(()) + } + } + }; + + if dismissed.is_err() { + // If dismiss_popup returns Err(DeadResource) there is not much what + // can do about it here, we just remove all our grabs as they are dead now + // anyway. The pointer/keyboard grab will be unset automatically so we + // should be fine. + guard.active_grabs.drain(..); + } + + guard.current_grab().cloned() + } +} + +/// Represents the explicit grab a client requested for a popup +/// +/// An explicit grab can be used by a client to redirect all keyboard +/// input to a single popup. The focus of the keyboard will stay on +/// the popup for as long as the grab is valid, that is as long as the +/// compositor did not call [`ungrab`](PopupGrab::ungrab) or the client +/// did not destroy the popup. A grab can be nested by requesting a grab +/// on a popup who's parent is the currently grabbed popup. The grab will +/// be returned to the parent after the popup has been dismissed. +/// +/// This module also provides default implementations for [`KeyboardGrab`] and +/// [`PointerGrab`] that implement the behavior described in the [`xdg-shell`](https://wayland.app/protocols/xdg-shell#xdg_popup:request:grab) +/// specification. See [`PopupKeyboardGrab`] and [`PopupPointerGrab`] for more +/// information on the default implementations. +/// +/// In case the implemented behavior is not suited for your use-case the grab can be +/// either decorated or a custom [`KeyboardGrab`]/[`PointerGrab`] can use the methods +/// on the [`PopupGrab`] to implement a custom behavior. +/// +/// One example would be to use a timer to automatically dismiss the popup after some +/// timeout. +/// +/// The grab is obtained by calling [`PopupManager::grab_popup`](super::PopupManager::grab_popup). +pub struct PopupGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + root: ::KeyboardFocus, + serial: Serial, + previous_serial: Option, + toplevel_grab: PopupGrabInner, + keyboard_handle: Option>, + keyboard_grab_start_data: KeyboardGrabStartData, + pointer_grab_start_data: PointerGrabStartData, +} + +impl fmt::Debug for PopupGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PopupGrab") + .field("root", &self.root) + .field("serial", &self.serial) + .field("previous_serial", &self.previous_serial) + .field("keyboard_handle", &self.keyboard_handle) + .field("keyboard_grab_start_data", &self.keyboard_grab_start_data) + .field("pointer_grab_start_data", &self.pointer_grab_start_data) + .finish() + } +} + +impl Clone for PopupGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + fn clone(&self) -> Self { + PopupGrab { + root: self.root.clone(), + serial: self.serial, + previous_serial: self.previous_serial, + toplevel_grab: self.toplevel_grab.clone(), + keyboard_handle: self.keyboard_handle.clone(), + keyboard_grab_start_data: self.keyboard_grab_start_data.clone(), + pointer_grab_start_data: self.pointer_grab_start_data.clone(), + } + } +} + +impl PopupGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus + From, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + pub(super) fn new( + toplevel_popups: PopupGrabInner, + root: ::KeyboardFocus, + serial: Serial, + previous_serial: Option, + keyboard_handle: Option>, + ) -> Self { + PopupGrab { + root: root.clone(), + serial, + previous_serial, + toplevel_grab: toplevel_popups, + keyboard_handle, + keyboard_grab_start_data: KeyboardGrabStartData { + // We set the focus to root as this will make + // sure the grab will stay alive until the + // toplevel is destroyed or the grab is unset + focus: Some(root.clone()), + }, + pointer_grab_start_data: PointerGrabStartData { + button: 0, + // We set the focus to root as this will make + // sure the grab will stay alive until the + // toplevel is destroyed or the grab is unset + focus: Some((root.into(), (0f64, 0f64).into())), + location: (0f64, 0f64).into(), + }, + } + } + + /// Returns the serial that was used to grab the popup + pub fn serial(&self) -> Serial { + self.serial + } + + /// Returns the previous serial that was used to grab + /// the parent popup in case of nested grabs + pub fn previous_serial(&self) -> Option { + self.previous_serial + } + + /// Check if this grab has ended + /// + /// A grab has ended if either all popups + /// associated with the grab have been dismissed + /// by the server with [`PopupGrab::ungrab`] or by the client + /// by destroying the popup. + /// + /// This will also return [`false`] if the root + /// of the grab has been destroyed. + pub fn has_ended(&self) -> bool { + !self.root.alive() || !self.toplevel_grab.has_active_grabs() + } + + /// Returns the current grabbed [`WlSurface`]. + /// + /// If the grab has ended this will return the root surface + /// so that the client expected focus can be restored + pub fn current_grab(&self) -> Option<::KeyboardFocus> { + self.toplevel_grab + .current_grab() + .map(From::from) + .or_else(|| Some(self.root.clone())) + } + + /// Ungrab and dismiss a popup + /// + /// This will dismiss either the topmost or all popups + /// according to the specified [`PopupUngrabStrategy`] + /// + /// Returns the new topmost popup in case of nested popups + /// or if the grab has ended the root surface + pub fn ungrab(&mut self, strategy: PopupUngrabStrategy) -> Option { + let root_surface = self.root.wl_surface()?; + self.toplevel_grab + .ungrab(&root_surface, strategy) + .or(Some(root_surface.into_owned())) + } + + /// Convenience method for getting a [`KeyboardGrabStartData`] for this grab. + /// + /// The focus of the [`KeyboardGrabStartData`] will always be the root + /// of the popup grab, e.g. the surface of the toplevel, to make sure + /// the grab is not automatically unset. + pub fn keyboard_grab_start_data(&self) -> &KeyboardGrabStartData { + &self.keyboard_grab_start_data + } + + /// Convenience method for getting a [`PointerGrabStartData`] for this grab. + /// + /// The focus of the [`PointerGrabStartData`] will always be the root + /// of the popup grab, e.g. the surface of the toplevel, to make sure + /// the grab is not automatically unset. + pub fn pointer_grab_start_data(&self) -> &PointerGrabStartData { + &self.pointer_grab_start_data + } + + fn unset_keyboard_grab(&self, data: &mut D, serial: Serial) { + if let Some(keyboard) = self.keyboard_handle.as_ref() { + if keyboard.is_grabbed() + && (keyboard.has_grab(self.serial) + || keyboard.has_grab(self.previous_serial.unwrap_or(self.serial))) + { + keyboard.unset_grab(data); + keyboard.set_focus(data, Some(self.root.clone()), serial); + } + } + } +} + +/// Default implementation of a [`KeyboardGrab`] for [`PopupGrab`] +/// +/// The [`PopupKeyboardGrab`] will keep the focus of the keyboard +/// on the topmost popup until the grab has ended. If the +/// grab has ended it will restore the focus on the root of the grab +/// and unset the [`KeyboardGrab`] +pub struct PopupKeyboardGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + popup_grab: PopupGrab, +} + +impl fmt::Debug for PopupKeyboardGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PopupKeyboardGrab") + .field("popup_grab", &self.popup_grab) + .finish() + } +} + +impl PopupKeyboardGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + /// Create a [`PopupKeyboardGrab`] for the provided [`PopupGrab`] + pub fn new(popup_grab: &PopupGrab) -> Self { + PopupKeyboardGrab { + popup_grab: popup_grab.clone(), + } + } +} + +impl KeyboardGrab for PopupKeyboardGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus + From, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + fn input( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + keycode: Keycode, + state: KeyState, + modifiers: Option, + serial: Serial, + time: InputTime, + ) { + // Check if the grab changed and update the focus + // If the grab has ended this will return the root + // surface to restore the client expected focus. + if let Some(focus) = self.popup_grab.current_grab() { + handle.set_focus(data, Some(focus), serial); + } + + if self.popup_grab.has_ended() { + handle.unset_grab(self, data, serial, false); + } + + handle.input(data, keycode, state, modifiers, serial, time) + } + + fn set_focus( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + focus: Option<::KeyboardFocus>, + serial: Serial, + ) { + // Ignore focus changes unless the grab has ended + if self.popup_grab.has_ended() { + handle.set_focus(data, focus, serial); + handle.unset_grab(self, data, serial, false); + return; + } + + // Allow to set the focus to the current grab, this can + // happen if the user initially sets the focus to + // popup instead of relying on the grab behavior + if self.popup_grab.current_grab() == focus { + handle.set_focus(data, focus, serial); + } + } + + fn start_data(&self) -> &KeyboardGrabStartData { + self.popup_grab.keyboard_grab_start_data() + } + + fn unset(&mut self, _data: &mut D) {} +} + +/// Default implementation of a [`PointerGrab`] for [`PopupGrab`] +/// +/// The [`PopupPointerGrab`] will make sure that the pointer focus +/// stays on the same client as the grabbed popup (similar to an +/// "owner-events" grab in X11 parlance). If an input event happens +/// outside of the grabbed [`WlSurface`] the popup will be dismissed +/// and the grab ends. In case of a nested grab all parent grabs will +/// also be dismissed. +/// +/// If the grab has ended the pointer focus is restored and the +/// [`PointerGrab`] is unset. Additional it will unset an active +/// [`KeyboardGrab`] that matches the [`Serial`] of this grab and +/// restore the keyboard focus like described in [`PopupKeyboardGrab`] +pub struct PopupPointerGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + popup_grab: PopupGrab, +} + +impl fmt::Debug for PopupPointerGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PopupPointerGrab") + .field("popup_grab", &self.popup_grab) + .finish() + } +} + +impl PopupPointerGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + /// Create a [`PopupPointerGrab`] for the provided [`PopupGrab`] + pub fn new(popup_grab: &PopupGrab) -> Self { + PopupPointerGrab { + popup_grab: popup_grab.clone(), + } + } +} + +impl PointerGrab for PopupPointerGrab +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus + From, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, +{ + fn motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + if self.popup_grab.has_ended() { + handle.unset_grab(self, data, event.serial, event.time, true); + return; + } + + // Check that the focus is of the same client as the grab + // If yes allow it, if not unset the focus. + if focus + .as_ref() + .and_then(|f1| { + self.popup_grab + .current_grab() + .as_ref() + .and_then(|f2| f2.wl_surface()) + .map(|s| f1.0.same_client_as(&s.id())) + }) + .unwrap_or(false) + { + handle.motion(data, focus, event); + } else { + handle.motion(data, None, event); + } + } + + fn relative_motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, event: &ButtonEvent) { + let serial = event.serial; + let time = event.time; + let state = event.state; + + if self.popup_grab.has_ended() { + handle.unset_grab(self, data, serial, time, true); + handle.button(data, event); + return; + } + + // Check if the client of the focused surface is still equal to the grabbed surface client + // if not the popup will be dismissed + if state == ButtonState::Pressed + && !handle + .current_focus() + .and_then(|f| { + self.popup_grab + .current_grab() + .and_then(|f2| f.0.wl_surface().map(|s| f2.same_client_as(&s.id()))) + }) + .unwrap_or(false) + { + let _ = self.popup_grab.ungrab(PopupUngrabStrategy::All); + handle.unset_grab(self, data, serial, time, true); + handle.button(data, event); + return; + } + + handle.button(data, event); + } + + fn axis(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, details: AxisFrame) { + handle.axis(data, details); + } + + fn frame(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &PointerGrabStartData { + self.popup_grab.pointer_grab_start_data() + } + + fn unset(&mut self, data: &mut D) { + let serial = SERIAL_COUNTER.next_serial(); + self.popup_grab.unset_keyboard_grab(data, serial); + } +} diff --git a/vendor/smithay/src/desktop/wayland/popup/manager.rs b/vendor/smithay/src/desktop/wayland/popup/manager.rs new file mode 100644 index 0000000..4405c43 --- /dev/null +++ b/vendor/smithay/src/desktop/wayland/popup/manager.rs @@ -0,0 +1,442 @@ +use crate::{ + input::{Seat, SeatHandler}, + utils::{DeadResource, IsAlive, Logical, Point, Serial}, + wayland::{ + compositor::{get_role, with_states}, + seat::WaylandFocus, + shell::xdg::{PopupCachedState, XDG_POPUP_ROLE, XdgPopupSurfaceData, XdgPopupSurfaceRoleAttributes}, + }, +}; +use std::sync::{Arc, Mutex}; +use tracing::trace; +use wayland_protocols::xdg::shell::server::xdg_wm_base; +use wayland_server::{Resource, protocol::wl_surface::WlSurface}; + +use super::{PopupGrab, PopupGrabError, PopupGrabInner, PopupKind}; + +/// Helper to track popups. +#[derive(Debug, Default)] +pub struct PopupManager { + unmapped_popups: Vec, + popup_trees: Vec, + popup_grabs: Vec, +} + +impl PopupManager { + /// Start tracking a new popup. + pub fn track_popup(&mut self, kind: PopupKind) -> Result<(), DeadResource> { + if kind.parent().is_some() { + self.add_popup(kind) + } else { + trace!("Adding unmapped popups: {:?}", kind); + self.unmapped_popups.push(kind); + Ok(()) + } + } + + /// Needs to be called for [`PopupManager`] to correctly update its internal state. + pub fn commit(&mut self, surface: &WlSurface) { + if get_role(surface) == Some(XDG_POPUP_ROLE) { + if let Some(i) = self + .unmapped_popups + .iter() + .position(|p| p.wl_surface() == surface) + { + trace!("Popup got mapped"); + let popup = self.unmapped_popups.swap_remove(i); + // at this point the popup must have a parent, + // or it would have raised a protocol error + let _ = self.add_popup(popup); + } + } + } + + /// Take an explicit grab for the provided [`PopupKind`] + /// + /// Returns a [`PopupGrab`] on success or an [`PopupGrabError`] + /// if the grab has been denied. + pub fn grab_popup( + &mut self, + root: ::KeyboardFocus, + popup: PopupKind, + seat: &Seat, + serial: Serial, + ) -> Result, PopupGrabError> + where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus + From, + ::PointerFocus: From<::KeyboardFocus> + WaylandFocus, + { + let surface = popup.wl_surface(); + assert_eq!( + root.wl_surface().as_deref(), + Some(&find_popup_root_surface(&popup)?) + ); + + match popup { + PopupKind::Xdg(ref _xdg) => (), + PopupKind::InputMethod(ref _input_method) => { + return Err(PopupGrabError::InvalidGrab); + } + } + + // The primary store for the grab is the seat, additional we store it + // in the popupmanager for active cleanup + seat.user_data().insert_if_missing(PopupGrabInner::default); + let toplevel_popups = seat.user_data().get::().unwrap().clone(); + + // It the popup grab is not alive it is likely + // that it either is new and have never been + // added to the popupmanager or that it has been + // cleaned up. + if !toplevel_popups.has_any_grabs() { + self.popup_grabs.push(toplevel_popups.clone()); + } + + let previous_serial = match toplevel_popups.grab(&popup, serial) { + Ok(serial) => serial, + Err(err) => { + match err { + PopupGrabError::ParentDismissed => { + let _ = PopupManager::dismiss_popup(&root.wl_surface().unwrap(), &popup); + } + PopupGrabError::NotTheTopmostPopup => { + surface.post_error( + xdg_wm_base::Error::NotTheTopmostPopup, + "xdg_popup was not created on the topmost popup", + ); + } + _ => {} + } + + return Err(err); + } + }; + + Ok(PopupGrab::new( + toplevel_popups, + root, + serial, + previous_serial, + seat.get_keyboard(), + )) + } + + fn add_popup(&mut self, popup: PopupKind) -> Result<(), DeadResource> { + let root = find_popup_root_surface(&popup)?; + + with_states(&root, |states| { + let tree = PopupTree::default(); + if states.data_map.insert_if_missing_threadsafe(|| tree.clone()) { + trace!("Adding popup {:?} to new PopupTree on root {:?}", popup, root); + tree.insert(popup); + + tree.set_registered(true); + self.popup_trees.push(tree); + } else { + let tree = states.data_map.get::().unwrap(); + trace!( + "Adding popup {:?} to existing PopupTree on root {:?}", + popup, root + ); + tree.insert(popup); + + if tree.set_registered(true) { + self.popup_trees.push(tree.clone()); + } + } + }); + + Ok(()) + } + + /// Finds the popup belonging to a given [`WlSurface`], if any. + pub fn find_popup(&self, surface: &WlSurface) -> Option { + self.unmapped_popups + .iter() + .find(|p| p.wl_surface() == surface && p.alive()) + .cloned() + .or_else(|| { + self.popup_trees + .iter() + .flat_map(|tree| tree.iter_popups()) + .find(|(p, _)| p.wl_surface() == surface) + .map(|(p, _)| p) + }) + } + + /// Returns the popups and their relative positions for a given toplevel surface, if any. + pub fn popups_for_surface( + surface: &WlSurface, + ) -> impl Iterator)> + use<> { + with_states(surface, |states| { + states + .data_map + .get::() + .map(|x| x.iter_popups()) + .into_iter() + .flatten() + }) + } + + /// Dismiss the `popup` associated with the `surface. + pub fn dismiss_popup(surface: &WlSurface, popup: &PopupKind) -> Result<(), DeadResource> { + if !surface.alive() { + return Err(DeadResource); + } + with_states(surface, |states| { + let tree = states.data_map.get::(); + + if let Some(tree) = tree { + tree.dismiss_popup(popup); + } + }); + Ok(()) + } + + /// Needs to be called periodically (but not necessarily frequently) + /// to cleanup internal resources. + pub fn cleanup(&mut self) { + self.popup_grabs.retain_mut(|grabs| { + grabs.cleanup(); + grabs.has_any_grabs() + }); + self.popup_trees.retain_mut(|tree| { + let alive = tree.cleanup_and_get_alive(); + if !alive { + let _ = tree.set_registered(false); + } + alive + }); + self.unmapped_popups.retain(|surf| surf.alive()); + } +} + +/// Finds the toplevel wl_surface this popup belongs to. +/// +/// Either because the parent of this popup is said toplevel +/// or because its parent popup belongs (indirectly) to said toplevel. +pub fn find_popup_root_surface(popup: &PopupKind) -> Result { + let mut parent = popup.parent().ok_or(DeadResource)?; + while get_role(&parent) == Some(XDG_POPUP_ROLE) { + parent = with_states(&parent, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .parent + .as_ref() + .cloned() + }) + .ok_or(DeadResource)?; + } + Ok(parent) +} + +/// Computes this popup's location relative to its toplevel wl_surface's geometry. +/// +/// This function will go up the parent stack and add up the relative locations. Useful for +/// transitive children popups. +pub fn get_popup_toplevel_coords(popup: &PopupKind) -> Point { + let mut parent = match popup.parent() { + Some(parent) => parent, + None => return (0, 0).into(), + }; + + let mut offset = (0, 0).into(); + while get_role(&parent) == Some(XDG_POPUP_ROLE) { + offset += with_states(&parent, |states| { + states + .cached_state + .get::() + .current() + .last_acked + .map(|c| c.state.geometry.loc) + .unwrap_or_default() + }); + parent = with_states(&parent, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .parent + .as_ref() + .cloned() + .unwrap() + }); + } + + offset +} + +#[derive(Debug, Default, Clone)] +struct PopupTree(Arc>); + +#[derive(Debug, Default, Clone)] +struct PopupTreeInner { + children: Vec, + registered: bool, +} + +#[derive(Debug, Clone)] +struct PopupNode { + surface: PopupKind, + children: Vec, +} + +impl PopupTree { + fn iter_popups(&self) -> impl Iterator)> + use<> { + self.0 + .lock() + .unwrap() + .children + .iter() + // A newly created xdg_popup is stacked on top of all previously created xdg_popups. We + // push new ones to the end of the vector, so we should iterate in reverse, from newest + // to oldest. + .rev() + .filter(|node| node.surface.alive()) + .flat_map(|n| n.iter_popups_relative_to((0, 0)).map(|(p, l)| (p.clone(), l))) + .collect::>() + .into_iter() + } + + fn insert(&self, popup: PopupKind) { + let tree = &mut *self.0.lock().unwrap(); + for child in tree.children.iter_mut() { + if child.try_insert(&popup) { + return; + } + } + tree.children.push(PopupNode::new(popup)); + } + + fn dismiss_popup(&self, popup: &PopupKind) { + let mut tree = self.0.lock().unwrap(); + + let mut i = 0; + while i < tree.children.len() { + let child = &mut tree.children[i]; + + if child.dismiss_popup(popup) { + let _ = tree.children.remove(i); + break; + } else { + i += 1; + } + } + } + + fn cleanup_and_get_alive(&mut self) -> bool { + let mut tree = self.0.lock().unwrap(); + tree.children + .retain_mut(|child| child.cleanup_and_get_alive(false)); + !tree.children.is_empty() + } + + /// Marks whether this tree is registered in the PopupManager's popup_trees + /// + /// Returns true if the registration status changed + fn set_registered(&self, registered: bool) -> bool { + let mut tree = self.0.lock().unwrap(); + let was_registered = tree.registered; + tree.registered = registered; + was_registered != registered + } +} + +impl PopupNode { + fn new(surface: PopupKind) -> Self { + PopupNode { + surface, + children: Vec::new(), + } + } + + fn iter_popups_relative_to>>( + &self, + loc: P, + ) -> impl Iterator)> { + let relative_to = loc.into() + self.surface.location(); + self.children + .iter() + // A newly created xdg_popup is stacked on top of all previously created xdg_popups. We + // push new ones to the end of the vector, so we should iterate in reverse, from newest + // to oldest. + .rev() + .filter(|node| node.surface.alive()) + .flat_map(move |x| { + Box::new(x.iter_popups_relative_to(relative_to)) + as Box)>> + }) + .chain(std::iter::once((&self.surface, relative_to))) + } + + fn try_insert(&mut self, popup: &PopupKind) -> bool { + let parent = popup.parent().unwrap(); + if self.surface.wl_surface() == &parent { + self.children.push(PopupNode::new(popup.clone())); + true + } else { + for child in &mut self.children { + if child.try_insert(popup) { + return true; + } + } + false + } + } + + fn send_done(&self) { + for child in self.children.iter().rev() { + child.send_done(); + } + + self.surface.send_done(); + } + + fn dismiss_popup(&mut self, popup: &PopupKind) -> bool { + if self.surface.wl_surface() == popup.wl_surface() { + self.send_done(); + return true; + } + + let mut i = 0; + while i < self.children.len() { + let child = &mut self.children[i]; + + if child.dismiss_popup(popup) { + let _ = self.children.remove(i); + return false; + } else { + i += 1; + } + } + + false + } + + fn cleanup_and_get_alive(&mut self, xdg_parent_destroyed: bool) -> bool { + let alive = self.surface.alive(); + let mut xdg_destroyed = false; + + if let PopupKind::Xdg(_) = self.surface { + if alive && xdg_parent_destroyed { + self.surface.wl_surface().post_error( + xdg_wm_base::Error::NotTheTopmostPopup, + "xdg_popup was destroyed while it was not the topmost popup", + ); + } + xdg_destroyed = !alive; + } + + self.children + .retain_mut(|child| child.cleanup_and_get_alive(xdg_destroyed)); + + alive + } +} diff --git a/vendor/smithay/src/desktop/wayland/popup/mod.rs b/vendor/smithay/src/desktop/wayland/popup/mod.rs new file mode 100644 index 0000000..d8829f5 --- /dev/null +++ b/vendor/smithay/src/desktop/wayland/popup/mod.rs @@ -0,0 +1,105 @@ +mod grab; +mod manager; + +pub use grab::*; +pub use manager::*; +use wayland_server::protocol::wl_surface::WlSurface; + +use crate::{ + utils::{IsAlive, Logical, Point, Rectangle}, + wayland::{ + compositor::with_states, + input_method, + shell::xdg::{self, SurfaceCachedState}, + }, +}; + +/// Represents a popup surface +#[derive(Debug, Clone, PartialEq)] +pub enum PopupKind { + /// xdg-shell [`PopupSurface`](xdg::PopupSurface) + Xdg(xdg::PopupSurface), + /// input-method [`PopupSurface`](input_method::PopupSurface) + InputMethod(input_method::PopupSurface), +} + +impl IsAlive for PopupKind { + #[inline] + fn alive(&self) -> bool { + match self { + PopupKind::Xdg(p) => p.alive(), + PopupKind::InputMethod(p) => p.alive(), + } + } +} + +impl From for WlSurface { + #[inline] + fn from(p: PopupKind) -> Self { + p.wl_surface().clone() + } +} + +impl PopupKind { + /// Retrieves the underlying [`WlSurface`] + #[inline] + pub fn wl_surface(&self) -> &WlSurface { + match *self { + PopupKind::Xdg(ref t) => t.wl_surface(), + PopupKind::InputMethod(ref t) => t.wl_surface(), + } + } + + fn parent(&self) -> Option { + match *self { + PopupKind::Xdg(ref t) => t.get_parent_surface(), + PopupKind::InputMethod(ref t) => t.get_parent().map(|parent| parent.surface.clone()), + } + } + + /// Returns the surface geometry as set by the client using `xdg_surface::set_window_geometry` + pub fn geometry(&self) -> Rectangle { + let wl_surface = self.wl_surface(); + match *self { + PopupKind::Xdg(_) => with_states(wl_surface, |states| { + states + .cached_state + .get::() + .current() + .geometry + .unwrap_or_default() + }), + PopupKind::InputMethod(ref t) => t.get_parent().map(|parent| parent.location).unwrap_or_default(), + } + } + + fn send_done(&self) { + match *self { + PopupKind::Xdg(ref t) => t.send_popup_done(), + PopupKind::InputMethod(_) => {} //Nothing to do the IME takes care of this itself + } + } + + fn location(&self) -> Point { + match *self { + PopupKind::Xdg(ref t) => { + t.with_committed_state(|current| current.map(|state| state.geometry.loc).unwrap_or_default()) + } + PopupKind::InputMethod(ref t) => t.location(), + } + } +} + +impl From for PopupKind { + #[inline] + fn from(p: xdg::PopupSurface) -> PopupKind { + PopupKind::Xdg(p) + } +} + +impl From for PopupKind { + #[inline] + fn from(p: input_method::PopupSurface) -> PopupKind { + PopupKind::InputMethod(p) + } +} diff --git a/vendor/smithay/src/desktop/wayland/utils.rs b/vendor/smithay/src/desktop/wayland/utils.rs new file mode 100644 index 0000000..40104be --- /dev/null +++ b/vendor/smithay/src/desktop/wayland/utils.rs @@ -0,0 +1,532 @@ +//! Helper functions to ease dealing with surface trees + +use crate::{ + backend::renderer::{ + element::{ + Id, PrimaryScanoutOutput, RenderElementPresentationState, RenderElementState, RenderElementStates, + }, + utils::{RendererSurfaceState, RendererSurfaceStateUserData}, + }, + desktop::WindowSurfaceType, + output::{Output, WeakOutput}, + utils::{Logical, Point, Rectangle, Time}, + wayland::{ + compositor::{SurfaceAttributes, SurfaceData, TraversalAction, with_surface_tree_downward}, + dmabuf::{DmabufFeedback, SurfaceDmabufFeedbackState}, + presentation::{PresentationFeedbackCachedState, PresentationFeedbackCallback, Refresh}, + }, +}; +use std::{cell::RefCell, sync::Mutex, time::Duration}; +use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback; +use wayland_server::protocol::wl_surface; + +pub use super::super::space::wayland::output_update; + +impl RendererSurfaceState { + fn contains_point>>(&self, attrs: &SurfaceAttributes, point: P) -> bool { + let point = point.into(); + let size = match self.surface_view.map(|view| view.dst) { + None => return false, // If the surface has no size, it can't have an input region. + Some(size) => size, + }; + + let rect = Rectangle::from_size(size).to_f64(); + + // The input region is always within the surface itself, so if the surface itself doesn't contain the + // point we can return false. + if !rect.contains(point) { + return false; + } + + // If there's no input region, we're done. + if attrs.input_region.is_none() { + return true; + } + + attrs + .input_region + .as_ref() + .unwrap() + .contains(point.to_i32_floor()) + } +} + +/// Returns the bounding box of a given surface and all its subsurfaces. +/// +/// - `location` can be set to offset the returned bounding box. +pub fn bbox_from_surface_tree

(surface: &wl_surface::WlSurface, location: P) -> Rectangle +where + P: Into>, +{ + let location = location.into(); + let mut bounding_box = Rectangle::new(location, (0, 0).into()); + with_surface_tree_downward( + surface, + location, + |_, states, loc: &Point| { + let mut loc = *loc; + let data = states.data_map.get::(); + + if let Some(surface_view) = data.and_then(|d| d.lock().unwrap().surface_view) { + loc += surface_view.offset; + // Update the bounding box. + bounding_box = bounding_box.merge(Rectangle::new(loc, surface_view.dst)); + + TraversalAction::DoChildren(loc) + } else { + // If the parent surface is unmapped, then the child surfaces are hidden as + // well, no need to consider them here. + TraversalAction::SkipChildren + } + }, + |_, _, _| {}, + |_, _, _| true, + ); + bounding_box +} + +/// Returns the topmost (sub-)surface under a given position matching the input regions of the surface. +/// +/// In case no surface input region matches the point [`None`] is returned. +/// +/// - `point` has to be the position to query, relative to (0, 0) of the given surface + `location`. +/// - `location` can be used to offset the returned point. +pub fn under_from_surface_tree

( + surface: &wl_surface::WlSurface, + point: Point, + location: P, + surface_type: WindowSurfaceType, +) -> Option<(wl_surface::WlSurface, Point)> +where + P: Into>, +{ + let found = RefCell::new(None); + with_surface_tree_downward( + surface, + location.into(), + |_, states, location: &Point| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(surface_view) = data.and_then(|d| d.lock().unwrap().surface_view) { + location += surface_view.offset; + if surface_type.contains(WindowSurfaceType::SUBSURFACE) { + TraversalAction::DoChildren(location) + } else { + TraversalAction::SkipChildren + } + } else { + // We are completely hidden, no point in traversing our children + TraversalAction::SkipChildren + } + }, + |wl_surface, states, location: &Point| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(surface_view) = data.and_then(|d| d.lock().unwrap().surface_view) { + location += surface_view.offset; + + let contains_the_point = data + .map(|data| { + data.lock() + .unwrap() + .contains_point(&*states.cached_state.get().current(), point - location.to_f64()) + }) + .unwrap_or(false); + if contains_the_point { + *found.borrow_mut() = Some((wl_surface.clone(), location)); + } + } + }, + |_, _, _| { + // only continue if the point is not found + found.borrow().is_none() + }, + ); + found.into_inner() +} + +type SurfacePrimaryScanoutOutput = Mutex; + +/// Run a closure on all surfaces of a surface tree +pub fn with_surfaces_surface_tree(surface: &wl_surface::WlSurface, mut processor: F) +where + F: FnMut(&wl_surface::WlSurface, &SurfaceData), +{ + with_surface_tree_downward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |surface, states, _| processor(surface, states), + |_, _, _| true, + ) +} + +/// Retrieve a previously stored primary scan-out output from a surface +/// +/// This will always return `None` if [`update_surface_primary_scanout_output`] is not used. +pub fn surface_primary_scanout_output( + _surface: &wl_surface::WlSurface, + states: &SurfaceData, +) -> Option { + states + .data_map + .insert_if_missing_threadsafe(SurfacePrimaryScanoutOutput::default); + let surface_primary_scanout_output = states.data_map.get::().unwrap(); + surface_primary_scanout_output.lock().unwrap().current_output() +} + +/// Update the surface primary scan-out output from a output render report +/// +/// The compare function can be used to alter the behavior for selecting the primary scan-out +/// output. See [`update_from_render_element_states`](crate::backend::renderer::element::PrimaryScanoutOutput::update_from_render_element_states) for more information about primary scan-out +/// output selection. +pub fn update_surface_primary_scanout_output( + surface: &wl_surface::WlSurface, + output: &Output, + surface_data: &SurfaceData, + namespace: Option, + states: &RenderElementStates, + compare: F, +) -> Option +where + F: for<'a> Fn(&'a Output, &'a RenderElementState, &'a Output, &'a RenderElementState) -> &'a Output, +{ + surface_data + .data_map + .insert_if_missing_threadsafe(SurfacePrimaryScanoutOutput::default); + let surface_primary_scanout_output = surface_data + .data_map + .get::() + .unwrap(); + surface_primary_scanout_output + .lock() + .unwrap() + .update_from_render_element_states(surface, output, namespace, states, compare) +} + +/// Sends frame callbacks for a surface and its subsurfaces with the given `time`. +/// +/// The frame callbacks for a [`WlSurface`](wl_surface::WlSurface) will only be sent if the +/// primary scan-out output equals the provided output or if the surface has no primary +/// scan-out output and the frame callback is overdue. A frame callback is considered +/// overdue if the last time a frame callback has been sent is greater than the provided +/// throttle threshold. If the threshold is `None` this will never send frame callbacks +/// for a surface that is not visible. Specifying [`Duration::ZERO`] as the throttle threshold +/// will always send frame callbacks for non visible surfaces. +/// +/// Surfaces the renderer knows to have no buffer, and their subsurfaces, never receive frame +/// callbacks, the same way they are not rendered. +pub fn send_frames_surface_tree( + surface: &wl_surface::WlSurface, + output: &Output, + time: T, + throttle: Option, + mut primary_scan_out_output: F, +) where + T: Into, + F: FnMut(&wl_surface::WlSurface, &SurfaceData) -> Option, +{ + let time = time.into(); + + with_surface_tree_downward( + surface, + (), + |_, states, &()| { + if surface_is_unmapped(states) { + TraversalAction::SkipChildren + } else { + TraversalAction::DoChildren(()) + } + }, + |surface, states, &()| { + // the processor still runs for a surface whose children were skipped + if surface_is_unmapped(states) { + return; + } + + states + .data_map + .insert_if_missing_threadsafe(SurfaceFrameThrottlingState::default); + let surface_frame_throttling_state = + states.data_map.get::().unwrap(); + + let on_primary_scanout_output = primary_scan_out_output(surface, states) + .map(|preferred_output| preferred_output == *output) + .unwrap_or(false); + + let frame_overdue = surface_frame_throttling_state.is_overdue(time, throttle); + + // We only want to send frame callbacks on the primary scan-out output + // or if we have no output and the frame is overdue, this can only + // happen if the surface is completely occluded on all outputs + let send_frame_callback = on_primary_scanout_output || frame_overdue; + + if send_frame_callback { + surface_frame_throttling_state.update_last_sent(time); + + // the surface may not have any user_data if it is a subsurface and has not + // yet been committed + for callback in states + .cached_state + .get::() + .current() + .frame_callbacks + .drain(..) + { + callback.done(time.as_millis() as u32); + } + } + }, + |_, _, &()| true, + ); +} + +/// Sends dmabuf feedback for a surface and its subsurfaces with the given select function. +/// +/// The dmabuf feedback for a [`WlSurface`](wl_surface::WlSurface) will only be sent if the +/// primary scan-out output equals the provided output and the surface has requested dmabuf +/// feedback. +pub fn send_dmabuf_feedback_surface_tree<'a, P, F>( + surface: &wl_surface::WlSurface, + output: &Output, + mut primary_scan_out_output: P, + select_dmabuf_feedback: F, +) where + P: FnMut(&wl_surface::WlSurface, &SurfaceData) -> Option, + F: Fn(&wl_surface::WlSurface, &SurfaceData) -> &'a DmabufFeedback, +{ + with_surface_tree_downward( + surface, + (), + |_, _, &()| TraversalAction::DoChildren(()), + |surface, states, &()| { + let on_primary_scanout_output = primary_scan_out_output(surface, states) + .map(|preferred_output| preferred_output == *output) + .unwrap_or(false); + + if !on_primary_scanout_output { + return; + } + + let Some(surface_feedback) = SurfaceDmabufFeedbackState::from_states(states) else { + return; + }; + + let feedback = select_dmabuf_feedback(surface, states); + surface_feedback.set_feedback(feedback); + }, + |_, _, &()| true, + ); +} + +/// Holds the presentation feedback for a surface +#[derive(Debug)] +pub struct SurfacePresentationFeedback { + callbacks: Vec, + flags: wp_presentation_feedback::Kind, +} + +impl SurfacePresentationFeedback { + /// Create a [`SurfacePresentationFeedback`] from the surface states. + /// + /// Returns `None` if the surface has no stored presentation feedback + pub fn from_states(states: &SurfaceData, flags: wp_presentation_feedback::Kind) -> Option { + let mut guard = states.cached_state.get::(); + let presentation_feedback_state = guard.current(); + if presentation_feedback_state.callbacks.is_empty() { + return None; + } + + let callbacks = std::mem::take(&mut presentation_feedback_state.callbacks); + Some(SurfacePresentationFeedback { callbacks, flags }) + } + + /// Mark the presentation feedbacks for this surface as presented + /// + /// If the passed in clk_id does not match the clk_id of a stored + /// presentation feedback the feedback will be discarded. + pub fn presented( + &mut self, + output: &Output, + clk_id: u32, + time: impl Into, + refresh: Refresh, + seq: u64, + flags: wp_presentation_feedback::Kind, + ) { + let time = time.into(); + for callback in self.callbacks.drain(..) { + if callback.clk_id() == clk_id { + callback.presented(output, time, refresh, seq, flags | self.flags) + } else { + callback.discarded() + } + } + } + + /// Mark the presentation feedbacks for this surface as discarded + pub fn discarded(&mut self) { + for callback in self.callbacks.drain(..) { + callback.discarded() + } + } +} + +impl Drop for SurfacePresentationFeedback { + fn drop(&mut self) { + self.discarded() + } +} + +/// Stores the [`SurfacePresentationFeedback`] for a specific output +/// +/// This is intended to be used in combination with [`take_presentation_feedback_surface_tree`]. +#[derive(Debug)] +pub struct OutputPresentationFeedback { + output: WeakOutput, + callbacks: Vec, +} + +impl OutputPresentationFeedback { + /// Create a new [`OutputPresentationFeedback`] for a specific [`Output`] + pub fn new(output: &Output) -> Self { + OutputPresentationFeedback { + output: output.downgrade(), + callbacks: Vec::new(), + } + } + + /// Returns the associated output + /// + /// Returns `None` if the output has been destroyed. + pub fn output(&self) -> Option { + self.output.upgrade() + } + + /// Mark all stored [`SurfacePresentationFeedback`]s as presented + /// + /// The flags passed to this function will be combined with the stored + /// per surface flags. + pub fn presented( + &mut self, + time: T, + refresh: Refresh, + seq: u64, + flags: wp_presentation_feedback::Kind, + ) where + T: Into>, + Kind: crate::utils::NonNegativeClockSource, + { + let time = time.into(); + let clk_id = Kind::ID as u32; + if let Some(output) = self.output.upgrade() { + for mut callback in self.callbacks.drain(..) { + callback.presented(&output, clk_id, time, refresh, seq, flags); + } + } else { + self.discarded(); + } + } + + /// Mark all stored [`SurfacePresentationFeedback`]s as discarded + pub fn discarded(&mut self) { + for mut callback in self.callbacks.drain(..) { + callback.discarded(); + } + } +} + +/// Takes the [`PresentationFeedbackCallback`]s from the surface tree +/// +/// This moves the [`PresentationFeedbackCallback`]s from the surfaces +/// where the primary scan-out matches the output of the [`OutputPresentationFeedback`] +/// to the [`OutputPresentationFeedback`] +/// +/// The flags closure can be used to set special flags per surface like [`wp_presentation_feedback::Kind::ZeroCopy`] +pub fn take_presentation_feedback_surface_tree( + surface: &wl_surface::WlSurface, + output_feedback: &mut OutputPresentationFeedback, + mut primary_scan_out_output: F1, + mut presentation_feedback_flags: F2, +) where + F1: FnMut(&wl_surface::WlSurface, &SurfaceData) -> Option, + F2: FnMut(&wl_surface::WlSurface, &SurfaceData) -> wp_presentation_feedback::Kind, +{ + with_surface_tree_downward( + surface, + (), + |_, _, &()| TraversalAction::DoChildren(()), + |surface, states, &()| { + let on_primary_scanout_output = primary_scan_out_output(surface, states) + .map(|preferred_output| preferred_output == output_feedback.output) + .unwrap_or(false); + + if !on_primary_scanout_output { + return; + } + + let flags = presentation_feedback_flags(surface, states); + if let Some(feedback) = SurfacePresentationFeedback::from_states(states, flags) { + output_feedback.callbacks.push(feedback); + } + }, + |_, _, &()| true, + ); +} + +/// Retrieves the per surface [`wp_presentation_feedback::Kind`] flags +/// +/// This will return [`wp_presentation_feedback::Kind::ZeroCopy`] if the surface +/// has been presented using zero-copy according to the [`RenderElementState`] +/// in the provided [`RenderElementStates`] +pub fn surface_presentation_feedback_flags_from_states( + surface: &wl_surface::WlSurface, + namespace: Option, + states: &RenderElementStates, +) -> wp_presentation_feedback::Kind { + let zero_copy = states + .element_render_state( + namespace + .map(|val| Id::from(surface).namespaced(val)) + .unwrap_or(surface.into()), + ) + .map(|state| state.presentation_state == RenderElementPresentationState::ZeroCopy) + .unwrap_or(false); + + if zero_copy { + wp_presentation_feedback::Kind::ZeroCopy + } else { + wp_presentation_feedback::Kind::empty() + } +} + +/// Whether the renderer state shows no buffer to draw. Surfaces without renderer state are +/// not considered unmapped, so compositors managing buffers themselves keep receiving callbacks. +fn surface_is_unmapped(states: &SurfaceData) -> bool { + states + .data_map + .get::() + .is_some_and(|d| d.lock().unwrap().surface_view.is_none()) +} + +#[derive(Debug, Default)] +struct SurfaceFrameThrottlingState(Mutex>); + +impl SurfaceFrameThrottlingState { + pub fn is_overdue(&self, time: Duration, throttle: Option) -> bool { + if let Some(throttle) = throttle { + let guard = self.0.lock().unwrap(); + guard + .map(|last| time.saturating_sub(last) > throttle) + .unwrap_or(true) + } else { + false + } + } + + pub fn update_last_sent(&self, time: Duration) { + *self.0.lock().unwrap() = Some(time); + } +} diff --git a/vendor/smithay/src/desktop/wayland/window.rs b/vendor/smithay/src/desktop/wayland/window.rs new file mode 100644 index 0000000..4273951 --- /dev/null +++ b/vendor/smithay/src/desktop/wayland/window.rs @@ -0,0 +1,516 @@ +#[cfg(feature = "wayland_frontend")] +use crate::utils::Serial; +#[cfg(feature = "xwayland")] +use crate::{desktop::space::SpaceElement, xwayland::X11Surface}; +use crate::{ + desktop::{PopupManager, space::RenderZindex, utils::*}, + output::Output, + utils::{IsAlive, Logical, Point, Rectangle, user_data::UserDataMap}, + wayland::{ + compositor::{SurfaceData, with_states}, + dmabuf::DmabufFeedback, + seat::WaylandFocus, + shell::xdg::{SurfaceCachedState, ToplevelSurface}, + }, +}; +use std::{ + borrow::Cow, + hash::{Hash, Hasher}, + sync::{ + Arc, Mutex, Weak, + atomic::{AtomicU8, Ordering}, + }, + time::Duration, +}; +use wayland_protocols::{ + wp::presentation_time::server::wp_presentation_feedback, xdg::shell::server::xdg_toplevel, +}; +use wayland_server::protocol::wl_surface; + +crate::utils::ids::id_gen!(window_id); + +/// Represents the surface of a [`Window`] +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum WindowSurface { + /// An xdg toplevel surface + Wayland(ToplevelSurface), + /// An X11 surface + #[cfg(feature = "xwayland")] + X11(X11Surface), +} + +#[derive(Debug)] +pub(crate) struct WindowInner { + pub(crate) id: usize, + surface: WindowSurface, + bbox: Mutex>, + pub(crate) z_index: AtomicU8, + user_data: UserDataMap, +} + +impl Drop for WindowInner { + fn drop(&mut self) { + window_id::remove(self.id); + } +} + +/// Represents possible errors returned from a window ping +#[derive(Debug, thiserror::Error)] +#[cfg(feature = "wayland_frontend")] +pub enum PingError { + /// This window's underlying surface does not support a ping protocol + #[error("Ping not supported for this window")] + NotSupported, + /// The operation failed because the underlying surface has been destroyed + #[error("The ping failed because the underlying surface has been destroyed")] + DeadSurface, + /// The provided serial cannot be used for a ping + #[error("Invalid serial for ping")] + InvalidSerial, + /// There is already a ping pending + #[error("There is already a ping pending `{0:?}`")] + PingAlreadyPending(Serial), +} + +#[cfg(feature = "wayland_frontend")] +impl From for PingError { + fn from(value: crate::wayland::shell::PingError) -> Self { + use crate::wayland::shell::PingError; + + match value { + PingError::DeadSurface => Self::DeadSurface, + PingError::PingAlreadyPending(serial) => Self::PingAlreadyPending(serial), + } + } +} + +#[cfg(feature = "xwayland")] +impl From for PingError { + fn from(value: crate::xwayland::xwm::PingError) -> Self { + use crate::xwayland::xwm::PingError; + + match value { + PingError::NotSupported => Self::NotSupported, + PingError::InvalidTimestamp => Self::InvalidSerial, + PingError::PingAlreadyPending(timestamp) => Self::PingAlreadyPending(Serial(timestamp)), + PingError::Connection(_) => Self::DeadSurface, + } + } +} + +/// Represents a single application window +#[derive(Debug, Clone)] +pub struct Window(pub(crate) Arc); + +impl PartialEq for Window { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.id == other.0.id + } +} + +impl Eq for Window {} + +impl Hash for Window { + #[inline] + fn hash(&self, state: &mut H) { + self.0.id.hash(state); + } +} + +impl IsAlive for Window { + #[inline] + fn alive(&self) -> bool { + match &self.0.surface { + WindowSurface::Wayland(s) => s.alive(), + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => s.alive(), + } + } +} + +impl Window { + /// Create a weak reference to this window that does not prevent + /// the inner resources from being dropped. + pub fn downgrade(&self) -> WeakWindow { + WeakWindow(Arc::downgrade(&self.0)) + } +} + +/// A weak reference to a [`Window`] that does not keep it alive. +/// +/// Use [`WeakWindow::upgrade`] to obtain a [`Window`] if the inner +/// resources have not yet been dropped. +#[derive(Debug, Clone)] +pub struct WeakWindow(Weak); + +impl WeakWindow { + /// Attempt to upgrade to a strong [`Window`] reference. + /// Returns `None` if the window has already been dropped. + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(Window) + } +} + +bitflags::bitflags! { + /// Defines the surface types that can be + /// queried with [`Window::surface_under`] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct WindowSurfaceType: u32 { + /// Include the toplevel surface + const TOPLEVEL = 1; + /// Include all subsurfaces + /// + /// This value only works in addition to either `TOPLEVEL` or `POPUP`. + const SUBSURFACE = 2; + /// Include all popup surfaces + const POPUP = 4; + /// Query all surfaces + const ALL = Self::TOPLEVEL.bits() | Self::SUBSURFACE.bits() | Self::POPUP.bits(); + } +} + +impl Window { + /// Construct a new [`Window`] from a xdg toplevel surface + /// + /// This function is deprecated. Use [`Window::new_wayland_window`] instead. + #[deprecated] + pub fn new(toplevel: ToplevelSurface) -> Window { + Self::new_wayland_window(toplevel) + } + + /// Construct a new [`Window`] from a xdg toplevel surface + pub fn new_wayland_window(toplevel: ToplevelSurface) -> Window { + let id = window_id::next(); + + Window(Arc::new(WindowInner { + id, + surface: WindowSurface::Wayland(toplevel), + bbox: Mutex::new(Rectangle::zero()), + z_index: AtomicU8::new(RenderZindex::Shell as u8), + user_data: UserDataMap::new(), + })) + } + + /// Construct a new [`Window`] from an X11 surface + #[cfg(feature = "xwayland")] + pub fn new_x11_window(surface: X11Surface) -> Window { + let id = window_id::next(); + + Window(Arc::new(WindowInner { + id, + surface: WindowSurface::X11(surface), + bbox: Mutex::new(Rectangle::zero()), + z_index: AtomicU8::new(RenderZindex::Shell as u8), + user_data: UserDataMap::new(), + })) + } + + /// Returns the window ID + pub fn id(&self) -> usize { + self.0.id + } + + /// Checks if the window is a wayland one. + #[inline] + pub fn is_wayland(&self) -> bool { + matches!(self.0.surface, WindowSurface::Wayland(_)) + } + + /// Checks if the window is an X11 one. + #[cfg(feature = "xwayland")] + #[inline] + pub fn is_x11(&self) -> bool { + matches!(self.0.surface, WindowSurface::X11(_)) + } + + /// Returns the geometry of this window. + pub fn geometry(&self) -> Rectangle { + match self.underlying_surface() { + WindowSurface::Wayland(s) => { + let bbox = self.bbox(); + // It's the set geometry clamped to the bounding box with the full bounding box as the fallback. + with_states(s.wl_surface(), |states| { + states + .cached_state + .get::() + .current() + .geometry + .and_then(|geo| geo.intersection(bbox)) + }) + .unwrap_or(bbox) + } + + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => SpaceElement::geometry(s), + } + } + + /// Returns a bounding box over this window and its children. + pub fn bbox(&self) -> Rectangle { + match &self.0.surface { + WindowSurface::Wayland(_) => *self.0.bbox.lock().unwrap(), + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => s.bbox(), + } + } + + /// Returns a bounding box over this window and children including popups. + /// + /// Note: You need to use a [`PopupManager`] to track popups, otherwise the bounding box + /// will not include the popups. + pub fn bbox_with_popups(&self) -> Rectangle { + let mut bounding_box = self.bbox(); + if let Some(surface) = self.wl_surface() { + for (popup, location) in PopupManager::popups_for_surface(&surface) { + let surface = popup.wl_surface(); + let offset = self.geometry().loc + location - popup.geometry().loc; + bounding_box = bounding_box.merge(bbox_from_surface_tree(surface, offset)); + } + } + + bounding_box + } + + /// Activate/Deactivate this window + pub fn set_activated(&self, active: bool) -> bool { + match &self.0.surface { + WindowSurface::Wayland(s) => s.with_pending_state(|state| { + if active { + state.states.set(xdg_toplevel::State::Activated) + } else { + state.states.unset(xdg_toplevel::State::Activated) + } + }), + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => { + let already_activated = s.is_activated(); + if s.set_activated(active).is_ok() { + already_activated ^ active + } else { + false + } + } + } + } + + /// Sends a ping to the window to check that the client is still responding. + /// + /// For `xdg_toplevel` surfaces, you can implement + /// [`XdgShellHandler::client_pong`](crate::wayland::shell::xdg::XdgShellHandler::client_pong) + /// to be notified of replies. + /// + /// For X11/XWayland surfaces, you can implement + /// [`XwmHandler::ping_acked`](crate::xwayland::XwmHandler::ping_acked) to be notified of + /// replies. + /// + /// Fails if the window's underlying surface does not support a ping protocol, is dead, or + /// already has a pending ping. + #[cfg(feature = "wayland_frontend")] + pub fn send_ping(&self, serial: Serial) -> Result<(), PingError> { + match &self.0.surface { + WindowSurface::Wayland(s) => s.client().send_ping(serial)?, + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => s.send_ping(serial.0)?, + } + Ok(()) + } + + /// Sends the frame callback to all the subsurfaces in this window that requested it + /// + /// See [`send_frames_surface_tree`] for more information + pub fn send_frame( + &self, + output: &Output, + time: T, + throttle: Option, + primary_scan_out_output: F, + ) where + T: Into, + F: FnMut(&wl_surface::WlSurface, &SurfaceData) -> Option + Copy, + { + let time = time.into(); + if let Some(surface) = self.wl_surface() { + send_frames_surface_tree(&surface, output, time, throttle, primary_scan_out_output); + for (popup, _) in PopupManager::popups_for_surface(&surface) { + let surface = popup.wl_surface(); + send_frames_surface_tree(surface, output, time, throttle, primary_scan_out_output); + } + } + } + + /// Sends the dmabuf feedback to all the subsurfaces in this window that requested it + /// + /// See [`send_dmabuf_feedback_surface_tree`] for more information + pub fn send_dmabuf_feedback<'a, P, F>( + &self, + output: &Output, + primary_scan_out_output: P, + select_dmabuf_feedback: F, + ) where + P: FnMut(&wl_surface::WlSurface, &SurfaceData) -> Option + Copy, + F: Fn(&wl_surface::WlSurface, &SurfaceData) -> &'a DmabufFeedback + Copy, + { + if let Some(surface) = self.wl_surface() { + send_dmabuf_feedback_surface_tree( + &surface, + output, + primary_scan_out_output, + select_dmabuf_feedback, + ); + for (popup, _) in PopupManager::popups_for_surface(&surface) { + let surface = popup.wl_surface(); + send_dmabuf_feedback_surface_tree( + surface, + output, + primary_scan_out_output, + select_dmabuf_feedback, + ); + } + } + } + + /// Takes the [`PresentationFeedbackCallback`](crate::wayland::presentation::PresentationFeedbackCallback)s from all subsurfaces in this window + /// + /// see [`take_presentation_feedback_surface_tree`] for more information + pub fn take_presentation_feedback( + &self, + output_feedback: &mut OutputPresentationFeedback, + primary_scan_out_output: F1, + presentation_feedback_flags: F2, + ) where + F1: FnMut(&wl_surface::WlSurface, &SurfaceData) -> Option + Copy, + F2: FnMut(&wl_surface::WlSurface, &SurfaceData) -> wp_presentation_feedback::Kind + Copy, + { + if let Some(surface) = self.wl_surface() { + take_presentation_feedback_surface_tree( + &surface, + output_feedback, + primary_scan_out_output, + presentation_feedback_flags, + ); + for (popup, _) in PopupManager::popups_for_surface(&surface) { + let surface = popup.wl_surface(); + take_presentation_feedback_surface_tree( + surface, + output_feedback, + primary_scan_out_output, + presentation_feedback_flags, + ); + } + } + } + + /// Run a closure on all surfaces in this window (including it's popups, if [`PopupManager`] is used) + pub fn with_surfaces(&self, mut processor: F) + where + F: FnMut(&wl_surface::WlSurface, &SurfaceData), + { + if let Some(surface) = self.wl_surface() { + with_surfaces_surface_tree(&surface, &mut processor); + for (popup, _) in PopupManager::popups_for_surface(&surface) { + let surface = popup.wl_surface(); + with_surfaces_surface_tree(surface, &mut processor); + } + } + } + + /// Updates internal values + /// + /// Needs to be called whenever the toplevel surface or any unsynchronized subsurfaces of this window are updated + /// to correctly update the bounding box of this window. + pub fn on_commit(&self) { + if let Some(surface) = self.wl_surface() { + *self.0.bbox.lock().unwrap() = bbox_from_surface_tree(&surface, (0, 0)); + } + } + + /// Finds the topmost surface under this point matching the input regions of the surface and returns + /// it together with the location of this surface. + /// + /// In case no surface input region matches the point [`None`] is returned. + /// + /// - `point` should be relative to (0,0) of the window. + pub fn surface_under>>( + &self, + point: P, + surface_type: WindowSurfaceType, + ) -> Option<(wl_surface::WlSurface, Point)> { + let point = point.into(); + + match &self.0.surface { + WindowSurface::Wayland(surface) => { + let surface = surface.wl_surface(); + if surface_type.contains(WindowSurfaceType::POPUP) { + for (popup, location) in PopupManager::popups_for_surface(surface) { + let offset = self.geometry().loc + location - popup.geometry().loc; + if let Some(result) = + under_from_surface_tree(popup.wl_surface(), point, offset, surface_type) + { + return Some(result); + } + } + } + + if surface_type.contains(WindowSurfaceType::TOPLEVEL) { + return under_from_surface_tree(surface, point, (0, 0), surface_type); + } + } + #[cfg(feature = "xwayland")] + WindowSurface::X11(surface) => { + if surface_type.contains(WindowSurfaceType::TOPLEVEL) { + return surface.surface_under(point, (0, 0), surface_type); + } + } + } + + None + } + + /// Returns the underlying xdg toplevel surface + pub fn toplevel(&self) -> Option<&ToplevelSurface> { + match &self.0.surface { + WindowSurface::Wayland(s) => Some(s), + #[cfg(feature = "xwayland")] + WindowSurface::X11(_) => None, + } + } + + /// Returns the underlying X11 surface + #[cfg(feature = "xwayland")] + pub fn x11_surface(&self) -> Option<&X11Surface> { + match &self.0.surface { + WindowSurface::Wayland(_) => None, + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => Some(s), + } + } + + /// Returns the underlying surface + pub fn underlying_surface(&self) -> &WindowSurface { + &self.0.surface + } + + /// Override the z_index of this Window + pub fn override_z_index(&self, z_index: u8) { + self.0.z_index.store(z_index, Ordering::SeqCst); + } + + /// Returns a [`UserDataMap`] to allow associating arbitrary data with this window. + #[inline] + pub fn user_data(&self) -> &UserDataMap { + &self.0.user_data + } +} + +impl WaylandFocus for Window { + #[inline] + fn wl_surface(&self) -> Option> { + match &self.0.surface { + WindowSurface::Wayland(s) => Some(Cow::Borrowed(s.wl_surface())), + #[cfg(feature = "xwayland")] + WindowSurface::X11(s) => s.wl_surface().map(Cow::Owned), + } + } +} diff --git a/vendor/smithay/src/input/dnd/grab.rs b/vendor/smithay/src/input/dnd/grab.rs new file mode 100644 index 0000000..7522236 --- /dev/null +++ b/vendor/smithay/src/input/dnd/grab.rs @@ -0,0 +1,636 @@ +use std::{fmt, sync::Arc}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::DisplayHandle; + +#[cfg(feature = "xwayland")] +use crate::{wayland::seat::WaylandFocus, xwayland::XWaylandClientData}; +#[cfg(feature = "xwayland")] +use wayland_server::Resource; + +use crate::{ + backend::input::InputTime, + input::{ + Seat, SeatHandler, + dnd::OfferData, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, + MotionEvent as PointerMotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, + }, + touch::{ + DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, TouchGrab, + TouchInnerHandle, UpEvent, + }, + }, + utils::{Logical, Point, SERIAL_COUNTER, Serial}, +}; + +use super::{DndFocus, Source}; + +/// Type of interaction that started a DnD grab +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GrabType { + /// Pointer input was validated for the requested grab + Pointer, + /// Touch input was validated for the requested grab + Touch, +} + +/// Grab during a client-initiated DnD operation. +pub struct DnDGrab + 'static> { + #[cfg(feature = "wayland_frontend")] + dh: DisplayHandle, + pointer_start_data: Option>, + touch_start_data: Option>, + last_position: Point, + data_source: Arc, + current_focus: Option, + offer_data: Option>, + seat: Seat, + should_drop: bool, +} + +impl fmt::Debug for DnDGrab +where + D: SeatHandler + 'static, + S: Source + fmt::Debug, + F: DndFocus + fmt::Debug + 'static, + F::OfferData: fmt::Debug + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_struct("DnDGrab"); + + #[cfg(feature = "wayland_frontend")] + { + f.field("dh", &self.dh); + } + + f.field("pointer_start_data", &self.pointer_start_data) + .field("touch_start_data", &self.touch_start_data) + .field("last_position", &self.last_position) + .field("data_source", &self.data_source) + .field("current_focus", &self.current_focus) + .field("offer_data", &self.offer_data) + .field("seat", &self.seat) + .finish() + } +} + +impl DnDGrab +where + D::PointerFocus: DndFocus, +{ + /// Create a new DnDGrab from an implicit pointer grab for a given source + pub fn new_pointer( + #[cfg(feature = "wayland_frontend")] dh: &DisplayHandle, + start_data: PointerGrabStartData, + source: S, + seat: Seat, + ) -> Self { + let last_position = start_data.location; + Self { + #[cfg(feature = "wayland_frontend")] + dh: dh.clone(), + pointer_start_data: Some(start_data), + touch_start_data: None, + last_position, + data_source: Arc::new(source), + current_focus: None, + offer_data: None, + seat, + should_drop: false, + } + } +} + +impl DnDGrab +where + D::TouchFocus: DndFocus, +{ + /// Create a new DnDGrab from an implicit touch grab for a given source + pub fn new_touch( + #[cfg(feature = "wayland_frontend")] dh: &DisplayHandle, + start_data: TouchGrabStartData, + source: S, + seat: Seat, + ) -> Self { + let last_position = start_data.location; + Self { + #[cfg(feature = "wayland_frontend")] + dh: dh.clone(), + pointer_start_data: None, + touch_start_data: Some(start_data), + last_position, + data_source: Arc::new(source), + current_focus: None, + offer_data: None, + seat, + should_drop: false, + } + } +} + +/// Enum over DndFocus candidates receiving a drop from `DnDGrab` +pub enum DndTarget<'a, D: SeatHandler> { + /// A Pointer-based DnDGrab ended on a `D::PointerFocus` + Pointer(&'a D::PointerFocus), + /// A Touch-based DnDGrab ended on a `D::TouchFocus` + Touch(&'a D::TouchFocus), +} + +impl fmt::Debug for DndTarget<'_, D> +where + D::PointerFocus: fmt::Debug, + D::TouchFocus: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pointer(p) => f.debug_tuple("DndTarget::Pointer").field(p).finish(), + Self::Touch(t) => f.debug_tuple("DndTarget::Touch").field(t).finish(), + } + } +} + +impl<'a, D: SeatHandler> DndTarget<'a, D> { + /// Returns the contained `Pointer`-value, consuming `self``. + /// + /// ## Panics + /// + /// Panics if the self value equals `Touch`. + pub fn unwrap_pointer(self) -> &'a D::PointerFocus { + match self { + DndTarget::Pointer(p) => p, + DndTarget::Touch(_) => panic!("unwrap_pointer on touch-based dnd grab target"), + } + } + + /// Returns the contained `Touch`-value, consuming `self``. + /// + /// ## Panics + /// + /// Panics if the self value equals `Pointer`. + pub fn unwrap_touch(self) -> &'a D::TouchFocus { + match self { + DndTarget::Pointer(_) => panic!("unwrap_pointer on pointer-based dnd grab target"), + DndTarget::Touch(t) => t, + } + } +} + +impl<'a, F, D: SeatHandler> DndTarget<'a, D> { + /// Returns the contained value consuming `self`. + pub fn into_inner(self) -> &'a F { + match self { + DndTarget::Pointer(p) => p, + DndTarget::Touch(t) => t, + } + } +} + +/// Events that are generated during drag'n'drop +pub trait DndGrabHandler: SeatHandler + Sized { + /// The drag'n'drop action was finished by the user releasing the pointer button / touch inputs. + /// + /// At this point, any icon should be removed. + /// + /// * `target` - The target that the contents were dropped on. + /// * `validated` - Whether the drop offer was negotiated and accepted. + /// * `seat` - The seat on which the DnD action was finished. + /// * `location` - The location the drop was finished at + fn dropped( + &mut self, + target: Option>, + validated: bool, + seat: Seat, + location: Point, + ) { + let _ = (target, validated, seat, location); + } + + /// The grab was cancelled by removing the grab by some means other than releasing the mouse + /// button or touch up. + fn cancelled(&mut self, seat: Seat, location: Point) { + let _ = (seat, location); + } +} + +impl DnDGrab +where + D: DndGrabHandler, + D: SeatHandler, + D: 'static, + S: Source, + F: DndFocus + 'static, +{ + fn update_focus( + &mut self, + data: &mut D, + focus: Option<(F, Point)>, + location: Point, + serial: Serial, + time: InputTime, + ) { + if self + .current_focus + .as_ref() + .is_some_and(|current| focus.as_ref().is_none_or(|(f, _)| f != current)) + { + // focus changed, we need to make a leave if appropriate + if let Some(focus) = self.current_focus.take() { + // only leave if there is a data source or we are on the original client + focus.leave(data, self.offer_data.as_mut(), &self.seat); + + // disable the offers + if let Some(offer_data) = self.offer_data.take() { + offer_data.disable(); + } + } + } + + if let Some((focus, surface_location)) = focus { + // early return if the surface is no longer valid + if !focus.alive() { + return; + } + + let (x, y) = (location - surface_location).into(); + if self.current_focus.is_none() { + if self + .data_source + .metadata() + .is_some_and(|metadata| metadata.mime_types.is_empty()) + { + // delay until they have materialized + return; + } + + // We entered a new surface, send the data offer if appropriate + self.offer_data = focus.enter( + data, + #[cfg(feature = "wayland_frontend")] + &self.dh, + self.data_source.clone(), + &self.seat, + Point::new(x, y), + &serial, + ); + self.current_focus = Some(focus); + } else { + // make a move + focus.motion(data, self.offer_data.as_mut(), &self.seat, Point::new(x, y), time); + } + } + } + + fn cancel(&mut self, data: &mut D) { + if let Some(ref offer_data) = self.offer_data { + offer_data.disable(); + } + + self.data_source.cancel(); + + DndGrabHandler::cancelled(data, self.seat.clone(), self.last_position); + + if let Some(ref focus) = self.current_focus { + focus.leave(data, self.offer_data.as_mut(), &self.seat); + } + } + + fn drop<'a>(&'a mut self, data: &mut D, into_target: impl Fn(&'a F) -> DndTarget<'a, D>) { + // the user dropped, proceed to the drop + let validated = self.offer_data.as_ref().is_some_and(|data| data.validated()); + if let Some(ref focus) = self.current_focus { + focus.drop(data, self.offer_data.as_mut(), &self.seat); + } + + if let Some(ref offer_data) = self.offer_data { + if validated { + offer_data.drop(); + } else { + offer_data.disable(); + } + } + + if !validated { + self.data_source.cancel(); + } else { + self.data_source.drop_performed(); + } + + DndGrabHandler::dropped( + data, + self.current_focus.as_ref().map(into_target), + validated, + self.seat.clone(), + self.last_position, + ); + // in all cases abandon the drop + // no more buttons are pressed, release the grab + if let Some(ref focus) = self.current_focus { + focus.leave(data, self.offer_data.as_mut(), &self.seat); + } + } +} + +impl PointerGrab for DnDGrab +where + D: DndGrabHandler, + D: SeatHandler, + ::PointerFocus: DndFocus + 'static, + D: 'static, + S: Source, +{ + fn motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &PointerMotionEvent, + ) { + handle.motion(data, self.ptr_focus(), event); + + self.last_position = event.location; + + self.update_focus(data, focus, event.location, event.serial, event.time); + } + + fn relative_motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + _focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, self.ptr_focus(), event); + } + + fn button(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, event: &ButtonEvent) { + handle.button(data, event); + + if handle.current_pressed().is_empty() { + // the user dropped, proceed to the drop + self.should_drop = true; + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn axis(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, details: AxisFrame) { + // we just forward the axis events as is + handle.axis(data, details); + } + + fn frame(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &PointerGrabStartData { + self.pointer_start_data.as_ref().unwrap() + } + + fn unset(&mut self, data: &mut D) { + if self.should_drop { + self.drop(data, DndTarget::Pointer); + } else { + self.cancel(data); + } + } +} + +impl TouchGrab for DnDGrab +where + D: DndGrabHandler, + D: SeatHandler, + ::TouchFocus: DndFocus + 'static, + D: 'static, + S: Source, +{ + fn down( + &mut self, + _data: &mut D, + _handle: &mut TouchInnerHandle<'_, D>, + _focus: Option<(::TouchFocus, Point)>, + _event: &DownEvent, + ) { + // Ignore + } + + fn up(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &UpEvent) { + if event.slot != self.start_data().slot { + return; + } + + // the user dropped, proceed to the drop + self.should_drop = true; + handle.unset_grab(self, data); + } + + fn motion( + &mut self, + data: &mut D, + handle: &mut TouchInnerHandle<'_, D>, + focus: Option<(::TouchFocus, Point)>, + event: &TouchMotionEvent, + ) { + if event.slot != self.start_data().slot { + return; + } + + handle.motion(data, self.touch_focus(), event); + + self.last_position = event.location; + + self.update_focus( + data, + focus, + event.location, + SERIAL_COUNTER.next_serial(), + event.time, + ); + } + + fn frame(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>) { + handle.frame(data); + } + + fn cancel(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>) { + handle.unset_grab(self, data); + } + + fn shape( + &mut self, + _data: &mut D, + _handle: &mut TouchInnerHandle<'_, D>, + _event: &crate::input::touch::ShapeEvent, + ) { + } + + fn orientation( + &mut self, + _data: &mut D, + _handle: &mut TouchInnerHandle<'_, D>, + _event: &crate::input::touch::OrientationEvent, + ) { + } + + fn start_data(&self) -> &TouchGrabStartData { + self.touch_start_data.as_ref().unwrap() + } + + fn unset(&mut self, data: &mut D) { + if self.should_drop { + self.drop(data, DndTarget::Touch); + } else { + self.cancel(data); + } + } +} + +#[cfg(not(feature = "xwayland"))] +impl DnDGrab +where + D: DndGrabHandler, + D: SeatHandler, + ::PointerFocus: DndFocus + 'static, + D: 'static, + S: Source, +{ + fn ptr_focus(&self) -> Option<(::PointerFocus, Point)> { + None + } +} + +#[cfg(not(feature = "xwayland"))] +impl DnDGrab +where + D: DndGrabHandler, + D: SeatHandler, + ::TouchFocus: DndFocus + 'static, + D: 'static, + S: Source, +{ + fn touch_focus(&self) -> Option<(::TouchFocus, Point)> { + None + } +} + +#[cfg(feature = "xwayland")] +impl DnDGrab +where + D: DndGrabHandler, + D: SeatHandler, + ::PointerFocus: DndFocus + 'static, + D: 'static, + S: Source, +{ + fn ptr_focus(&self) -> Option<(::PointerFocus, Point)> { + // While the grab is active, we don't want any focus except for xwayland + self.pointer_start_data + .as_ref()? + .focus + .clone() + .filter(|(focus, _)| { + focus.wl_surface().is_some_and(|s| { + s.client() + .is_some_and(|c| c.get_data::().is_some()) + }) + }) + } +} + +#[cfg(feature = "xwayland")] +impl DnDGrab +where + D: DndGrabHandler, + D: SeatHandler, + ::TouchFocus: DndFocus + 'static, + D: 'static, + S: Source, +{ + fn touch_focus(&self) -> Option<(::TouchFocus, Point)> { + // While the grab is active, we don't want any focus except for xwayland + self.touch_start_data + .as_ref()? + .focus + .clone() + .filter(|(focus, _)| { + focus.wl_surface().is_some_and(|s| { + s.client() + .is_some_and(|c| c.get_data::().is_some()) + }) + }) + } +} diff --git a/vendor/smithay/src/input/dnd/mod.rs b/vendor/smithay/src/input/dnd/mod.rs new file mode 100644 index 0000000..334a851 --- /dev/null +++ b/vendor/smithay/src/input/dnd/mod.rs @@ -0,0 +1,175 @@ +//! Drag'n'Drop related types for smithay's input-abstractions + +mod grab; + +use std::{any::Any, os::fd::OwnedFd, sync::Arc}; + +use smallvec::SmallVec; +#[cfg(feature = "wayland_frontend")] +use wayland_server::DisplayHandle; + +#[cfg(feature = "xwayland")] +use crate::wayland::seat::WaylandFocus; +use crate::{ + backend::input::InputTime, + input::{Seat, SeatHandler}, + utils::{IsAlive, Logical, Point, Serial}, +}; + +pub use self::grab::*; + +/// Enumeration of valid actions of a Drag'n'Drop operation +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum DndAction { + /// No action + #[default] + None, + /// Data will be copied + Copy, + /// Data will be moved + Move, + /// User will be asked how to handle the data + Ask, +} + +/// The metadata describing a data source +#[derive(Debug, Clone, PartialEq)] +pub struct SourceMetadata { + /// The MIME types supported by this source + pub mime_types: Vec, + /// The Drag'n'Drop actions supported by this source + pub dnd_actions: SmallVec<[DndAction; 3]>, +} + +impl Default for SourceMetadata { + fn default() -> Self { + Self { + mime_types: Vec::new(), + dnd_actions: SmallVec::new(), + } + } +} + +/// A Drag'n'Drop data source +pub trait Source: IsAlive + Send + Sync + 'static { + /// Method specifically for implementing drag'n'drop operations, + /// which are only visible to a particular client with data + /// being transferred out-of-band. + fn is_client_local(&self, target: &dyn Any) -> bool { + let _ = target; + false + } + + /// Access the metadata associated with this source. + /// + /// If this returns `None` the source is not managed by smithay (e.g. client_local) + fn metadata(&self) -> Option; + /// The target accepted a mime-type, or rejected the offer when `None` + /// + /// Proxying implementations, such as a nested compositor forwarding a + /// drag to its own clients, need this to relay the target's choice to + /// the upstream source. Sources that originate their own data can + /// ignore it. + fn accepted(&self, mime_type: Option) { + let _ = mime_type; + } + /// An action was selected by the target + fn choose_action(&self, action: DndAction); + /// The target requests data to be transferred to the given file descriptor for the given mime-type + fn send(&self, mime_type: &str, fd: OwnedFd); + /// A drop was performed + fn drop_performed(&self); + /// The source is cancelled + fn cancel(&self); + /// The source is done + fn finished(&self); +} + +/// Data associated with an offer of a [`Source`] to a particular target. +pub trait OfferData: Send + 'static { + /// The offer is now considered disabled and not valid anymore + fn disable(&self); + /// The offer is accepted and a drop is being performed + fn drop(&self); + /// Returns whether the offer is still considered valid + fn validated(&self) -> bool; +} + +// We sadly have to duplicate this whole trait for code in `DnDGrab`, +// because conditional trait bounds are not a thing (yet? rust-lang/rust#115590) +#[cfg(feature = "xwayland")] +/// A potential Drag'n'Drop target +pub trait DndFocus: WaylandFocus + IsAlive + PartialEq { + /// OfferData implementation returned by this target + type OfferData: OfferData + where + S: Source; + + /// An active Drag'n'Drop operation has entered the client + fn enter( + &self, + data: &mut D, + #[cfg(feature = "wayland_frontend")] dh: &DisplayHandle, + source: Arc, + seat: &Seat, + location: Point, + serial: &Serial, + ) -> Option>; + + /// An active Drag'n'Drop operation, which has previously + /// entered the client, has been moved + fn motion( + &self, + data: &mut D, + offer: Option<&mut Self::OfferData>, + seat: &Seat, + location: Point, + time: InputTime, + ); + + /// An active Drag'n'Drop operation, which has previously + /// entered the client, left again. + fn leave(&self, data: &mut D, offer: Option<&mut Self::OfferData>, seat: &Seat); + + /// An active Drag'n'Drop operation, which has previously + /// entered the client, has been dropped. + fn drop(&self, data: &mut D, offer: Option<&mut Self::OfferData>, seat: &Seat); +} +#[cfg(not(feature = "xwayland"))] +/// A potential Drag'n'Drop target +pub trait DndFocus: IsAlive + PartialEq { + /// OfferData implementation returned by this target + type OfferData: OfferData + where + S: Source; + + /// An active Drag'n'Drop operation has entered the client + fn enter( + &self, + data: &mut D, + #[cfg(feature = "wayland_frontend")] dh: &DisplayHandle, + source: Arc, + seat: &Seat, + location: Point, + serial: &Serial, + ) -> Option>; + + /// An active Drag'n'Drop operation, which has previously + /// entered the client, has been moved + fn motion( + &self, + data: &mut D, + offer: Option<&mut Self::OfferData>, + seat: &Seat, + location: Point, + time: InputTime, + ); + + /// An active Drag'n'Drop operation, which has previously + /// entered the client, left again. + fn leave(&self, data: &mut D, offer: Option<&mut Self::OfferData>, seat: &Seat); + + /// An active Drag'n'Drop operation, which has previously + /// entered the client, has been dropped. + fn drop(&self, data: &mut D, offer: Option<&mut Self::OfferData>, seat: &Seat); +} diff --git a/vendor/smithay/src/input/keyboard/keymap_file.rs b/vendor/smithay/src/input/keyboard/keymap_file.rs new file mode 100644 index 0000000..e3eecba --- /dev/null +++ b/vendor/smithay/src/input/keyboard/keymap_file.rs @@ -0,0 +1,130 @@ +use std::ffi::CString; +use std::os::unix::io::{AsFd, BorrowedFd}; + +use siphasher::sip128::SipHasher; +use tracing::error; +use xkbcommon::xkb::{self, KEYMAP_FORMAT_TEXT_V1, Keymap}; + +use crate::utils::SealedFile; + +/// Unique ID for a keymap +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct KeymapFileId([u8; 16]); + +impl KeymapFileId { + fn for_keymap(keymap: &str) -> Self { + // Use a hash, so `keymap` events aren't sent when keymap hasn't changed, particularly + // with `virtual-keyboard-unstable-v1`. + Self(SipHasher::new().hash(keymap.as_bytes()).as_bytes()) + } +} + +/// Wraps an XKB keymap into a sealed file or stores as just a string for sending to WlKeyboard over an fd +#[derive(Debug)] +pub struct KeymapFile { + sealed: Option, + keymap: String, + id: KeymapFileId, +} + +impl KeymapFile { + /// Turn the keymap into a string using KEYMAP_FORMAT_TEXT_V1, create a sealed file for it, and store the string + pub fn new(keymap: &Keymap) -> Self { + let name = c"smithay-keymap"; + let keymap = keymap.get_as_string(KEYMAP_FORMAT_TEXT_V1); + let sealed = SealedFile::with_content(name, &CString::new(keymap.as_str()).unwrap()); + + if let Err(err) = sealed.as_ref() { + error!("Error when creating sealed keymap file: {}", err); + } + + Self { + id: KeymapFileId::for_keymap(&keymap), + sealed: sealed.ok(), + keymap, + } + } + + #[cfg(feature = "wayland_frontend")] + pub(crate) fn change_keymap(&mut self, keymap: &Keymap) { + let keymap = keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1); + + let name = c"smithay-keymap-file"; + let sealed = SealedFile::with_content(name, &CString::new(keymap.clone()).unwrap()); + + if let Err(err) = sealed.as_ref() { + error!("Error when creating sealed keymap file: {}", err); + } + + self.id = KeymapFileId::for_keymap(&keymap); + self.sealed = sealed.ok(); + self.keymap = keymap; + } + + #[cfg(feature = "wayland_frontend")] + /// Run a closure with the file descriptor to ensure safety + pub fn with_fd(&self, supports_sealed: bool, cb: F) -> Result<(), std::io::Error> + where + F: FnOnce(BorrowedFd<'_>, usize), + { + use std::{io::Write, path::PathBuf}; + + if let Some(file) = supports_sealed.then_some(self.sealed.as_ref()).flatten() { + cb(file.as_fd(), file.size()); + } else { + let dir = std::env::var_os("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(std::env::temp_dir); + + let mut file = tempfile::tempfile_in(dir)?; + file.write_all(self.keymap.as_bytes())?; + file.flush()?; + + cb(file.as_fd(), self.keymap.len()); + } + Ok(()) + } + + /// Send the keymap contained within to a WlKeyboard + pub fn send( + &self, + keyboard: &wayland_server::protocol::wl_keyboard::WlKeyboard, + ) -> Result<(), std::io::Error> { + use wayland_server::{Resource, protocol::wl_keyboard::KeymapFormat}; + + self.with_fd(keyboard.version() >= 7, |fd, size| { + keyboard.keymap(KeymapFormat::XkbV1, fd, size as u32); + }) + } + + /// Get this keymap's unique ID. + pub(crate) fn id(&self) -> KeymapFileId { + self.id + } +} + +#[cfg(test)] +mod tests { + use super::*; + use xkbcommon::xkb; + + #[test] + fn test_keymap_file_id() { + let id1 = KeymapFileId::for_keymap("keymap data 1"); + let id2 = KeymapFileId::for_keymap("keymap data 2"); + let id3 = KeymapFileId::for_keymap("keymap data 1"); + + assert_ne!(id1, id2); + assert_eq!(id1, id3); + } + + #[test] + fn test_keymap_file_creation() { + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let xkb_config = crate::input::keyboard::XkbConfig::default(); + let keymap = xkb_config.compile_keymap(&context).unwrap(); + let keymap_file = KeymapFile::new(&keymap); + + assert_ne!(keymap_file.id().0, [0_u8; 16]); + } +} diff --git a/vendor/smithay/src/input/keyboard/mod.rs b/vendor/smithay/src/input/keyboard/mod.rs new file mode 100644 index 0000000..9a5e93b --- /dev/null +++ b/vendor/smithay/src/input/keyboard/mod.rs @@ -0,0 +1,1748 @@ +//! Keyboard-related types for smithay's input abstraction + +use crate::backend::input::{InputTime, KeyState}; +use crate::utils::{IsAlive, SERIAL_COUNTER, Serial}; +use downcast_rs::{Downcast, impl_downcast}; +use std::collections::{HashMap, HashSet}; +#[cfg(feature = "wayland_frontend")] +use std::sync::RwLock; +use std::{ + default::Default, + fmt, io, + sync::{Arc, Mutex}, +}; +use thiserror::Error; +use tracing::{debug, info, info_span, instrument, trace}; + +use xkbcommon::xkb::ffi::XKB_STATE_LAYOUT_EFFECTIVE; +pub use xkbcommon::xkb::{self, ContextFlags, Keycode, Keysym, keysyms}; + +use super::{GrabStatus, Seat, SeatHandler}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::{Resource, Weak}; +#[cfg(feature = "wayland_frontend")] +mod keymap_file; +#[cfg(feature = "wayland_frontend")] +pub use keymap_file::{KeymapFile, KeymapFileId}; + +mod modifiers_state; +pub use modifiers_state::{ModifiersState, SerializedMods}; + +mod xkb_config; +pub use xkb_config::XkbConfig; + +/// Trait representing object that can receive keyboard interactions +pub trait KeyboardTarget: IsAlive + fmt::Debug + Send +where + D: SeatHandler, +{ + /// Keyboard focus of a given seat was assigned to this handler + fn enter(&self, seat: &Seat, data: &mut D, keys: Vec>, serial: Serial); + /// The keyboard focus of a given seat left this handler + fn leave(&self, seat: &Seat, data: &mut D, serial: Serial); + /// A key was pressed on a keyboard from a given seat + fn key( + &self, + seat: &Seat, + data: &mut D, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: InputTime, + ); + /// Hold modifiers were changed on a keyboard from a given seat + fn modifiers(&self, seat: &Seat, data: &mut D, modifiers: ModifiersState, serial: Serial); + /// Keyboard focus of a given seat moved from another handler to this handler + fn replace( + &self, + replaced: ::KeyboardFocus, + seat: &Seat, + data: &mut D, + keys: Vec>, + modifiers: ModifiersState, + serial: Serial, + ) { + KeyboardTarget::::leave(&replaced, seat, data, serial); + KeyboardTarget::::enter(self, seat, data, keys, serial); + KeyboardTarget::::modifiers(self, seat, data, modifiers, serial); + } +} + +/// Mapping of the led of a keymap +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LedMapping { + /// Index of the NUMLOCK led + pub num: Option, + /// Index of the CAPSLOCK led + pub caps: Option, + /// Index of the SCROLLLOCK led + pub scroll: Option, +} + +impl LedMapping { + /// Get the mapping from a keymap + pub fn from_keymap(keymap: &xkb::Keymap) -> Self { + Self { + num: match keymap.led_get_index(xkb::LED_NAME_NUM) { + xkb::LED_INVALID => None, + index => Some(index), + }, + caps: match keymap.led_get_index(xkb::LED_NAME_CAPS) { + xkb::LED_INVALID => None, + index => Some(index), + }, + scroll: match keymap.led_get_index(xkb::LED_NAME_SCROLL) { + xkb::LED_INVALID => None, + index => Some(index), + }, + } + } +} + +/// Current state of the led when available +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub struct LedState { + /// State of NUMLOCK led + pub num: Option, + /// State of CAPSLOCK led + pub caps: Option, + /// State of SCROLLLOCK led + pub scroll: Option, +} + +impl LedState { + /// Update the led state from an xkb state and mapping + /// + /// Returns whether the led state changed + pub fn update_with(&mut self, state: &xkb::State, mapping: &LedMapping) -> bool { + let previous_state = *self; + self.num = mapping.num.map(|idx| state.led_index_is_active(idx)); + self.caps = mapping.caps.map(|idx| state.led_index_is_active(idx)); + self.scroll = mapping.scroll.map(|idx| state.led_index_is_active(idx)); + *self != previous_state + } + + /// Initialize the led state from an xkb state and mapping + pub fn from_state(state: &xkb::State, mapping: &LedMapping) -> Self { + let mut led_state = LedState::default(); + led_state.update_with(state, mapping); + led_state + } +} + +/// An xkbcommon context, keymap, and state, that can be sent to another +/// thread, but should not have additional ref-counts kept on one thread. +pub struct Xkb { + context: xkb::Context, + keymap: xkb::Keymap, + state: xkb::State, +} + +impl Xkb { + /// The xkbcommon context. + /// + /// # Safety + /// A ref-count of the context should not outlive the `Xkb` + pub unsafe fn context(&self) -> &xkb::Context { + &self.context + } + + /// The xkbcommon keymap. + /// + /// # Safety + /// A ref-count of the keymap should not outlive the `Xkb` + pub unsafe fn keymap(&self) -> &xkb::Keymap { + &self.keymap + } + + /// The xkbcommon state. + /// + /// # Safety + /// A ref-count of the state should not outlive the `Xkb` + pub unsafe fn state(&self) -> &xkb::State { + &self.state + } + + /// The active keymap as `XKB_KEYMAP_FORMAT_TEXT_V1`. + /// + /// This identical to what gets sent to clients, so it can be compared + /// against keymaps received from them. + pub fn keymap_as_string(&self) -> String { + self.keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1) + } + + /// Get the active layout of the keyboard. + pub fn active_layout(&self) -> Layout { + (0..self.keymap.num_layouts()) + .find(|&idx| self.state.layout_index_is_active(idx, XKB_STATE_LAYOUT_EFFECTIVE)) + .map(Layout) + .unwrap_or_default() + } + + /// Get the human readable name for the layout. + pub fn layout_name(&self, layout: Layout) -> &str { + self.keymap.layout_get_name(layout.0) + } + + /// Iterate over layouts present in the keymap. + pub fn layouts(&self) -> impl Iterator + use<> { + (0..self.keymap.num_layouts()).map(Layout) + } + + /// Returns the syms for the underlying keycode without any modifications by the current keymap + /// state applied. + pub fn raw_syms_for_key_in_layout(&self, keycode: Keycode, layout: Layout) -> &[Keysym] { + self.keymap.key_get_syms_by_level(keycode, layout.0, 0) + } +} + +impl fmt::Debug for Xkb { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Xkb") + .field("context", &self.context.get_raw_ptr()) + .field("keymap", &self.keymap.get_raw_ptr()) + .field("state", &self.state.get_raw_ptr()) + .finish() + } +} + +// This is OK because all parts of `xkb` will remain on the +// same thread +unsafe impl Send for Xkb {} + +/// Identifies which input source a key event comes from. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeyboardSource { + /// The physical keyboard(s) driven directly by the compositor's input backend. + Physical, + /// An auxiliary input source (e.g. a `zwp_virtual_keyboard_v1` instance or a libei + /// connection), distinguished by a compositor-assigned opaque id. + Auxiliary(u64), +} + +impl KeyboardSource { + /// The default source used by [`KeyboardHandle::input`] (the physical keyboard). + pub const MAIN: KeyboardSource = KeyboardSource::Physical; + + /// Mint a fresh, process-unique auxiliary source. + pub fn new_auxiliary() -> Self { + static NEXT_AUX_SOURCE: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(1); + KeyboardSource::Auxiliary(NEXT_AUX_SOURCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) + } +} + +pub(crate) struct KbdInternal { + pub(crate) focus: Option<(::KeyboardFocus, Serial)>, + pending_focus: Option<::KeyboardFocus>, + pub(crate) pressed_keys: HashSet, + pub(crate) key_sources: HashMap>, + pub(crate) forwarded_pressed_keys: HashSet, + pub(crate) mods_state: ModifiersState, + xkb: Arc>, + pub(crate) repeat_rate: i32, + pub(crate) repeat_delay: i32, + led_mapping: LedMapping, + pub(crate) led_state: LedState, + grab: GrabStatus>, +} + +// focus_hook does not implement debug, so we have to impl Debug manually +impl fmt::Debug for KbdInternal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KbdInternal") + .field("focus", &self.focus) + .field("pending_focus", &self.pending_focus) + .field("pressed_keys", &self.pressed_keys) + .field("forwarded_pressed_keys", &self.forwarded_pressed_keys) + .field("mods_state", &self.mods_state) + .field("xkb", &self.xkb) + .field("repeat_rate", &self.repeat_rate) + .field("repeat_delay", &self.repeat_delay) + .finish() + } +} + +// This is OK because all parts of `xkb` will remain on the +// same thread +unsafe impl Send for KbdInternal {} + +impl KbdInternal { + fn new( + xkb_config: XkbConfig<'_>, + repeat_rate: i32, + repeat_delay: i32, + context_flags: ContextFlags, + ) -> Result, ()> { + // we create a new context for each keyboard because libxkbcommon is actually NOT threadsafe + // so confining it inside the KbdInternal allows us to use Rusts mutability rules to make + // sure nothing goes wrong. + // + // FIXME: This is an issue with the xkbcommon-rs crate that does not reflect this + // non-threadsafety properly. + let context = xkb::Context::new(context_flags); + let keymap = xkb_config.compile_keymap(&context)?; + let state = xkb::State::new(&keymap); + let led_mapping = LedMapping::from_keymap(&keymap); + let led_state = LedState::from_state(&state, &led_mapping); + Ok(KbdInternal { + focus: None, + pending_focus: None, + pressed_keys: HashSet::new(), + key_sources: HashMap::new(), + forwarded_pressed_keys: HashSet::new(), + mods_state: ModifiersState::default(), + xkb: Arc::new(Mutex::new(Xkb { + context, + keymap, + state, + })), + repeat_rate, + repeat_delay, + led_mapping, + led_state, + grab: GrabStatus::None, + }) + } + + // Feed a key event from `source` into the shared seat state. Returns + // `(modifiers_changed, leds_changed, is_transition)`. + // + // `is_transition` is `true` only when this event actually changes the combined pressed set + // i.e. the first source to press a keycode, or the last source to release it. + fn key_input(&mut self, source: KeyboardSource, keycode: Keycode, state: KeyState) -> (bool, bool, bool) { + // track pressed keys per source, the seat xkb only follows the *combined* set + let direction = match state { + KeyState::Pressed => { + let holders = self.key_sources.entry(keycode).or_default(); + let was_held = !holders.is_empty(); + holders.insert(source); + if was_held { + // already down from another source: absorb + return (false, false, false); + } + self.pressed_keys.insert(keycode); + xkb::KeyDirection::Down + } + KeyState::Released => { + match self.key_sources.get_mut(&keycode) { + Some(holders) => { + holders.remove(&source); + if !holders.is_empty() { + // still held by another source: absorb + return (false, false, false); + } + self.key_sources.remove(&keycode); + self.pressed_keys.remove(&keycode); + xkb::KeyDirection::Up + } + // not tracked as held: absorb + None => return (false, false, false), + } + } + }; + + // update state + // Offset the keycode by 8, as the evdev XKB rules reflect X's + // broken keycode system, which starts at 8. + let mut xkb = self.xkb.lock().unwrap(); + let state_components = xkb.state.update_key(keycode, direction); + let modifiers_changed = state_components != 0; + if modifiers_changed { + self.mods_state.update_with(&xkb.state); + } + let leds_changed = self.led_state.update_with(&xkb.state, &self.led_mapping); + (modifiers_changed, leds_changed, true) + } + + /// Release every keycode currently held by `source`, as if the source sent a release for + /// each. A keycode only actually transitions up (and gets forwarded) if no other source is + /// still holding it. Returns the keycodes that transitioned up, so the caller can forward + /// the releases to the focused client. + fn release_source_keys(&mut self, source: KeyboardSource) -> Vec { + let held: Vec = self + .key_sources + .iter() + .filter(|(_, holders)| holders.contains(&source)) + .map(|(keycode, _)| *keycode) + .collect(); + let mut transitioned = Vec::new(); + for keycode in held { + let (_, _, is_transition) = self.key_input(source, keycode, KeyState::Released); + if is_transition { + transitioned.push(keycode); + } + } + transitioned + } + + fn with_grab(&mut self, data: &mut D, seat: &Seat, f: F) + where + F: FnOnce(&mut D, &mut KeyboardInnerHandle<'_, D>, &mut dyn KeyboardGrab), + { + let mut grab = std::mem::replace(&mut self.grab, GrabStatus::Borrowed); + match grab { + GrabStatus::Borrowed => panic!("Accessed a keyboard grab from within a keyboard grab access."), + GrabStatus::Active(_, ref mut handler) => { + // If this grab is associated with a surface that is no longer alive, discard it + if let Some(ref surface) = handler.start_data().focus { + if !surface.alive() { + handler.unset(data); + self.grab = GrabStatus::None; + f( + data, + &mut KeyboardInnerHandle { inner: self, seat }, + &mut DefaultGrab, + ); + return; + } + } + f( + data, + &mut KeyboardInnerHandle { inner: self, seat }, + &mut **handler, + ); + } + GrabStatus::None => { + f( + data, + &mut KeyboardInnerHandle { inner: self, seat }, + &mut DefaultGrab, + ); + } + } + + if let GrabStatus::Borrowed = self.grab { + // the grab has not been ended nor replaced, put it back in place + self.grab = grab; + } + } +} + +/// Errors that can be encountered when creating a keyboard handler +#[derive(Debug, Error)] +pub enum Error { + /// libxkbcommon could not load the specified keymap + #[error("Libxkbcommon could not load the specified keymap")] + BadKeymap, + /// Smithay could not create a tempfile to share the keymap with clients + #[error("Failed to create tempfile to share the keymap: {0}")] + IoError(io::Error), +} + +pub(crate) struct KbdRc { + pub(crate) internal: Mutex>, + #[cfg(feature = "wayland_frontend")] + pub(crate) keymap: Mutex, + #[cfg(feature = "wayland_frontend")] + pub(crate) known_kbds: Mutex>>, + #[cfg(feature = "wayland_frontend")] + pub(crate) last_enter: Mutex>, + pub(crate) span: tracing::Span, + #[cfg(feature = "wayland_frontend")] + pub(crate) active_keymap: RwLock, +} + +#[cfg(not(feature = "wayland_frontend"))] +impl fmt::Debug for KbdRc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KbdRc").field("internal", &self.internal).finish() + } +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for KbdRc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KbdRc") + .field("internal", &self.internal) + .field("keymap", &self.keymap) + .field("known_kbds", &self.known_kbds) + .field("last_enter", &self.last_enter) + .finish() + } +} + +/// Handle to the underlying keycode to allow for different conversions +pub struct KeysymHandle<'a> { + xkb: &'a Mutex, + keycode: Keycode, +} + +impl fmt::Debug for KeysymHandle<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.keycode) + } +} + +impl<'a> KeysymHandle<'a> { + /// Get the reference to the xkb state. + pub fn xkb(&self) -> &Mutex { + self.xkb + } + + /// Returns the sym for the underlying keycode with all modifications by the current keymap state applied. + /// + /// This function is similar to [`KeysymHandle::modified_syms`], but is intended for cases where the user + /// does not want to or cannot handle multiple keysyms. + /// + /// If the key does not have exactly one keysym, returns [`keysyms::KEY_NoSymbol`]. + pub fn modified_sym(&self) -> Keysym { + self.xkb.lock().unwrap().state.key_get_one_sym(self.keycode) + } + + /// Returns the syms for the underlying keycode with all modifications by the current keymap state applied. + pub fn modified_syms(&self) -> Vec { + self.xkb.lock().unwrap().state.key_get_syms(self.keycode).to_vec() + } + + /// Returns the syms for the underlying keycode without any modifications by the current keymap state applied. + pub fn raw_syms(&self) -> Vec { + let xkb = self.xkb.lock().unwrap(); + xkb.keymap + .key_get_syms_by_level(self.keycode, xkb.state.key_get_layout(self.keycode), 0) + .to_vec() + } + + /// Get the raw latin keysym or fallback to current raw keysym. + /// + /// This method is handy to implement layout agnostic bindings. Keep in mind that + /// it could be not-ideal to use just this function, since some layouts utilize non-standard + /// shift levels and you should look into [`Self::modified_sym`] first. + /// + /// The `None` is returned when the underlying keycode doesn't produce a valid keysym. + pub fn raw_latin_sym_or_raw_current_sym(&self) -> Option { + let xkb = self.xkb.lock().unwrap(); + let effective_layout = Layout(xkb.state.key_get_layout(self.keycode)); + + // don't call `self.raw_syms()` to avoid a deadlock + // and an unnecessary allocation into a Vec + let raw_syms = + xkb.keymap + .key_get_syms_by_level(self.keycode, xkb.state.key_get_layout(self.keycode), 0); + // NOTE: There's always a keysym in the current layout given that we have modified_sym. + let base_sym = *raw_syms.first()?; + + // If the character is ascii or non-printable, return it. + if base_sym.key_char().map(|ch| ch.is_ascii()).unwrap_or(true) { + return Some(base_sym); + }; + + // Try to look other layouts and find the one with ascii character. + for layout in xkb.layouts() { + if layout == effective_layout { + continue; + } + + if let Some(keysym) = xkb.raw_syms_for_key_in_layout(self.keycode, layout).first() { + // NOTE: Only check for ascii non-control characters, since control ones are + // layout agnostic. + if keysym + .key_char() + .map(|key| key.is_ascii() && !key.is_ascii_control()) + .unwrap_or(false) + { + return Some(*keysym); + } + } + } + + Some(base_sym) + } + + /// Returns the raw code in X keycode system (shifted by 8) + pub fn raw_code(&'a self) -> Keycode { + self.keycode + } +} + +/// The currently active state of the Xkb. +pub struct XkbContext<'a> { + xkb: &'a Mutex, + mods_state: &'a mut ModifiersState, + mods_changed: &'a mut bool, + leds_state: &'a mut LedState, + leds_changed: &'a mut bool, + leds_mapping: &'a LedMapping, +} + +impl XkbContext<'_> { + /// Get the reference to the xkb state. + pub fn xkb(&self) -> &Mutex { + self.xkb + } + + /// Set layout of the keyboard to the given index. + pub fn set_layout(&mut self, layout: Layout) { + let mut xkb = self.xkb.lock().unwrap(); + + let state = xkb.state.update_mask( + self.mods_state.serialized.depressed, + self.mods_state.serialized.latched, + self.mods_state.serialized.locked, + 0, + 0, + layout.0, + ); + + if state != 0 { + self.mods_state.update_with(&xkb.state); + *self.mods_changed = true; + } + + *self.leds_changed = self.leds_state.update_with(&xkb.state, self.leds_mapping); + } + + /// Set the modifier state from raw masks. + /// + /// The arguments are the raw xkb depressed, latched and locked modifier + /// masks, and the locked layout group index, e.g. as specified by the + /// `modifiers` request of `zwp_virtual_keyboard_v1`. + pub fn set_modifier_mask( + &mut self, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + group: u32, + ) { + let mut xkb = self.xkb.lock().unwrap(); + + let state = xkb + .state + .update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group); + + if state != 0 { + self.mods_state.update_with(&xkb.state); + *self.mods_changed = true; + } + + *self.leds_changed = self.leds_state.update_with(&xkb.state, self.leds_mapping); + } + + /// Switches layout forward cycling when it reaches the end. + pub fn cycle_next_layout(&mut self) { + let xkb = self.xkb.lock().unwrap(); + let next_layout = (xkb.active_layout().0 + 1) % xkb.keymap.num_layouts(); + drop(xkb); + self.set_layout(Layout(next_layout)); + } + + /// Switches layout backward cycling when it reaches the start. + pub fn cycle_prev_layout(&mut self) { + let xkb = self.xkb.lock().unwrap(); + let num_layouts = xkb.keymap.num_layouts(); + let next_layout = (num_layouts + xkb.active_layout().0 - 1) % num_layouts; + drop(xkb); + self.set_layout(Layout(next_layout)); + } +} + +impl fmt::Debug for XkbContext<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("XkbContext") + .field("mods_state", &self.mods_state) + .field("mods_changed", &self.mods_changed) + .finish() + } +} + +/// Reference to the XkbLayout in the active keymap. +/// +/// The layout may become invalid after calling [`KeyboardHandle::set_xkb_config`] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Layout(pub xkb::LayoutIndex); + +/// Result for key input filtering (see [`KeyboardHandle::input`]) +#[derive(Debug)] +pub enum FilterResult { + /// Forward the given keycode to the client + Forward, + /// Do not forward and return value + Intercept(T), +} + +/// Data about the event that started the grab. +pub struct GrabStartData { + /// The focused surface, if any, at the start of the grab. + pub focus: Option<::KeyboardFocus>, +} + +impl fmt::Debug for GrabStartData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GrabStartData") + .field("focus", &self.focus) + .finish() + } +} + +impl Clone for GrabStartData { + fn clone(&self) -> Self { + GrabStartData { + focus: self.focus.clone(), + } + } +} + +/// A trait to implement a keyboard grab +/// +/// In some context, it is necessary to temporarily change the behavior of the keyboard. This is +/// typically known as a keyboard grab. A example would be, during a popup grab the keyboard focus +/// will not be changed and stay on the grabbed popup. +/// +/// This trait is the interface to intercept regular keyboard events and change them as needed, its +/// interface mimics the [`KeyboardHandle`] interface. +/// +/// If your logic decides that the grab should end, both [`KeyboardInnerHandle`] and [`KeyboardHandle`] have +/// a method to change it. +/// +/// When your grab ends (either as you requested it or if it was forcefully cancelled by the server), +/// the struct implementing this trait will be dropped. As such you should put clean-up logic in the destructor, +/// rather than trying to guess when the grab will end. +pub trait KeyboardGrab: Downcast { + /// An input was reported. + /// + /// `modifiers` are only passed when their state actually changes. The modifier must be + /// sent after the key event. + #[allow(clippy::too_many_arguments)] + fn input( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + keycode: Keycode, + state: KeyState, + modifiers: Option, + serial: Serial, + time: InputTime, + ); + + /// A focus change was requested. + fn set_focus( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + focus: Option<::KeyboardFocus>, + serial: Serial, + ); + + /// The data about the event that started the grab. + fn start_data(&self) -> &GrabStartData; + + /// The grab has been unset or replaced with another grab. + fn unset(&mut self, data: &mut D); +} + +impl_downcast!(KeyboardGrab where D: SeatHandler); + +/// An handle to a keyboard handler +/// +/// It can be cloned and all clones manipulate the same internal state. +/// +/// This handle gives you 2 main ways to interact with the keyboard handling: +/// +/// - set the current focus for this keyboard: designing the surface that will receive the key inputs +/// using the [`KeyboardHandle::set_focus`] method. +/// - process key inputs from the input backend, allowing them to be caught at the compositor-level +/// or forwarded to the client. See the documentation of the [`KeyboardHandle::input`] method for +/// details. +pub struct KeyboardHandle { + pub(crate) arc: Arc>, +} + +impl fmt::Debug for KeyboardHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KeyboardHandle").field("arc", &self.arc).finish() + } +} + +impl Clone for KeyboardHandle { + #[inline] + fn clone(&self) -> Self { + KeyboardHandle { + arc: self.arc.clone(), + } + } +} + +impl ::std::cmp::PartialEq for KeyboardHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} + +impl KeyboardHandle { + /// Create a keyboard handler from a set of RMLVO rules + pub(crate) fn new( + xkb_config: XkbConfig<'_>, + repeat_delay: i32, + repeat_rate: i32, + context_flags: ContextFlags, + ) -> Result { + let span = info_span!("input_keyboard"); + let _guard = span.enter(); + + info!("Initializing a xkbcommon handler with keymap query"); + let internal = + KbdInternal::new(xkb_config, repeat_rate, repeat_delay, context_flags).map_err(|_| { + debug!("Loading keymap failed"); + Error::BadKeymap + })?; + + let xkb = internal.xkb.lock().unwrap(); + + info!(name = xkb.keymap.layouts().next(), "Loaded Keymap"); + + #[cfg(feature = "wayland_frontend")] + let keymap_file = KeymapFile::new(&xkb.keymap); + #[cfg(feature = "wayland_frontend")] + let active_keymap = keymap_file.id(); + + drop(xkb); + drop(_guard); + Ok(Self { + arc: Arc::new(KbdRc { + #[cfg(feature = "wayland_frontend")] + keymap: Mutex::new(keymap_file), + internal: Mutex::new(internal), + #[cfg(feature = "wayland_frontend")] + known_kbds: Mutex::new(Vec::new()), + #[cfg(feature = "wayland_frontend")] + last_enter: Mutex::new(None), + #[cfg(feature = "wayland_frontend")] + active_keymap: RwLock::new(active_keymap), + span, + }), + }) + } + + #[cfg(feature = "wayland_frontend")] + #[instrument(parent = &self.arc.span, skip(self, data, keymap))] + pub(crate) fn change_keymap( + &self, + data: &mut D, + focus: &Option<&mut ::KeyboardFocus>, + keymap: &xkb::Keymap, + mods: ModifiersState, + ) { + let mut keymap_file = self.arc.keymap.lock().unwrap(); + keymap_file.change_keymap(keymap); + + self.send_keymap(data, focus, &keymap_file, mods); + } + + /// Send a new wl_keyboard keymap, without updating the internal keymap. + /// + /// Returns `true` if the keymap changed from the previous keymap. + #[cfg(feature = "wayland_frontend")] + #[instrument(parent = &self.arc.span, skip(self, data, keymap_file))] + pub(crate) fn send_keymap( + &self, + data: &mut D, + focus: &Option<&mut ::KeyboardFocus>, + keymap_file: &KeymapFile, + mods: ModifiersState, + ) -> bool { + use std::os::unix::io::AsFd; + use tracing::warn; + use wayland_server::{Resource, protocol::wl_keyboard::KeymapFormat}; + + // Ignore request which do not change the keymap. + let new_id = keymap_file.id(); + if new_id == *self.arc.active_keymap.read().unwrap() { + return false; + } + *self.arc.active_keymap.write().unwrap() = new_id; + + // Update keymap for every wl_keyboard. + let known_kbds = &self.arc.known_kbds; + for kbd in &*known_kbds.lock().unwrap() { + let Ok(kbd) = kbd.upgrade() else { + continue; + }; + + let res = keymap_file.with_fd(kbd.version() >= 7, |fd, size| { + kbd.keymap(KeymapFormat::XkbV1, fd.as_fd(), size as u32) + }); + if let Err(e) = res { + warn!( + err = ?e, + "Failed to send keymap to client" + ); + } + } + + // Send updated modifiers. + let seat = self.get_seat(data); + if let Some(focus) = focus { + focus.modifiers(&seat, data, mods, SERIAL_COUNTER.next_serial()); + } + + true + } + + fn update_xkb_state(&self, data: &mut D, keymap: xkb::Keymap) { + let mut internal = self.arc.internal.lock().unwrap(); + + let mut state = xkb::State::new(&keymap); + for key in &internal.pressed_keys { + state.update_key(*key, xkb::KeyDirection::Down); + } + + let led_mapping = LedMapping::from_keymap(&keymap); + internal.led_mapping = led_mapping; + internal.mods_state.update_with(&state); + let leds_changed = internal.led_state.update_with(&state, &led_mapping); + let mut xkb = internal.xkb.lock().unwrap(); + xkb.keymap = keymap.clone(); + xkb.state = state; + drop(xkb); + + let mods = internal.mods_state; + let focus = internal.focus.as_mut().map(|(focus, _)| focus); + + #[cfg(not(feature = "wayland_frontend"))] + if let Some(focus) = focus.as_ref() { + let seat = self.get_seat(data); + focus.modifiers(&seat, data, mods, SERIAL_COUNTER.next_serial()); + }; + + #[cfg(feature = "wayland_frontend")] + self.change_keymap(data, &focus, &keymap, mods); + + if leds_changed { + let led_state = internal.led_state; + std::mem::drop(internal); + let seat = self.get_seat(data); + data.led_state_changed(&seat, led_state); + } + } + + /// Change the [`Keymap`](xkb::Keymap) used by the keyboard. + /// + /// The input is a keymap in XKB_KEYMAP_FORMAT_TEXT_V1 format. + pub fn set_keymap_from_string(&self, data: &mut D, keymap: String) -> Result<(), Error> { + // Construct the Keymap internally instead of accepting one as input + // because libxkbcommon is not thread-safe. + let keymap = xkb::Keymap::new_from_string( + &self.arc.internal.lock().unwrap().xkb.lock().unwrap().context, + keymap, + xkb::KEYMAP_FORMAT_TEXT_V1, + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) + .ok_or_else(|| { + debug!("Loading keymap from string failed"); + Error::BadKeymap + })?; + self.update_xkb_state(data, keymap); + Ok(()) + } + + /// Change the [`XkbConfig`] used by the keyboard. + pub fn set_xkb_config(&self, data: &mut D, xkb_config: XkbConfig<'_>) -> Result<(), Error> { + let keymap = xkb_config + .compile_keymap(&self.arc.internal.lock().unwrap().xkb.lock().unwrap().context) + .map_err(|_| { + debug!("Loading keymap from XkbConfig failed"); + Error::BadKeymap + })?; + self.update_xkb_state(data, keymap); + Ok(()) + } + + /// Access the underlying Xkb state and perform mutable operations on it, like + /// changing layouts. + /// + /// The changes to the state are automatically broadcasted to the focused client on exit. + pub fn with_xkb_state(&self, data: &mut D, mut callback: F) -> T + where + F: FnMut(XkbContext<'_>) -> T, + { + let (result, new_led_state) = { + let internal = &mut *self.arc.internal.lock().unwrap(); + let mut mods_changed = false; + let mut leds_changed = false; + let state = XkbContext { + mods_state: &mut internal.mods_state, + xkb: &mut internal.xkb, + mods_changed: &mut mods_changed, + leds_state: &mut internal.led_state, + leds_changed: &mut leds_changed, + leds_mapping: &internal.led_mapping, + }; + + let result = callback(state); + + if mods_changed { + if let Some((focus, _)) = internal.focus.as_mut() { + let seat = self.get_seat(data); + focus.modifiers(&seat, data, internal.mods_state, SERIAL_COUNTER.next_serial()); + }; + } + + (result, leds_changed.then_some(internal.led_state)) + }; + + if let Some(led_state) = new_led_state { + let seat = self.get_seat(data); + data.led_state_changed(&seat, led_state) + } + + result + } + + /// Change the current grab on this keyboard to the provided grab + /// + /// Overwrites any current grab. + pub fn set_grab + 'static>(&self, data: &mut D, grab: G, serial: Serial) { + let mut inner = self.arc.internal.lock().unwrap(); + if let GrabStatus::Active(_, handler) = &mut inner.grab { + handler.unset(data); + } + inner.grab = GrabStatus::Active(serial, Box::new(grab)); + } + + /// Remove any current grab on this keyboard, resetting it to the default behavior + pub fn unset_grab(&self, data: &mut D) { + let mut inner = self.arc.internal.lock().unwrap(); + if let GrabStatus::Active(_, handler) = &mut inner.grab { + handler.unset(data); + } + inner.grab = GrabStatus::None; + } + + /// Check if this keyboard is currently grabbed with this serial + pub fn has_grab(&self, serial: Serial) -> bool { + self.with_grab(|s, _| s == serial).unwrap_or(false) + } + + /// Check if this keyboard is currently being grabbed + pub fn is_grabbed(&self) -> bool { + let guard = self.arc.internal.lock().unwrap(); + !matches!(guard.grab, GrabStatus::None) + } + + /// Returns the start data for the grab, if any. + pub fn grab_start_data(&self) -> Option> { + self.with_grab(|_, g| g.start_data().clone()) + } + + /// Calls `f` with the active grab, if any. + pub fn with_grab(&self, f: impl FnOnce(Serial, &dyn KeyboardGrab) -> T) -> Option { + let guard = self.arc.internal.lock().unwrap(); + if let GrabStatus::Active(s, g) = &guard.grab { + Some(f(*s, &**g)) + } else { + None + } + } + + /// Handle a keystroke + /// + /// All keystrokes from the input backend should be fed _in order_ to this method of the + /// keyboard handler. It will internally track the state of the keymap. + /// + /// The `filter` argument is expected to be a closure which will peek at the generated input + /// as interpreted by the keymap before it is forwarded to the focused client. If this closure + /// returns [`FilterResult::Forward`], the input will be sent to the client. If it returns + /// [`FilterResult::Intercept`], a value can be passed to be returned by the whole function. + /// This mechanism can be used to implement compositor-level key bindings for example. + /// + /// The module [`keysyms`] exposes definitions of all possible keysyms to be compared against. + /// This includes non-character keysyms, such as XF86 special keys. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data, filter))] + pub fn input( + &self, + data: &mut D, + keycode: Keycode, + state: KeyState, + serial: Serial, + time: InputTime, + filter: F, + ) -> Option + where + F: FnOnce(&mut D, &ModifiersState, KeysymHandle<'_>) -> FilterResult, + { + self.input_from_source(KeyboardSource::MAIN, data, keycode, state, serial, time, filter) + } + + /// Like [`KeyboardHandle::input`], but attributes the event to a specific [`KeyboardSource`]. + #[allow(clippy::too_many_arguments)] + pub fn input_from_source( + &self, + source: KeyboardSource, + data: &mut D, + keycode: Keycode, + state: KeyState, + serial: Serial, + time: InputTime, + filter: F, + ) -> Option + where + F: FnOnce(&mut D, &ModifiersState, KeysymHandle<'_>) -> FilterResult, + { + let (filter_result, mods_changed) = self.input_intercept(source, data, keycode, state, filter)?; + if let FilterResult::Intercept(val) = filter_result { + trace!("Input was intercepted by filter"); + return Some(val); + } + + self.input_forward(data, keycode, state, serial, time, mods_changed); + None + } + + /// Release every key currently held by `source` (e.g. when a virtual keyboard is destroyed + /// or a libei connection drops), forwarding the resulting releases to the focused client. + /// + /// This is a teardown-only path: the releases are forwarded directly and do not run + /// the compositor's input filter, so a departing source can't trigger a key binding + pub fn release_source(&self, data: &mut D, source: KeyboardSource) { + let (transitioned, mods) = { + let mut guard = self.arc.internal.lock().unwrap(); + let transitioned = guard.release_source_keys(source); + (transitioned, guard.mods_state) + }; + let _ = mods; + let serial = SERIAL_COUNTER.next_serial(); + let time = InputTime::now(); + for keycode in transitioned { + // modifiers may have changed as a modifier key was released; let input_forward + // re-derive and send the current state. + self.input_forward(data, keycode, KeyState::Released, serial, time, true); + } + } + + /// Update the state of the keyboard without forwarding the event to the focused client + /// + /// Useful in conjunction with [`KeyboardHandle::input_forward`] in case you want + /// to asynchronously decide if the event should be forwarded to the focused client. + /// + /// Prefer using [`KeyboardHandle::input`] or [`KeyboardHandle::input_from_source`] if this + /// decision can be done synchronously in the `filter` closure. + /// + /// `None` return means that the input was absorbed because it was already held/released + /// beforehand, and must not be passed to [`KeyboardHandle::input_forward`]. + pub fn input_intercept( + &self, + source: KeyboardSource, + data: &mut D, + keycode: Keycode, + state: KeyState, + filter: F, + ) -> Option<(T, bool)> + where + F: FnOnce(&mut D, &ModifiersState, KeysymHandle<'_>) -> T, + { + trace!("Handling keystroke"); + + let mut guard = self.arc.internal.lock().unwrap(); + let (mods_changed, leds_changed, is_transition) = guard.key_input(source, keycode, state); + let led_state = guard.led_state; + let mods_state = guard.mods_state; + let xkb = guard.xkb.clone(); + std::mem::drop(guard); + + if leds_changed { + let seat = self.get_seat(data); + data.led_state_changed(&seat, led_state); + } + + // The event was absorbed because another source is holding this keycode: don't + // double-run the filter (avoids re-triggering shortcuts) and don't forward a duplicate. + if !is_transition { + // If is_transition is false, the key hasn't hit xkb, so mods_changed cannot be true. + debug_assert!( + !mods_changed, + "mods cannot change for a key already held/released" + ); + return None; + } + + let key_handle = KeysymHandle { xkb: &xkb, keycode }; + trace!(mods_state = ?mods_state, sym = xkb::keysym_get_name(key_handle.modified_sym()), "Calling input filter"); + let filter_result = filter(data, &mods_state, key_handle); + + Some((filter_result, mods_changed)) + } + + /// Forward a key event to the focused client + /// + /// Useful in conjunction with [`KeyboardHandle::input_intercept`]. + pub fn input_forward( + &self, + data: &mut D, + keycode: Keycode, + state: KeyState, + serial: Serial, + time: InputTime, + mods_changed: bool, + ) { + self.forward(data, keycode, state, serial, time, mods_changed, false); + } + + /// Forward a key event to the focused client, bypassing the active grab, if any. + /// + /// This can be used by compositors to prevent infinite loops when a client + /// with a grab (e.g., fcitx) forwards a key through its own virtual keyboard, which + /// can be tracked with `InputMethodHandle::keyboard_grab_client`. + /// + /// Like [`KeyboardHandle::input_forward`], this doesn't update the xkb state, and + /// `mods_changed` only controls whether a `modifiers` event is sent along with the + /// key. While a grab is active the focus doesn't receive modifier updates (they go + /// to the grab), so the compositor has no way of knowing if the client's modifiers + /// are stale, and it's safe to pass `true` in this case, since a redundant + /// `modifiers` event is harmless. + pub fn input_forward_bypassing_grab( + &self, + data: &mut D, + keycode: Keycode, + state: KeyState, + serial: Serial, + time: InputTime, + mods_changed: bool, + ) { + self.forward(data, keycode, state, serial, time, mods_changed, true); + } + + #[allow(clippy::too_many_arguments)] + fn forward( + &self, + data: &mut D, + keycode: Keycode, + state: KeyState, + serial: Serial, + time: InputTime, + mods_changed: bool, + bypass_grab: bool, + ) { + let mut guard = self.arc.internal.lock().unwrap(); + match state { + KeyState::Pressed => { + guard.forwarded_pressed_keys.insert(keycode); + } + KeyState::Released => { + guard.forwarded_pressed_keys.remove(&keycode); + } + }; + + // forward to client if no keybinding is triggered. + // Modifiers are only sent when the shared seat state actually changed; the client + // resolves the following key event against them, so they must precede the key (handled + // in `KeyboardInnerHandle::input`). + let seat = self.get_seat(data); + let mods = guard.mods_state; + let modifiers = mods_changed.then_some(mods); + if bypass_grab { + let mut handle = KeyboardInnerHandle { + inner: &mut guard, + seat: &seat, + }; + DefaultGrab.input(data, &mut handle, keycode, state, modifiers, serial, time); + } else { + guard.with_grab(data, &seat, |data, handle, grab| { + grab.input(data, handle, keycode, state, modifiers, serial, time); + }); + } + if guard.focus.is_some() { + trace!("Input forwarded to client"); + } else { + trace!("No client currently focused"); + } + } + + /// Set the current focus of this keyboard + /// + /// If the new focus is different from the previous one, any previous focus + /// will be sent a [`wl_keyboard::Event::Leave`](wayland_server::protocol::wl_keyboard::Event::Leave) + /// event, and if the new focus is not `None`, + /// a [`wl_keyboard::Event::Enter`](wayland_server::protocol::wl_keyboard::Event::Enter) event will be sent. + #[instrument(level = "debug", parent = &self.arc.span, skip(self, data, focus), fields(focus = focus.is_some()))] + pub fn set_focus(&self, data: &mut D, focus: Option<::KeyboardFocus>, serial: Serial) { + let mut guard = self.arc.internal.lock().unwrap(); + guard.pending_focus.clone_from(&focus); + let seat = self.get_seat(data); + guard.with_grab(data, &seat, |data, handle, grab| { + grab.set_focus(data, handle, focus, serial); + }); + } + + /// Return the key codes of the currently pressed keys. + pub fn pressed_keys(&self) -> HashSet { + let guard = self.arc.internal.lock().unwrap(); + guard.pressed_keys.clone() + } + + /// Iterate over the keysyms of the currently pressed keys. + pub fn with_pressed_keysyms(&self, f: F) -> R + where + F: FnOnce(Vec>) -> R, + R: 'static, + { + let guard = self.arc.internal.lock().unwrap(); + { + let handles = guard + .pressed_keys + .iter() + .map(|keycode| KeysymHandle { + xkb: &guard.xkb, + keycode: *keycode, + }) + .collect::>(); + f(handles) + } + } + + /// Get the current modifiers state. + pub fn modifier_state(&self) -> ModifiersState { + self.arc.internal.lock().unwrap().mods_state + } + + /// Find a keycode that produces `keysym` at *any* shift level of the currently active + /// layout, if any. + /// + /// Intended for injecting a keysym as a real key through the shared seat *while modifiers are + /// held* (e.g. a libei `ei_text.keysym` `c` arriving while Ctrl is down, or an uppercase `Y` while + /// Super is down) + pub fn keycode_for_keysym(&self, keysym: Keysym) -> Option { + let guard = self.arc.internal.lock().unwrap(); + let xkb = guard.xkb.lock().unwrap(); + let layout = xkb.state.serialize_layout(XKB_STATE_LAYOUT_EFFECTIVE); + (xkb.keymap.min_keycode().raw()..=xkb.keymap.max_keycode().raw()) + .map(Keycode::new) + .find(|&keycode| { + (0..xkb.keymap.num_levels_for_key(keycode, layout)).any(|level| { + xkb.keymap + .key_get_syms_by_level(keycode, layout, level) + .contains(&keysym) + }) + }) + } + + /// Set the modifiers state. + /// + /// Note that the round-trip from XKB state to serialized form and back to XKB state is lossy, + /// as documented in [`xkb::State::update_mask`]. + pub fn set_modifier_state(&self, mods_state: ModifiersState) -> u32 { + let internal = &mut self.arc.internal.lock().unwrap(); + + let (leds_changed, led_state, modifiers_changed, serialized) = { + let state = &mut internal.xkb.lock().unwrap().state; + + let serialized = mods_state.serialize_back(state); + + let modifiers_changed = state.update_mask( + serialized.depressed, + serialized.latched, + serialized.locked, + serialized.layout_effective & xkb::STATE_LAYOUT_DEPRESSED, + serialized.layout_effective & xkb::STATE_LAYOUT_LATCHED, + serialized.layout_effective & xkb::STATE_LAYOUT_LOCKED, + ); + + // Return early it nothing changed. + if modifiers_changed == 0 { + return 0; + } + + let led_mapping = &internal.led_mapping; + let mut led_state = internal.led_state; + let leds_changed = led_state.update_with(state, led_mapping); + + (leds_changed, led_state, modifiers_changed, serialized) + }; + + internal.mods_state = mods_state; + internal.mods_state.serialized = serialized; + + if leds_changed { + internal.led_state = led_state; + } + + modifiers_changed + } + + /// Advertises changed modifier state using [`KeyboardTarget::modifiers`]. + /// + /// Use this with [`KeyboardHandle::set_modifier_state`] when necessary. + pub fn advertise_modifier_state(&self, data: &mut D) { + let internal = &mut *self.arc.internal.lock().unwrap(); + + if let Some((focus, _)) = internal.focus.as_mut() { + let seat = self.get_seat(data); + focus.modifiers(&seat, data, internal.mods_state, SERIAL_COUNTER.next_serial()); + }; + } + + /// Get the current led state + pub fn led_state(&self) -> LedState { + self.arc.internal.lock().unwrap().led_state + } + + /// Check if keyboard has focus + pub fn is_focused(&self) -> bool { + self.arc.internal.lock().unwrap().focus.is_some() + } + + /// Change the repeat info configured for this keyboard + #[instrument(parent = &self.arc.span, skip(self))] + pub fn change_repeat_info(&self, rate: i32, delay: i32) { + let mut guard = self.arc.internal.lock().unwrap(); + guard.repeat_delay = delay; + guard.repeat_rate = rate; + #[cfg(feature = "wayland_frontend")] + for kbd in &*self.arc.known_kbds.lock().unwrap() { + let Ok(kbd) = kbd.upgrade() else { + continue; + }; + if kbd.version() >= 4 { + kbd.repeat_info(rate, delay); + } + } + } + + /// Access the [`Serial`] of the last `keyboard_enter` event, if that focus is still active. + /// + /// In other words this will return `None` again, once a `keyboard_leave` occurred. + #[cfg(feature = "wayland_frontend")] + pub fn last_enter(&self) -> Option { + *self.arc.last_enter.lock().unwrap() + } + + fn get_seat(&self, data: &mut D) -> Seat { + let seat_state = data.seat_state(); + seat_state + .seats + .iter() + .find(|seat| seat.get_keyboard().map(|h| &h == self).unwrap_or(false)) + .cloned() + .unwrap() + } +} + +#[cfg(feature = "wayland_frontend")] +impl KeyboardHandle +where + D: SeatHandler + 'static, + ::KeyboardFocus: crate::wayland::seat::WaylandFocus, +{ + /// Inject a batch of keysyms as text into the currently focused client (KWin-style). + /// + /// This is a helper/fallback: a compositor should prefer delivering text through the + /// text-input protocol (`zwp_text_input_v3::commit_string`) when a text-input client is + /// focused, and fall back to this for clients that aren't (terminals, games, ...). + /// + /// Builds a throwaway keymap that binds each keysym to its own spare keycode at base level + /// (so no modifiers are ever needed), hands that keymap to clients, taps each keycode on the + /// focused client, then restores the seat keymap. The compositor's own seat xkb state is + /// **never** touched, so shortcut handling and physical-keyboard state are unaffected. + /// `modifiers(0)` accompanies the injection keymap so any modifier the seat currently holds + /// (e.g. a physically-held Shift) doesn't alter the injected characters. + pub fn inject_text_keysyms(&self, data: &mut D, keysyms: &[Keysym]) { + const FIRST: u32 = 9; + let mut keycode_decls = String::new(); + let mut symbol_decls = String::new(); + let mut codes = Vec::with_capacity(keysyms.len()); + for (i, keysym) in keysyms.iter().enumerate() { + let code = FIRST + i as u32; + if code > 255 { + break; + } + let name = xkb::keysym_get_name(*keysym); + if name.is_empty() || name == "NoSymbol" { + continue; + } + keycode_decls.push_str(&format!(" = {code};\n")); + symbol_decls.push_str(&format!(" key {{ [ {name} ] }};\n")); + codes.push(Keycode::new(code)); + } + if codes.is_empty() { + return; + } + + let custom = format!( + "xkb_keymap {{\n\ + xkb_keycodes \"custom\" {{\n minimum = 8;\n maximum = 255;\n{keycode_decls}}};\n\ + xkb_types \"(custom)\" {{ include \"complete\" }};\n\ + xkb_compatibility \"custom\" {{ include \"complete\" }};\n\ + xkb_symbols \"custom\" {{\n{symbol_decls}}};\n\ + }};\n" + ); + + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let Some(keymap) = xkb::Keymap::new_from_string( + &context, + custom, + xkb::KEYMAP_FORMAT_TEXT_V1, + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) else { + return; + }; + let xkb = Mutex::new(Xkb { + state: xkb::State::new(&keymap), + keymap, + context, + }); + let injection_keymap = KeymapFile::new(&xkb.lock().unwrap().keymap); + + let seat = self.get_seat(data); + let mut guard = self.arc.internal.lock().unwrap(); + if guard.focus.is_none() { + return; + } + let seat_mods = guard.mods_state; + + // Switch the focused client to the injection keymap (with no modifiers), then tap keys. + { + let focus = guard.focus.as_mut().map(|(focus, _)| focus); + self.send_keymap(data, &focus, &injection_keymap, ModifiersState::default()); + } + for keycode in codes { + let press = SERIAL_COUNTER.next_serial(); + let release = SERIAL_COUNTER.next_serial(); + if let Some((focus, _)) = guard.focus.as_mut() { + let handle = KeysymHandle { xkb: &xkb, keycode }; + focus.key(&seat, data, handle, KeyState::Pressed, press, InputTime::now()); + } + if let Some((focus, _)) = guard.focus.as_mut() { + let handle = KeysymHandle { xkb: &xkb, keycode }; + focus.key(&seat, data, handle, KeyState::Released, release, InputTime::now()); + } + } + + // Restore the seat keymap (and the seat's real modifier state) to the client. + let seat_keymap = self.arc.keymap.lock().unwrap(); + let focus = guard.focus.as_mut().map(|(focus, _)| focus); + self.send_keymap(data, &focus, &seat_keymap, seat_mods); + } +} + +impl KeyboardHandle +where + D: SeatHandler, + ::KeyboardFocus: Clone, +{ + /// Retrieve the current keyboard focus + pub fn current_focus(&self) -> Option<::KeyboardFocus> { + self.arc + .internal + .lock() + .unwrap() + .focus + .clone() + .map(|(focus, _)| focus) + } +} + +/// This inner handle is accessed from inside a keyboard grab logic, and directly +/// sends event to the client +pub struct KeyboardInnerHandle<'a, D: SeatHandler> { + inner: &'a mut KbdInternal, + seat: &'a Seat, +} + +impl fmt::Debug for KeyboardInnerHandle<'_, D> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KeyboardInnerHandle") + .field("inner", &self.inner) + .field("seat", &self.seat.arc.name) + .finish() + } +} + +impl KeyboardInnerHandle<'_, D> { + /// Change the current grab on this keyboard to the provided grab + /// + /// Overwrites any current grab. + pub fn set_grab + 'static>( + &mut self, + handler: &mut dyn KeyboardGrab, + data: &mut D, + serial: Serial, + grab: G, + ) { + handler.unset(data); + self.inner.grab = GrabStatus::Active(serial, Box::new(grab)); + } + + /// Remove any current grab on this keyboard, resetting it to the default behavior + /// + /// This will also restore the focus of the underlying keyboard if restore_focus + /// is [`true`] + pub fn unset_grab( + &mut self, + handler: &mut dyn KeyboardGrab, + data: &mut D, + serial: Serial, + restore_focus: bool, + ) { + handler.unset(data); + self.inner.grab = GrabStatus::None; + // restore the focus + if restore_focus { + let focus = self.inner.pending_focus.clone(); + self.set_focus(data, focus, serial); + } + } + + /// Access the current focus of this keyboard + pub fn current_focus(&self) -> Option<&::KeyboardFocus> { + self.inner.focus.as_ref().map(|f| &f.0) + } + + /// Access the keymap file currently used by this keyboard. + #[cfg(feature = "wayland_frontend")] + pub(crate) fn with_keymap_file(&self, f: impl FnOnce(&KeymapFile) -> R) -> R { + // Only the keyboard's internal state is locked by this handle, not its keymap file. + let keyboard = self.seat.get_keyboard().unwrap(); + let keymap_file = keyboard.arc.keymap.lock().unwrap(); + f(&keymap_file) + } + + /// Convert a given keycode as a [`KeysymHandle`] modified by this keyboards state + pub fn keysym_handle(&self, keycode: Keycode) -> KeysymHandle<'_> { + KeysymHandle { + keycode, + xkb: &self.inner.xkb, + } + } + + /// Get the current modifiers state + pub fn modifier_state(&self) -> ModifiersState { + self.inner.mods_state + } + + /// Send the input to the focused keyboards + pub fn input( + &mut self, + data: &mut D, + keycode: Keycode, + key_state: KeyState, + modifiers: Option, + serial: Serial, + time: InputTime, + ) { + let (focus, _) = match self.inner.focus.as_mut() { + Some(focus) => focus, + None => return, + }; + + #[cfg(feature = "wayland_frontend")] + if let Some(keyboard_handle) = self.seat.get_keyboard() { + let keymap_file = keyboard_handle.arc.keymap.lock().unwrap(); + let mods = self.inner.mods_state; + keyboard_handle.send_keymap(data, &Some(focus), &keymap_file, mods); + } + + let key = KeysymHandle { + xkb: &self.inner.xkb, + keycode, + }; + + focus.key(self.seat, data, key, key_state, serial, time); + if let Some(mods) = modifiers { + focus.modifiers(self.seat, data, mods, serial); + } + } + + /// Iterate over the currently pressed keys. + pub fn with_pressed_keysyms(&self, f: F) -> R + where + F: FnOnce(Vec>) -> R, + R: 'static, + { + let handles = self + .inner + .pressed_keys + .iter() + .map(|code| self.keysym_handle(*code)) + .collect(); + f(handles) + } + + /// The forwarded held keys and modifier state to announce to a newly-focused target on a + /// focus change, from the seat's shared state. + fn focus_enter_state(&self) -> (Vec>, ModifiersState) { + ( + self.inner + .forwarded_pressed_keys + .iter() + .map(|keycode| KeysymHandle { + xkb: &self.inner.xkb, + keycode: *keycode, + }) + .collect(), + self.inner.mods_state, + ) + } + + /// Set the current focus of this keyboard + /// + /// If the new focus is different from the previous one, any previous focus + /// will be sent a [`wl_keyboard::Event::Leave`](wayland_server::protocol::wl_keyboard::Event::Leave) + /// event, and if the new focus is not `None`, + /// a [`wl_keyboard::Event::Enter`](wayland_server::protocol::wl_keyboard::Event::Enter) event will be sent. + pub fn set_focus( + &mut self, + data: &mut D, + focus: Option<::KeyboardFocus>, + serial: Serial, + ) { + if let Some(focus) = focus { + let old_focus = self.inner.focus.replace((focus.clone(), serial)); + match (focus, old_focus) { + (focus, Some((old_focus, _))) if focus == old_focus => { + trace!("Focus unchanged"); + } + (focus, Some((old_focus, _))) => { + trace!("Focus set to new surface"); + let (keys, mods) = self.focus_enter_state(); + + focus.replace(old_focus, self.seat, data, keys, mods, serial); + data.focus_changed(self.seat, Some(&focus)); + } + (focus, None) => { + let (keys, mods) = self.focus_enter_state(); + + focus.enter(self.seat, data, keys, serial); + focus.modifiers(self.seat, data, mods, serial); + data.focus_changed(self.seat, Some(&focus)); + } + } + } else if let Some((old_focus, _)) = self.inner.focus.take() { + trace!("Focus unset"); + old_focus.leave(self.seat, data, serial); + } + } +} + +// The default grab, the behavior when no particular grab is in progress +struct DefaultGrab; + +impl KeyboardGrab for DefaultGrab { + fn input( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + keycode: Keycode, + state: KeyState, + modifiers: Option, + serial: Serial, + time: InputTime, + ) { + handle.input(data, keycode, state, modifiers, serial, time) + } + + fn set_focus( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + focus: Option<::KeyboardFocus>, + serial: Serial, + ) { + handle.set_focus(data, focus, serial) + } + + fn start_data(&self) -> &GrabStartData { + unreachable!() + } + + fn unset(&mut self, _data: &mut D) {} +} diff --git a/vendor/smithay/src/input/keyboard/modifiers_state.rs b/vendor/smithay/src/input/keyboard/modifiers_state.rs new file mode 100644 index 0000000..65fcdb6 --- /dev/null +++ b/vendor/smithay/src/input/keyboard/modifiers_state.rs @@ -0,0 +1,181 @@ +use xkbcommon::xkb; + +/// Represents the current state of the keyboard modifiers +/// +/// Each field of this struct represents a modifier and is `true` if this modifier is active. +/// +/// For some modifiers, this means that the key is currently pressed, others are toggled/locked +/// (like caps lock). +/// +/// **Note:** The XKB state should usually be the single source of truth, and the +/// serialization is lossy and will not survive round trips. This is documented in +/// [`xkb::State::update_mask`]. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct ModifiersState { + /// The "control" key + pub ctrl: bool, + /// The "alt" key + pub alt: bool, + /// The "shift" key + pub shift: bool, + /// The "Caps lock" key + pub caps_lock: bool, + /// The "logo" key + /// + /// Also known as the "windows" key on most keyboards + pub logo: bool, + /// The "Num lock" key + pub num_lock: bool, + /// The "ISO level 3 shift" key + /// + /// Also known as the "AltGr" key + pub iso_level3_shift: bool, + + /// The "ISO level 5 shift" key + pub iso_level5_shift: bool, + + /// Cached serialized modifier state, e.g. for sending in `wl_keyboard.modifiers`. + /// + /// Note that this may have outdated information compared to the other fields, and that + /// this is not updated in [`ModifiersState::serialize_back`]. + pub serialized: SerializedMods, +} + +impl ModifiersState { + /// Updates the high-level modifiers state from an XKB state. + /// + /// **Note:** The XKB state should usually be the single source of truth, and the + /// serialization is lossy and will not survive round trips. This is documented in + /// [`xkb::State::update_mask`]. + pub fn update_with(&mut self, state: &xkb::State) { + self.ctrl = state.mod_name_is_active(&xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE); + self.alt = state.mod_name_is_active(&xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE); + self.shift = state.mod_name_is_active(&xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE); + self.caps_lock = state.mod_name_is_active(&xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE); + self.logo = state.mod_name_is_active(&xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE); + self.num_lock = state.mod_name_is_active(&xkb::MOD_NAME_NUM, xkb::STATE_MODS_EFFECTIVE); + self.iso_level3_shift = + state.mod_name_is_active(&xkb::MOD_NAME_ISO_LEVEL3_SHIFT, xkb::STATE_MODS_EFFECTIVE); + self.iso_level5_shift = state.mod_name_is_active(&xkb::MOD_NAME_MOD3, xkb::STATE_MODS_EFFECTIVE); + self.serialized = serialize_modifiers(state); + } + + /// Serializes the high-level modifiers state to be sent to XKB e.g. in + /// `wl_keyboard.modifiers`. + /// + /// **Note:** The XKB state should usually be the single source of truth, and the + /// serialization is lossy and will not survive round trips. This is documented in + /// [`xkb::State::update_mask`]. + /// + /// Note that cached serialized state is stored in [`ModifiersState::serialized`], but it may + /// have outdated information. This function ignores that field. You should update the cached + /// serialized state after using this function, like so: + /// + /// ```no_run + /// use smithay::input::keyboard::ModifiersState; + /// + /// let mut mods_state: ModifiersState; + /// # mods_state = todo!(); + /// # let xkb_state = todo!(); + /// + /// // Update the information + /// mods_state.ctrl = true; + /// + /// // Serialize e.g. for sending in `wl_keyboard.modifiers` + /// let serialized = mods_state.serialize_back(&xkb_state); + /// + /// // Update the cached serialized state + /// mods_state.serialized = serialized; + /// ``` + pub fn serialize_back(&self, state: &xkb::State) -> SerializedMods { + let keymap = state.get_keymap(); + + let mut locked: u32 = 0; + let mut depressed: u32 = 0; + + if self.caps_lock { + let index = keymap.mod_get_index(&xkb::MOD_NAME_CAPS); + if index != xkb::MOD_INVALID { + locked |= 1 << index; + } + } + if self.num_lock { + let index = keymap.mod_get_index(&xkb::MOD_NAME_NUM); + if index != xkb::MOD_INVALID { + locked |= 1 << index; + } + } + if self.ctrl { + let index = keymap.mod_get_index(&xkb::MOD_NAME_CTRL); + if index != xkb::MOD_INVALID { + depressed |= 1 << index; + } + } + if self.alt { + let index = keymap.mod_get_index(&xkb::MOD_NAME_ALT); + if index != xkb::MOD_INVALID { + depressed |= 1 << index; + } + } + if self.shift { + let index = keymap.mod_get_index(&xkb::MOD_NAME_SHIFT); + if index != xkb::MOD_INVALID { + depressed |= 1 << index; + } + } + if self.logo { + let index = keymap.mod_get_index(&xkb::MOD_NAME_LOGO); + if index != xkb::MOD_INVALID { + depressed |= 1 << index; + } + } + if self.iso_level3_shift { + let index = keymap.mod_get_index(&xkb::MOD_NAME_ISO_LEVEL3_SHIFT); + if index != xkb::MOD_INVALID { + depressed |= 1 << index; + } + } + if self.iso_level5_shift { + let index = keymap.mod_get_index(&xkb::MOD_NAME_MOD3); + if index != xkb::MOD_INVALID { + depressed |= 1 << index; + } + } + + let layout_effective = state.serialize_layout(xkb::STATE_LAYOUT_EFFECTIVE); + + SerializedMods { + depressed, + latched: 0, + locked, + layout_effective, + } + } +} + +/// Serialized modifier state +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct SerializedMods { + /// Depressed modifiers + pub depressed: u32, + /// Latched modifiers + pub latched: u32, + /// Locked modifiers + pub locked: u32, + /// Effective keyboard layout + pub layout_effective: u32, +} + +fn serialize_modifiers(state: &xkb::State) -> SerializedMods { + let depressed = state.serialize_mods(xkb::STATE_MODS_DEPRESSED); + let latched = state.serialize_mods(xkb::STATE_MODS_LATCHED); + let locked = state.serialize_mods(xkb::STATE_MODS_LOCKED); + let layout_effective = state.serialize_layout(xkb::STATE_LAYOUT_EFFECTIVE); + + SerializedMods { + depressed, + latched, + locked, + layout_effective, + } +} diff --git a/vendor/smithay/src/input/keyboard/xkb_config.rs b/vendor/smithay/src/input/keyboard/xkb_config.rs new file mode 100644 index 0000000..393f454 --- /dev/null +++ b/vendor/smithay/src/input/keyboard/xkb_config.rs @@ -0,0 +1,45 @@ +pub use xkbcommon::xkb; + +/// Configuration for xkbcommon. +/// +/// For the fields that are not set ("" or None, as set in the `Default` impl), xkbcommon will use +/// the values from the environment variables `XKB_DEFAULT_RULES`, `XKB_DEFAULT_MODEL`, +/// `XKB_DEFAULT_LAYOUT`, `XKB_DEFAULT_VARIANT` and `XKB_DEFAULT_OPTIONS`. +/// +/// For details, see the [documentation at xkbcommon.org][docs]. +/// +/// [docs]: https://xkbcommon.org/doc/current/structxkb__rule__names.html +#[derive(Clone, Debug, Default)] +pub struct XkbConfig<'a> { + /// The rules file to use. + /// + /// The rules file describes how to interpret the values of the model, layout, variant and + /// options fields. + pub rules: &'a str, + /// The keyboard model by which to interpret keycodes and LEDs. + pub model: &'a str, + /// A comma separated list of layouts (languages) to include in the keymap. + pub layout: &'a str, + /// A comma separated list of variants, one per layout, which may modify or augment the + /// respective layout in various ways. + pub variant: &'a str, + /// A comma separated list of options, through which the user specifies non-layout related + /// preferences, like which key combinations are used for switching layouts, or which key is the + /// Compose key. + pub options: Option, +} + +impl XkbConfig<'_> { + pub(crate) fn compile_keymap(&self, context: &xkb::Context) -> Result { + xkb::Keymap::new_from_names( + context, + self.rules, + self.model, + self.layout, + self.variant, + self.options.clone(), + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) + .ok_or(()) + } +} diff --git a/vendor/smithay/src/input/mod.rs b/vendor/smithay/src/input/mod.rs new file mode 100644 index 0000000..a9314be --- /dev/null +++ b/vendor/smithay/src/input/mod.rs @@ -0,0 +1,760 @@ +//! +//! Input abstractions +//! +//! This module provides some types loosely resembling instances of wayland seats, pointers, touch and keyboards. +//! It is however not directly tied to wayland and can be used to multiplex various input operations +//! between different handlers. +//! +//! If the `wayland_frontend`-feature is enabled the `smithay::wayland::seat`-module provides additional +//! functionality for the provided types of this module to map them to advertised wayland globals and objects. +//! +//! ## How to use it +//! +//! To start using this module you need to create a [`SeatState`] and use that to create [`Seat`]s. +//! Additionally you need to implement the [`SeatHandler`] trait. +//! +//! ### Initialization +//! +//! ``` +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::backend::input::{InputTime, KeyState}; +//! # use smithay::input::{ +//! # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, +//! # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, +//! # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, +//! # GestureHoldBeginEvent, GestureHoldEndEvent}, +//! # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, +//! # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, +//! # }; +//! # use smithay::utils::{IsAlive, Serial}; +//! +//! struct State { +//! seat_state: SeatState, +//! // ... +//! }; +//! +//! let mut seat_state = SeatState::::new(); +//! +//! // create the seat +//! let seat = seat_state.new_seat( +//! "seat-0", // the name of the seat, will be advertized to clients +//! ); +//! +//! # #[derive(Debug, Clone, PartialEq)] +//! # struct Target; +//! # impl IsAlive for Target { +//! # fn alive(&self) -> bool { true } +//! # } +//! # impl PointerTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} +//! # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} +//! # fn frame(&self, seat: &Seat, data: &mut State) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} +//! # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} +//! # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} +//! # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} +//! # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} +//! # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} +//! # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} +//! # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} +//! # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} +//! # } +//! # impl KeyboardTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} +//! # fn key( +//! # &self, +//! # seat: &Seat, +//! # data: &mut State, +//! # key: KeysymHandle<'_>, +//! # state: KeyState, +//! # serial: Serial, +//! # time: InputTime, +//! # ) {} +//! # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} +//! # } +//! # impl TouchTarget for Target { +//! # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} +//! # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} +//! # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } +//! # } +//! +//! // implement the required traits +//! impl SeatHandler for State { +//! type KeyboardFocus = Target; +//! type PointerFocus = Target; +//! type TouchFocus = Target; +//! +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) { +//! // handle focus changes, if you need to ... +//! } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { +//! // handle new images for the cursor ... +//! } +//! } +//! ``` +//! +//! ### Run usage +//! +//! Once the seat is initialized, you can add capabilities to it. +//! +//! Currently, pointer, touch and keyboard capabilities are supported by this module. +//! [`tablet`] provides similar abstractions for drawing tablets. +//! +//! You can add these capabilities via methods of the [`Seat`] struct: +//! [`Seat::add_keyboard`], [`Seat::add_pointer`] and [`Seat::add_touch`]. +//! These methods return handles that can be cloned and sent across thread, so you can keep one around +//! in your event-handling code to forward inputs to your clients. +//! + +use std::{ + fmt, + hash::Hash, + sync::{Arc, Mutex, Weak}, +}; + +use tracing::{info_span, instrument}; +use xkbcommon::xkb::ContextFlags; + +use self::touch::TouchTarget; +use self::{ + keyboard::{Error as KeyboardError, KeyboardHandle, KeyboardTarget, LedState}, + touch::TouchHandle, +}; +use self::{ + pointer::{CursorImageStatus, PointerHandle, PointerTarget}, + touch::TouchGrab, +}; +use crate::{ + input::{ + pointer::{ClickGrab, GrabStartData as PointerGrabStartData, PointerGrab}, + touch::{GrabStartData as TouchGrabStartData, TouchDownGrab}, + }, + utils::{Serial, user_data::UserDataMap}, +}; + +pub mod dnd; +pub mod keyboard; +pub mod pointer; +pub mod tablet; +pub mod touch; + +/// Handler trait for Seats +pub trait SeatHandler: Sized + 'static { + /// Type used to represent the target currently holding the keyboard focus + type KeyboardFocus: KeyboardTarget + PartialEq + Clone + 'static; + /// Type used to represent the target currently holding the pointer focus + type PointerFocus: PointerTarget + PartialEq + Clone + 'static; + /// Type used to represent the target currently holding the touch focus + type TouchFocus: TouchTarget + PartialEq + Clone + 'static; + + /// [SeatState] getter + fn seat_state(&mut self) -> &mut SeatState; + + /// Callback that will be notified whenever the focus of the seat changes. + fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&Self::KeyboardFocus>) {} + + /// Callback that will be notified whenever a client requests to set a custom cursor image. + fn cursor_image(&mut self, _seat: &Seat, _image: CursorImageStatus) {} + + /// Callback that will be notified whenever the keyboard led state changes. + fn led_state_changed(&mut self, _seat: &Seat, _led_state: LedState) {} + + /// Provides the implicit pointer grab for clicks + /// + /// When the user presses a pointer button, an implicit pointer grab is installed. If your + /// compositor needs custom behavior for this grab, you can implement this trait item and + /// return your own [`PointerGrab`] implementation. + fn click_grab(&mut self, start_data: PointerGrabStartData) -> impl PointerGrab { + ClickGrab::new(start_data) + } + + /// Provides the implicit touch grab for down events + /// + /// When the user presses down on the touchscreen, an implicit grab is installed. If your + /// compositor needs custom behavior for this grab, you can implement this trait item and + /// return your own [`TouchGrab`] implementation. + fn touch_down_grab(&mut self, start_data: TouchGrabStartData) -> impl TouchGrab { + TouchDownGrab::new(start_data) + } +} +/// Delegate type for all [Seat] globals. +/// +/// Events will be forwarded to an instance of the Seat global. +pub struct SeatState { + pub(crate) seats: Vec>, +} + +impl fmt::Debug for SeatState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SeatState").field("seats", &self.seats).finish() + } +} + +/// A Seat handle +/// +/// This struct gives you access to the control of the +/// capabilities of the associated seat. +/// +/// This is an handle to the inner logic, it can be cloned. +/// +/// See module-level documentation for details of use. +pub struct Seat { + pub(crate) arc: Arc>, +} + +/// Weak variant of an [`Seat`] +/// +/// Does not keep associated user data alive, +/// and can be used to refer to a potentially already destroyed seat. +#[derive(Debug)] +pub struct WeakSeat(Weak>); + +impl Clone for WeakSeat { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl WeakSeat { + /// Try to retrieve the original `Seat`, if it still exists + pub fn upgrade(&self) -> Option> { + self.0.upgrade().map(|arc| Seat { arc }) + } + + /// Check if the seat is still alive + pub fn is_alive(&self) -> bool { + self.0.strong_count() != 0 + } +} + +impl Seat { + /// Create a weak reference to this seat + pub fn downgrade(&self) -> WeakSeat { + WeakSeat(Arc::downgrade(&self.arc)) + } +} + +impl fmt::Debug for Seat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Seat").field("arc", &self.arc).finish() + } +} + +impl PartialEq for Seat { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} +impl Eq for Seat {} + +impl Hash for Seat { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.arc).hash(state) + } +} + +pub(crate) struct Inner { + pub(crate) pointer: Option>, + pub(crate) keyboard: Option>, + pub(crate) touch: Option>, + + #[cfg(feature = "wayland_frontend")] + pub(crate) global: Option, + #[cfg(feature = "wayland_frontend")] + pub(crate) known_seats: Vec>, +} + +#[cfg(not(feature = "wayland_frontend"))] +impl fmt::Debug for Inner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Inner") + .field("pointer", &self.pointer) + .field("keyboard", &self.keyboard) + .finish() + } +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for Inner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Inner") + .field("pointer", &self.pointer) + .field("keyboard", &self.keyboard) + .field("touch", &self.touch) + .field("global", &self.global) + .field("known_seats", &self.known_seats) + .finish() + } +} + +pub(crate) struct SeatRc { + #[allow(dead_code)] + pub(crate) name: String, + pub(crate) inner: Mutex>, + span: tracing::Span, + user_data_map: UserDataMap, +} + +impl fmt::Debug for SeatRc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SeatRc") + .field("name", &self.name) + .field("inner", &self.inner) + .field("user_data_map", &self.user_data_map) + .finish() + } +} + +impl Clone for Seat { + #[inline] + fn clone(&self) -> Self { + Self { + arc: self.arc.clone(), + } + } +} + +impl Default for SeatState { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl SeatState { + /// Create new delegate SeatState + pub fn new() -> Self { + Self { seats: Vec::new() } + } + + /// Create a new seat + pub fn new_seat(&mut self, name: N) -> Seat + where + N: Into, + { + let name = name.into(); + let span = info_span!("input_seat", name); + + let arc = Arc::new(SeatRc { + name, + inner: Mutex::new(Inner { + pointer: None, + keyboard: None, + touch: None, + + #[cfg(feature = "wayland_frontend")] + global: None, + #[cfg(feature = "wayland_frontend")] + known_seats: Vec::new(), + }), + span, + user_data_map: UserDataMap::new(), + }); + self.seats.push(Seat { arc: arc.clone() }); + + Seat { arc } + } +} + +impl Seat { + /// Access the `UserDataMap` associated with this `Seat` + pub fn user_data(&self) -> &UserDataMap { + &self.arc.user_data_map + } + + /// Adds the pointer capability to this seat + /// + /// You are provided a [`PointerHandle`], which allows you to send input events + /// to this pointer. This handle can be cloned. + /// + /// Calling this method on a seat that already has a pointer capability + /// will overwrite it, and will be seen by the clients as if the + /// mouse was unplugged and a new one was plugged. + /// + /// # Examples + /// + /// ```no_run + /// # use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; + /// # use smithay::backend::input::{InputTime, KeyState}; + /// # use smithay::input::{ + /// # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, + /// # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, + /// # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, + /// # GestureHoldBeginEvent, GestureHoldEndEvent}, + /// # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + /// # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, + /// # }; + /// # use smithay::utils::{IsAlive, Serial}; + /// # + /// # #[derive(Debug, Clone, PartialEq)] + /// # struct Target; + /// # impl IsAlive for Target { + /// # fn alive(&self) -> bool { true } + /// # } + /// # impl PointerTarget for Target { + /// # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} + /// # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} + /// # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} + /// # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} + /// # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} + /// # fn frame(&self, seat: &Seat, data: &mut State) {} + /// # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} + /// # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} + /// # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} + /// # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} + /// # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} + /// # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} + /// # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} + /// # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} + /// # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} + /// # } + /// # impl KeyboardTarget for Target { + /// # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} + /// # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} + /// # fn key( + /// # &self, + /// # seat: &Seat, + /// # data: &mut State, + /// # key: KeysymHandle<'_>, + /// # state: KeyState, + /// # serial: Serial, + /// # time: InputTime, + /// # ) {} + /// # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} + /// # } + /// # impl TouchTarget for Target { + /// # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} + /// # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} + /// # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} + /// # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} + /// # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} + /// # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} + /// # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} + /// # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } + /// # } + /// # struct State; + /// # impl SeatHandler for State { + /// # type KeyboardFocus = Target; + /// # type PointerFocus = Target; + /// # type TouchFocus = Target; + /// # + /// # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } + /// # fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) { unimplemented!() } + /// # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } + /// # } + /// # let mut seat: Seat = unimplemented!(); + /// let pointer_handle = seat.add_pointer(); + /// ``` + #[instrument(parent = &self.arc.span, skip(self))] + pub fn add_pointer(&mut self) -> PointerHandle { + let mut inner = self.arc.inner.lock().unwrap(); + let pointer = PointerHandle::new(); + if inner.pointer.is_some() { + // there is already a pointer, remove it and notify the clients + // of the change + inner.pointer = None; + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + } + inner.pointer = Some(pointer.clone()); + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + pointer + } + + /// Access the pointer of this seat if any + pub fn get_pointer(&self) -> Option> { + self.arc.inner.lock().unwrap().pointer.clone() + } + + /// Remove the pointer capability from this seat + /// + /// Clients will be appropriately notified. + #[instrument(parent = &self.arc.span, skip(self))] + pub fn remove_pointer(&mut self) { + let mut inner = self.arc.inner.lock().unwrap(); + if inner.pointer.is_some() { + inner.pointer = None; + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + } + } + + /// Adds the keyboard capability to this seat + /// + /// You are provided a [`KeyboardHandle`], which allows you to send input events + /// to this keyboard. This handle can be cloned. + /// + /// You also provide a Model/Layout/Variant/Options specification of the + /// keymap to be used for this keyboard, as well as any repeat-info that + /// will be forwarded to the clients. + /// + /// Calling this method on a seat that already has a keyboard capability + /// will overwrite it, and will be seen by the clients as if the + /// keyboard was unplugged and a new one was plugged. + /// + /// # Examples + /// + /// ```no_run + /// # use smithay::input::{Seat, SeatState, SeatHandler, keyboard::XkbConfig, pointer::CursorImageStatus}; + /// # use smithay::backend::input::{InputTime, KeyState}; + /// # use smithay::input::{ + /// # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, + /// # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, + /// # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, + /// # GestureHoldBeginEvent, GestureHoldEndEvent}, + /// # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + /// # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, + /// # }; + /// # use smithay::utils::{IsAlive, Serial}; + /// # + /// # #[derive(Debug, Clone, PartialEq)] + /// # struct Target; + /// # impl IsAlive for Target { + /// # fn alive(&self) -> bool { true } + /// # } + /// # impl PointerTarget for Target { + /// # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} + /// # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} + /// # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} + /// # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} + /// # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} + /// # fn frame(&self, seat: &Seat, data: &mut State) {} + /// # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} + /// # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} + /// # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} + /// # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} + /// # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} + /// # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} + /// # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} + /// # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} + /// # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} + /// # } + /// # impl KeyboardTarget for Target { + /// # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} + /// # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} + /// # fn key( + /// # &self, + /// # seat: &Seat, + /// # data: &mut State, + /// # key: KeysymHandle<'_>, + /// # state: KeyState, + /// # serial: Serial, + /// # time: InputTime, + /// # ) {} + /// # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} + /// # } + /// # impl TouchTarget for Target { + /// # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} + /// # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} + /// # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} + /// # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} + /// # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} + /// # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} + /// # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} + /// # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } + /// # } + /// # + /// # struct State; + /// # impl SeatHandler for State { + /// # type KeyboardFocus = Target; + /// # type PointerFocus = Target; + /// # type TouchFocus = Target; + /// # + /// # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } + /// # fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) { unimplemented!() } + /// # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } + /// # } + /// # let mut seat: Seat = unimplemented!(); + /// let keyboard = seat + /// .add_keyboard( + /// XkbConfig { + /// layout: "de", + /// variant: "nodeadkeys", + /// ..XkbConfig::default() + /// }, + /// 200, + /// 25, + /// ) + /// .expect("Failed to initialize the keyboard"); + /// ``` + #[instrument(parent = &self.arc.span, skip(self))] + pub fn add_keyboard( + &mut self, + xkb_config: keyboard::XkbConfig<'_>, + repeat_delay: i32, + repeat_rate: i32, + ) -> Result, KeyboardError> { + Self::add_keyboard_with_context_flags( + self, + xkb_config, + repeat_delay, + repeat_rate, + xkbcommon::xkb::CONTEXT_NO_FLAGS, + ) + } + + /// [`Self::add_keyboard`] equivalent, that allows for overwriting default xkb::Context flags. + /// + /// For more info read [`Self::add_keyboard`] docs. + #[instrument(parent = &self.arc.span, skip(self))] + pub fn add_keyboard_with_context_flags( + &mut self, + xkb_config: keyboard::XkbConfig<'_>, + repeat_delay: i32, + repeat_rate: i32, + context_flags: ContextFlags, + ) -> Result, KeyboardError> { + let mut inner = self.arc.inner.lock().unwrap(); + let keyboard = + self::keyboard::KeyboardHandle::new(xkb_config, repeat_delay, repeat_rate, context_flags)?; + if inner.keyboard.is_some() { + // there is already a keyboard, remove it and notify the clients + // of the change + inner.keyboard = None; + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + } + inner.keyboard = Some(keyboard.clone()); + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + Ok(keyboard) + } + + /// Access the keyboard of this seat if any + pub fn get_keyboard(&self) -> Option> { + self.arc.inner.lock().unwrap().keyboard.clone() + } + + /// Remove the keyboard capability from this seat + /// + /// Clients will be appropriately notified. + #[instrument(parent = &self.arc.span, skip(self))] + pub fn remove_keyboard(&mut self) { + let mut inner = self.arc.inner.lock().unwrap(); + if inner.keyboard.is_some() { + inner.keyboard = None; + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + } + } + + /// Adds the touch capability to this seat + /// + /// You are provided a [`TouchHandle`], which allows you to send input events + /// to this pointer. This handle can be cloned. + /// + /// Calling this method on a seat that already has a touch capability + /// will overwrite it, and will be seen by the clients as if the + /// touchscreen was unplugged and a new one was plugged in. + /// + /// # Examples + /// + /// ```no_run + /// # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; + /// # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; + /// # use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; + /// # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; + /// # + /// # struct State; + /// # impl CompositorHandler for State { + /// # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } + /// # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } + /// # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} + /// # } + /// # impl SeatHandler for State { + /// # type KeyboardFocus = WlSurface; + /// # type PointerFocus = WlSurface; + /// # type TouchFocus = WlSurface; + /// # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } + /// # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } + /// # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } + /// # } + /// # impl PointerConstraintsHandler for State {} + /// # let mut seat: Seat = unimplemented!(); + /// let touch_handle = seat.add_touch(); + /// ``` + pub fn add_touch(&mut self) -> TouchHandle { + Self::add_touch_with_default_grab(self, || Box::new(touch::DefaultGrab)) + } + + /// Adds the touch capability to this seat and allows the use of a custom default [`TouchGrab`] + /// + /// The default grab is used in case no other grab is currently active. When using [`Seat::add_touch`] + /// it will use [`touch::DefaultGrab`] which will install [`touch::TouchDownGrab`] on the first touch point. + /// [`touch::TouchDownGrab`] makes sure all further touch points will use the same target until all touch + /// points are released again. + /// + /// See [`Seat::add_touch`] for more information + pub fn add_touch_with_default_grab(&mut self, default_grab: F) -> TouchHandle + where + F: Fn() -> Box> + Send + 'static, + { + let mut inner = self.arc.inner.lock().unwrap(); + let touch = TouchHandle::new(default_grab); + if inner.touch.is_some() { + // If there's already a tocuh device, remove it notify the clients about the change. + inner.touch = None; + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + } + inner.touch = Some(touch.clone()); + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + touch + } + + /// Access the touch device of this seat, if any. + pub fn get_touch(&self) -> Option> { + self.arc.inner.lock().unwrap().touch.clone() + } + + /// Remove the touch capability from this seat + /// + /// Clients will be appropriately notified. + pub fn remove_touch(&mut self) { + let mut inner = self.arc.inner.lock().unwrap(); + if inner.touch.is_some() { + inner.touch = None; + #[cfg(feature = "wayland_frontend")] + inner.send_all_caps(); + } + } + + /// Gets this seat's name + pub fn name(&self) -> &str { + &self.arc.name + } +} + +pub(super) enum GrabStatus { + None, + Active(Serial, Box), + Borrowed, +} + +// `G` is not `Debug`, so we have to impl Debug manually +impl fmt::Debug for GrabStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GrabStatus::None => f.debug_tuple("GrabStatus::None").finish(), + GrabStatus::Active(serial, _) => f.debug_tuple("GrabStatus::Active").field(&serial).finish(), + GrabStatus::Borrowed => f.debug_tuple("GrabStatus::Borrowed").finish(), + } + } +} diff --git a/vendor/smithay/src/input/pointer/cursor_image.rs b/vendor/smithay/src/input/pointer/cursor_image.rs new file mode 100644 index 0000000..93ee72e --- /dev/null +++ b/vendor/smithay/src/input/pointer/cursor_image.rs @@ -0,0 +1,50 @@ +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_surface::WlSurface; + +pub use cursor_icon::CursorIcon; + +use crate::utils::{Logical, Point}; +use std::sync::Mutex; + +/// The role representing a surface set as the pointer cursor +#[derive(Debug, Default, Copy, Clone)] +pub struct CursorImageAttributes { + /// Location of the hotspot of the pointer in the surface + pub hotspot: Point, +} + +/// Data associated with XDG toplevel surface +/// +/// ```no_run +/// # #[cfg(feature = "wayland_frontend")] +/// use smithay::wayland::compositor; +/// use smithay::input::pointer::CursorImageSurfaceData; +/// +/// # let wl_surface = todo!(); +/// # #[cfg(feature = "wayland_frontend")] +/// compositor::with_states(&wl_surface, |states| { +/// states.data_map.get::(); +/// }); +/// ``` +pub type CursorImageSurfaceData = Mutex; + +/// Possible status of a cursor as requested by clients +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CursorImageStatus { + /// The cursor should be hidden + Hidden, + /// The compositor should draw the given named + /// cursor. + Named(CursorIcon), + // TODO bitmap, dmabuf cursor? Or let the compositor handle everything through "Default" + /// The cursor should be drawn using this surface as an image + #[cfg(feature = "wayland_frontend")] + Surface(WlSurface), +} + +impl CursorImageStatus { + /// Get default `Named` cursor. + pub fn default_named() -> Self { + Self::Named(CursorIcon::Default) + } +} diff --git a/vendor/smithay/src/input/pointer/grab.rs b/vendor/smithay/src/input/pointer/grab.rs new file mode 100644 index 0000000..d74f525 --- /dev/null +++ b/vendor/smithay/src/input/pointer/grab.rs @@ -0,0 +1,480 @@ +use std::fmt; + +use downcast_rs::{Downcast, impl_downcast}; + +use crate::{ + backend::input::ButtonState, + input::SeatHandler, + utils::{Logical, Point}, +}; + +use super::{ + AxisFrame, ButtonEvent, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, MotionEvent, PointerInnerHandle, RelativeMotionEvent, +}; + +/// A trait to implement a pointer grab +/// +/// In some context, it is necessary to temporarily change the behavior of the pointer. This is +/// typically known as a pointer grab. A typical example would be, during a drag'n'drop operation, +/// the underlying surfaces will no longer receive classic pointer event, but rather special events. +/// +/// This trait is the interface to intercept regular pointer events and change them as needed, its +/// interface mimics the [`PointerHandle`](super::PointerHandle) interface. +/// +/// Any interactions with [`PointerHandle`](super::PointerHandle) +/// should be done using [`PointerInnerHandle`], as handle is borrowed/locked before grab methods are called, +/// so calling methods on [`PointerHandle`](super::PointerHandle) would result in a deadlock. +/// +/// If your logic decides that the grab should end, both [`PointerInnerHandle`] +/// and [`PointerHandle`](super::PointerHandle) have +/// a method to change it. +/// +/// When your grab ends (either as you requested it or if it was forcefully cancelled by the server), +/// the struct implementing this trait will be dropped. As such you should put clean-up logic in the destructor, +/// rather than trying to guess when the grab will end. +pub trait PointerGrab: Send + Downcast { + /// A motion was reported + /// + /// This method allows you attach additional behavior to a motion event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::motion()` as part of your processing. If you + /// don't, the rest of the compositor will behave as if the motion event never occurred. + /// + /// Some grabs (such as drag'n'drop, shell resize and motion) unset the focus while they are active, + /// this is achieved by just setting the focus to `None` when invoking `PointerInnerHandle::motion()`. + fn motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ); + /// Relative motion was reported + /// + /// This method allows you attach additional behavior to a relative motion event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::relative_motion()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the motion event never occurred. + fn relative_motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ); + /// A button press was reported + /// + /// This method allows you attach additional behavior to a button event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::button()` as part of your processing. If you + /// don't, the rest of the compositor will behave as if the button event never occurred. + fn button(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, event: &ButtonEvent); + /// An axis scroll was reported + /// + /// This method allows you attach additional behavior to an axis event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::axis()` as part of your processing. If you + /// don't, the rest of the compositor will behave as if the axis event never occurred. + fn axis(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, details: AxisFrame); + /// End of a pointer frame + /// + /// A frame groups associated events. This terminates the frame. + fn frame(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>); + /// A pointer of a given seat started a swipe gesture + /// + /// This method allows you attach additional behavior to a swipe gesture begin event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_swipe_begin()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_swipe_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeBeginEvent, + ); + /// A pointer of a given seat updated a swipe gesture + /// + /// This method allows you attach additional behavior to a swipe gesture update event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_swipe_update()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_swipe_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeUpdateEvent, + ); + /// A pointer of a given seat ended a swipe gesture + /// + /// This method allows you attach additional behavior to a swipe gesture end event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_swipe_end()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_swipe_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeEndEvent, + ); + /// A pointer of a given seat started a pinch gesture + /// + /// This method allows you attach additional behavior to a pinch gesture begin event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_pinch_begin()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_pinch_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchBeginEvent, + ); + /// A pointer of a given seat updated a pinch gesture + /// + /// This method allows you attach additional behavior to a pinch gesture update event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_pinch_update()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_pinch_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchUpdateEvent, + ); + /// A pointer of a given seat ended a pinch gesture + /// + /// This method allows you attach additional behavior to a pinch gesture end event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_pinch_end()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_pinch_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchEndEvent, + ); + /// A pointer of a given seat started a hold gesture + /// + /// This method allows you attach additional behavior to a hold gesture begin event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_hold_begin()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_hold_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldBeginEvent, + ); + /// A pointer of a given seat ended a hold gesture + /// + /// This method allows you attach additional behavior to a hold gesture end event, possibly altering it. + /// You generally will want to invoke `PointerInnerHandle::gesture_hold_end()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the event never occurred. + fn gesture_hold_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldEndEvent, + ); + /// The data about the event that started the grab. + fn start_data(&self) -> &GrabStartData; + /// The grab has been unset or replaced with another grab. + fn unset(&mut self, data: &mut D); +} + +impl_downcast!(PointerGrab where D: SeatHandler); + +/// Data about the event that started the grab. +pub struct GrabStartData { + /// The focused surface and its location, if any, at the start of the grab. + /// + /// The location coordinates are in the global compositor space. + pub focus: Option<(::PointerFocus, Point)>, + /// The button that initiated the grab. + pub button: u32, + /// The location of the click that initiated the grab, in the global compositor space. + pub location: Point, +} + +impl fmt::Debug for GrabStartData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GrabStartData") + .field("focus", &self.focus.as_ref().map(|_| "...")) + .field("button", &self.button) + .field("location", &self.location) + .finish() + } +} + +impl Clone for GrabStartData { + fn clone(&self) -> Self { + GrabStartData { + focus: self.focus.clone(), + button: self.button, + location: self.location, + } + } +} + +// The default grab, the behavior when no particular grab is in progress +pub(super) struct DefaultGrab; + +impl PointerGrab for DefaultGrab { + fn motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + handle.motion(data, focus, event); + } + + fn relative_motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, event: &ButtonEvent) { + handle.button(data, event); + if event.state == ButtonState::Pressed { + let grab = data.click_grab(GrabStartData { + focus: handle.current_focus(), + button: event.button, + location: handle.current_location(), + }); + handle.set_grab(self, data, event.serial, Focus::Keep, grab); + } + } + + fn axis(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, details: AxisFrame) { + handle.axis(data, details); + } + + fn frame(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &GrabStartData { + unreachable!() + } + + fn unset(&mut self, _data: &mut D) {} +} + +/// A click grab, basic grab started when an user clicks a surface +/// to maintain it focused until the user releases the click. +/// +/// In case the user maintains several simultaneous clicks, release +/// the grab once all are released. +pub struct ClickGrab { + start_data: GrabStartData, + focus: Option<(D::PointerFocus, Point)>, +} + +impl ClickGrab { + pub(in crate::input) fn new(start_data: GrabStartData) -> Self { + Self { + focus: start_data.focus.clone(), + start_data, + } + } +} + +impl fmt::Debug for ClickGrab { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ClickGrab") + .field("start_data", &self.start_data) + .finish() + } +} + +impl PointerGrab for ClickGrab { + fn motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + if let (Some((new_target, new_location)), Some((grab_target, grab_location))) = + (&focus, self.focus.as_mut()) + { + if *new_target == *grab_target { + *grab_location = *new_location; + } + } + handle.motion(data, self.focus.clone(), event); + } + + fn relative_motion( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, event: &ButtonEvent) { + handle.button(data, event); + if handle.current_pressed().is_empty() { + // no more buttons are pressed, release the grab + handle.unset_grab(self, data, event.serial, event.time, false); + } + } + + fn axis(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>, details: AxisFrame) { + handle.axis(data, details); + } + + fn frame(&mut self, data: &mut D, handle: &mut PointerInnerHandle<'_, D>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut D, + handle: &mut PointerInnerHandle<'_, D>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &GrabStartData { + &self.start_data + } + + fn unset(&mut self, _data: &mut D) {} +} diff --git a/vendor/smithay/src/input/pointer/mod.rs b/vendor/smithay/src/input/pointer/mod.rs new file mode 100644 index 0000000..22401a5 --- /dev/null +++ b/vendor/smithay/src/input/pointer/mod.rs @@ -0,0 +1,1194 @@ +//! Pointer-related types for smithay's input abstraction + +use std::{ + fmt, + sync::{Arc, Mutex}, +}; + +use crate::{ + backend::input::{Axis, AxisRelativeDirection, AxisSource, ButtonState, InputTime}, + input::{GrabStatus, Seat, SeatHandler}, + utils::Serial, + utils::{IsAlive, Logical, Point}, +}; + +mod cursor_image; +pub use cursor_icon::CursorIcon; +pub use cursor_image::{CursorImageAttributes, CursorImageStatus, CursorImageSurfaceData}; + +mod grab; +use grab::DefaultGrab; +pub use grab::{ClickGrab, GrabStartData, PointerGrab}; +use tracing::{info_span, instrument}; + +/// An handle to a pointer handler +/// +/// It can be cloned and all clones manipulate the same internal state. +/// +/// This handle gives you access to an interface to send pointer events to your +/// clients. +/// +/// When sending events using this handle, they will be intercepted by a pointer +/// grab if any is active. See the [`PointerGrab`] trait for details. +pub struct PointerHandle { + pub(crate) arc: Arc>, +} + +pub(crate) struct PointerRc { + pub(crate) inner: Mutex>, + #[cfg(feature = "wayland_frontend")] + pub(crate) wl_pointer: crate::wayland::seat::pointer::WlPointerHandle, + #[cfg(feature = "wayland_frontend")] + pub(crate) wp_pointer_gestures: crate::wayland::pointer_gestures::WpPointerGesturePointerHandle, + #[cfg(feature = "wayland_frontend")] + pub(crate) wp_relative: crate::wayland::relative_pointer::WpRelativePointerHandle, + pub(crate) span: tracing::Span, +} + +#[cfg(not(feature = "wayland_frontend"))] +impl fmt::Debug for PointerHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PointerHandle") + .field("inner", &self.arc.inner) + .finish() + } +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for PointerHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PointerHandle") + .field("inner", &self.arc.inner) + .field("wl_seat", &self.arc.wl_pointer) + .field("wp_pointer_gestures", &self.arc.wp_pointer_gestures) + .field("wp_relative", &self.arc.wp_relative) + .finish() + } +} + +impl Clone for PointerHandle { + #[inline] + fn clone(&self) -> Self { + Self { + arc: self.arc.clone(), + } + } +} + +impl std::hash::Hash for PointerHandle { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.arc).hash(state) + } +} + +impl std::cmp::PartialEq for PointerHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} + +impl std::cmp::Eq for PointerHandle {} + +/// Trait representing object that can receive pointer interactions +pub trait PointerTarget: IsAlive + fmt::Debug + Send +where + D: SeatHandler, +{ + /// A pointer of a given seat entered this handler + fn enter(&self, seat: &Seat, data: &mut D, event: &MotionEvent); + /// A pointer of a given seat moved over this handler + fn motion(&self, seat: &Seat, data: &mut D, event: &MotionEvent); + /// A pointer of a given seat that provides relative motion moved over this handler + fn relative_motion(&self, seat: &Seat, data: &mut D, event: &RelativeMotionEvent); + /// A pointer of a given seat clicked a button + fn button(&self, seat: &Seat, data: &mut D, event: &ButtonEvent); + /// A pointer of a given seat scrolled on an axis + fn axis(&self, seat: &Seat, data: &mut D, frame: AxisFrame); + /// End of a pointer frame + fn frame(&self, seat: &Seat, data: &mut D); + /// A pointer of a given seat started a swipe gesture + fn gesture_swipe_begin(&self, seat: &Seat, data: &mut D, event: &GestureSwipeBeginEvent); + /// A pointer of a given seat updated a swipe gesture + fn gesture_swipe_update(&self, seat: &Seat, data: &mut D, event: &GestureSwipeUpdateEvent); + /// A pointer of a given seat ended a swipe gesture + fn gesture_swipe_end(&self, seat: &Seat, data: &mut D, event: &GestureSwipeEndEvent); + /// A pointer of a given seat started a pinch gesture + fn gesture_pinch_begin(&self, seat: &Seat, data: &mut D, event: &GesturePinchBeginEvent); + /// A pointer of a given seat updated a pinch gesture + fn gesture_pinch_update(&self, seat: &Seat, data: &mut D, event: &GesturePinchUpdateEvent); + /// A pointer of a given seat ended a pinch gesture + fn gesture_pinch_end(&self, seat: &Seat, data: &mut D, event: &GesturePinchEndEvent); + /// A pointer of a given seat started a hold gesture + fn gesture_hold_begin(&self, seat: &Seat, data: &mut D, event: &GestureHoldBeginEvent); + /// A pointer of a given seat ended a hold gesture + fn gesture_hold_end(&self, seat: &Seat, data: &mut D, event: &GestureHoldEndEvent); + /// A pointer of a given seat left this handler + fn leave(&self, seat: &Seat, data: &mut D, serial: Serial, time: InputTime); + /// A pointer of a given seat moved from another handler to this handler + fn replace( + &self, + replaced: ::PointerFocus, + seat: &Seat, + data: &mut D, + event: &MotionEvent, + ) { + PointerTarget::::leave(&replaced, seat, data, event.serial, event.time); + data.cursor_image(seat, CursorImageStatus::default_named()); + PointerTarget::::enter(self, seat, data, event); + } +} + +impl PointerHandle { + pub(crate) fn new() -> PointerHandle { + PointerHandle { + arc: Arc::new(PointerRc { + inner: Mutex::new(PointerInternal::new()), + #[cfg(feature = "wayland_frontend")] + wl_pointer: crate::wayland::seat::pointer::WlPointerHandle::default(), + #[cfg(feature = "wayland_frontend")] + wp_pointer_gestures: crate::wayland::pointer_gestures::WpPointerGesturePointerHandle::default( + ), + #[cfg(feature = "wayland_frontend")] + wp_relative: crate::wayland::relative_pointer::WpRelativePointerHandle::default(), + span: info_span!("input_pointer"), + }), + } + } + + /// Change the current grab on this pointer to the provided grab + /// + /// If focus is set to [`Focus::Clear`] any currently focused surface will be unfocused. + /// + /// Overwrites any current grab. + #[instrument(level = "debug", parent = &self.arc.span, skip(self, data, grab))] + pub fn set_grab + 'static>(&self, data: &mut D, grab: G, serial: Serial, focus: Focus) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .set_grab(data, &seat, serial, grab, focus); + } + + /// Remove any current grab on this pointer, resetting it to the default behavior + #[instrument(level = "debug", parent = &self.arc.span, skip(self, data))] + pub fn unset_grab(&self, data: &mut D, serial: Serial, time: InputTime) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .unset_grab(data, &seat, serial, time, true); + } + + /// Check if this pointer is currently grabbed with this serial + pub fn has_grab(&self, serial: Serial) -> bool { + self.with_grab(|s, _| s == serial).unwrap_or(false) + } + + /// Check if this pointer is currently being grabbed + pub fn is_grabbed(&self) -> bool { + let guard = self.arc.inner.lock().unwrap(); + !matches!(guard.grab, GrabStatus::None) + } + + /// Returns the start data for the grab, if any. + pub fn grab_start_data(&self) -> Option> { + self.with_grab(|_, g| g.start_data().clone()) + } + + /// Calls `f` with the active grab, if any. + pub fn with_grab(&self, f: impl FnOnce(Serial, &dyn PointerGrab) -> T) -> Option { + let guard = self.arc.inner.lock().unwrap(); + if let GrabStatus::Active(s, g) = &guard.grab { + Some(f(*s, &**g)) + } else { + None + } + } + + /// Notify that the pointer moved + /// + /// You provide the new location of the pointer, in the form of: + /// + /// - The coordinates of the pointer in the global compositor space + /// - The surface on top of which the cursor is, and the coordinates of its + /// origin in the global compositor space (or `None` of the pointer is not + /// on top of a client surface). + /// + /// This will internally take care of notifying the appropriate client objects + /// of enter/motion/leave events. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data, focus), fields(focus = ?focus.as_ref().map(|(_, loc)| ("...", loc))))] + pub fn motion( + &self, + data: &mut D, + focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + inner.pending_focus.clone_from(&focus); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.motion(data, handle, focus, event); + }); + } + + /// Notify about relative pointer motion + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the relative pointer protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data, focus), fields(focus = ?focus.as_ref().map(|(_, loc)| ("...", loc))))] + pub fn relative_motion( + &self, + data: &mut D, + focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + inner.pending_focus.clone_from(&focus); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.relative_motion(data, handle, focus, event); + }); + } + + /// Notify that a button was pressed + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn button(&self, data: &mut D, event: &ButtonEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + match event.state { + ButtonState::Pressed => { + inner.pressed_buttons.push(event.button); + } + ButtonState::Released => { + inner.pressed_buttons.retain(|b| *b != event.button); + } + } + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.button(data, handle, event); + }); + } + + /// Start an axis frame + /// + /// A single frame will group multiple scroll events as if they happened in the same instance. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn axis(&self, data: &mut D, details: AxisFrame) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.axis(data, handle, details); + }); + } + + /// End of a pointer frame + /// + /// A frame groups associated events. This terminates the frame. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn frame(&self, data: &mut D) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.frame(data, handle); + }); + } + + /// Notify about swipe gesture begin + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_swipe_begin(&self, data: &mut D, event: &GestureSwipeBeginEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_swipe_begin(data, handle, event); + }); + } + + /// Notify about swipe gesture update + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_swipe_update(&self, data: &mut D, event: &GestureSwipeUpdateEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_swipe_update(data, handle, event); + }); + } + + /// Notify about swipe gesture end + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_swipe_end(&self, data: &mut D, event: &GestureSwipeEndEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_swipe_end(data, handle, event); + }); + } + + /// Notify about pinch gesture begin + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_pinch_begin(&self, data: &mut D, event: &GesturePinchBeginEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_pinch_begin(data, handle, event); + }); + } + + /// Notify about pinch gesture update + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_pinch_update(&self, data: &mut D, event: &GesturePinchUpdateEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_pinch_update(data, handle, event); + }); + } + + /// Notify about pinch gesture end + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_pinch_end(&self, data: &mut D, event: &GesturePinchEndEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_pinch_end(data, handle, event); + }); + } + + /// Notify about hold gesture begin + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_hold_begin(&self, data: &mut D, event: &GestureHoldBeginEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_hold_begin(data, handle, event); + }); + } + + /// Notify about hold gesture end + /// + /// This will internally send the appropriate event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + #[instrument(level = "trace", parent = &self.arc.span, skip(self, data))] + pub fn gesture_hold_end(&self, data: &mut D, event: &GestureHoldEndEvent) { + let seat = self.get_seat(data); + self.arc + .inner + .lock() + .unwrap() + .with_grab(data, &seat, |data, handle, grab| { + grab.gesture_hold_end(data, handle, event); + }); + } + + /// Access the current location of this pointer in the global space + pub fn current_location(&self) -> Point { + self.arc.inner.lock().unwrap().location + } + + /// Update the current location of this pointer in the global space, + /// without sending any event and without updating the focus. + /// + /// If you want to update the location, and update the focus, + /// and send events, use [Self::motion] instead of this. + /// + /// This is useful when the pointer is only moved by relative events, + /// such as when a pointer lock is held by the focused surface. + /// The client can give us a cursor position hint, which corresponds to + /// the actual location the client may be rendering a pointer at. + /// This position hint should be used as the initial location + /// when the pointer lock is deactivated. + /// + /// The next time [Self::motion] is called, the focus will be + /// updated accordingly as if this function was never called. + /// Clients will never be notified of a location hint. + pub fn set_location(&self, location: Point) { + self.arc.inner.lock().unwrap().location = location; + } + + /// Access the [`Serial`] of the last `pointer_enter` event, if that focus is still active. + /// + /// In other words this will return `None` again, once a `pointer_leave` event occurred. + #[cfg(feature = "wayland_frontend")] + pub fn last_enter(&self) -> Option { + *self.arc.wl_pointer.last_enter.lock().unwrap() + } + + fn get_seat(&self, data: &mut D) -> Seat { + let seat_state = data.seat_state(); + seat_state + .seats + .iter() + .find(|seat| seat.get_pointer().map(|h| &h == self).unwrap_or(false)) + .cloned() + .unwrap() + } +} + +impl PointerHandle +where + D: SeatHandler, + ::PointerFocus: Clone, +{ + /// Retrieve the current pointer focus + pub fn current_focus(&self) -> Option<::PointerFocus> { + self.arc + .inner + .lock() + .unwrap() + .focus + .clone() + .map(|(focus, _)| focus) + } +} + +/// This inner handle is accessed from inside a pointer grab logic, and directly +/// sends event to the client +pub struct PointerInnerHandle<'a, D: SeatHandler> { + inner: &'a mut PointerInternal, + seat: &'a Seat, +} + +impl fmt::Debug for PointerInnerHandle<'_, D> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PointerInnerHandle") + .field("inner", &self.inner) + .field("seat", &self.seat.arc.name) + .finish() + } +} + +impl PointerInnerHandle<'_, D> { + /// Change the current grab on this pointer to the provided grab + /// + /// Overwrites any current grab. + pub fn set_grab + 'static>( + &mut self, + handler: &mut dyn PointerGrab, + data: &mut D, + serial: Serial, + focus: Focus, + grab: G, + ) { + handler.unset(data); + self.inner.set_grab(data, self.seat, serial, grab, focus); + } + + /// Remove any current grab on this pointer, resetting it to the default behavior + /// + /// This will also restore the focus of the underlying pointer if restore_focus + /// is [`true`] + pub fn unset_grab( + &mut self, + handler: &mut dyn PointerGrab, + data: &mut D, + serial: Serial, + time: InputTime, + restore_focus: bool, + ) { + handler.unset(data); + self.inner + .unset_grab(data, self.seat, serial, time, restore_focus); + } + + /// Access the current focus of this pointer + pub fn current_focus(&self) -> Option<(::PointerFocus, Point)> { + self.inner.focus.clone() + } + + /// Access the current location of this pointer in the global space + pub fn current_location(&self) -> Point { + self.inner.location + } + + /// A list of the currently physically pressed buttons + /// + /// This still includes buttons that your grab have intercepted and not sent + /// to the client. + pub fn current_pressed(&self) -> &[u32] { + &self.inner.pressed_buttons + } + + /// Notify that the pointer moved + /// + /// You provide the new location of the pointer, in the form of: + /// + /// - The coordinates of the pointer in the global compositor space + /// - The surface on top of which the cursor is, and the coordinates of its + /// origin in the global compositor space (or `None` of the pointer is not + /// on top of a client surface). + /// + /// This will internally take care of notifying the appropriate client objects + /// of enter/motion/leave events. + pub fn motion( + &mut self, + data: &mut D, + focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + self.inner.motion(data, self.seat, focus, event); + } + + /// Notify about relative pointer motion + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the relative pointer protocol. + pub fn relative_motion( + &mut self, + data: &mut D, + focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + self.inner.relative_motion(data, self.seat, focus, event); + } + + /// Notify that a button was pressed + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface. + pub fn button(&mut self, data: &mut D, event: &ButtonEvent) { + if let Some((focused, _)) = self.inner.focus.as_mut() { + focused.button(self.seat, data, event); + } + } + + /// Notify that an axis was scrolled + /// + /// This will internally send the appropriate axis events to the client + /// objects matching with the currently focused surface. + pub fn axis(&mut self, data: &mut D, details: AxisFrame) { + if let Some((focused, _)) = self.inner.focus.as_mut() { + focused.axis(self.seat, data, details); + } + } + + /// End of a pointer frame + /// + /// This will internally send the appropriate frame event to the client + /// objects matching with the currently focused surface. + pub fn frame(&mut self, data: &mut D) { + if let Some((focused, _)) = self.inner.focus.as_mut() { + focused.frame(self.seat, data); + } + } + + /// Notify about swipe gesture begin + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_swipe_begin(&mut self, data: &mut D, event: &GestureSwipeBeginEvent) { + self.inner.gesture_swipe_begin(data, self.seat, event); + } + + /// Notify about swipe gesture update + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_swipe_update(&mut self, data: &mut D, event: &GestureSwipeUpdateEvent) { + self.inner.gesture_swipe_update(data, self.seat, event); + } + + /// Notify about swipe gesture end + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_swipe_end(&mut self, data: &mut D, event: &GestureSwipeEndEvent) { + self.inner.gesture_swipe_end(data, self.seat, event); + } + + /// Notify about pinch gesture begin + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_pinch_begin(&mut self, data: &mut D, event: &GesturePinchBeginEvent) { + self.inner.gesture_pinch_begin(data, self.seat, event); + } + + /// Notify about pinch gesture update + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_pinch_update(&mut self, data: &mut D, event: &GesturePinchUpdateEvent) { + self.inner.gesture_pinch_update(data, self.seat, event); + } + + /// Notify about pinch gesture end + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_pinch_end(&mut self, data: &mut D, event: &GesturePinchEndEvent) { + self.inner.gesture_pinch_end(data, self.seat, event); + } + + /// Notify about hold gesture begin + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_hold_begin(&mut self, data: &mut D, event: &GestureHoldBeginEvent) { + self.inner.gesture_hold_begin(data, self.seat, event); + } + + /// Notify about hold gesture end + /// + /// This will internally send the appropriate button event to the client + /// objects matching with the currently focused surface, if the client uses + /// the pointer gestures protocol. + pub fn gesture_hold_end(&mut self, data: &mut D, event: &GestureHoldEndEvent) { + self.inner.gesture_hold_end(data, self.seat, event); + } +} + +pub(crate) struct PointerInternal { + pub(crate) focus: Option<(::PointerFocus, Point)>, + pending_focus: Option<(::PointerFocus, Point)>, + pub(crate) location: Point, + grab: GrabStatus>, + pressed_buttons: Vec, +} + +// image_callback does not implement debug, so we have to impl Debug manually +impl fmt::Debug for PointerInternal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PointerInternal") + .field("focus", &self.focus) + .field("pending_focus", &self.pending_focus) + .field("location", &self.location) + .field("grab", &self.grab) + .field("pressed_buttons", &self.pressed_buttons) + .field("image_callback", &"...") + .finish() + } +} + +impl PointerInternal { + fn new() -> Self { + Self { + focus: None, + pending_focus: None, + location: (0.0, 0.0).into(), + grab: GrabStatus::None, + pressed_buttons: Vec::new(), + } + } + + fn set_grab + 'static>( + &mut self, + data: &mut D, + seat: &Seat, + serial: Serial, + grab: G, + focus: Focus, + ) { + if let GrabStatus::Active(_, handler) = &mut self.grab { + handler.unset(data); + } + self.grab = GrabStatus::Active(serial, Box::new(grab)); + + if matches!(focus, Focus::Clear) { + let location = self.location; + self.motion( + data, + seat, + None, + &MotionEvent { + location, + serial, + time: InputTime::now(), + }, + ); + } + } + + fn unset_grab( + &mut self, + data: &mut D, + seat: &Seat, + serial: Serial, + time: InputTime, + restore_focus: bool, + ) { + if let GrabStatus::Active(_, handler) = &mut self.grab { + handler.unset(data); + } + self.grab = GrabStatus::None; + if restore_focus { + // restore the focus + let location = self.location; + let focus = self.pending_focus.clone(); + self.motion( + data, + seat, + focus, + &MotionEvent { + location, + serial, + time, + }, + ); + } + } + + fn motion( + &mut self, + data: &mut D, + seat: &Seat, + focus: Option<(::PointerFocus, Point)>, + event: &MotionEvent, + ) { + self.location = event.location; + if let Some((focus, loc)) = focus { + let event = MotionEvent { + location: event.location - loc, + serial: event.serial, + time: event.time, + }; + let old_focus = self.focus.replace((focus.clone(), loc)); + match (focus, old_focus) { + (focus, Some((old_focus, _))) if focus == old_focus => { + // we were on top of a target and remained on it + focus.motion(seat, data, &event); + } + (focus, Some((old_focus, _))) => { + // the target has been replaced + focus.replace(old_focus, seat, data, &event); + } + (focus, None) => { + // we entered a new target + focus.enter(seat, data, &event); + } + }; + } else if let Some((old_focus, _)) = self.focus.take() { + old_focus.leave(seat, data, event.serial, event.time); + data.cursor_image(seat, CursorImageStatus::default_named()); + } + } + + fn relative_motion( + &mut self, + data: &mut D, + seat: &Seat, + _focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.relative_motion(seat, data, event); + } + } + + fn gesture_swipe_begin(&mut self, data: &mut D, seat: &Seat, event: &GestureSwipeBeginEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_swipe_begin(seat, data, event); + } + } + + fn gesture_swipe_update(&mut self, data: &mut D, seat: &Seat, event: &GestureSwipeUpdateEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_swipe_update(seat, data, event); + } + } + + fn gesture_swipe_end(&mut self, data: &mut D, seat: &Seat, event: &GestureSwipeEndEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_swipe_end(seat, data, event); + } + } + + fn gesture_pinch_begin(&mut self, data: &mut D, seat: &Seat, event: &GesturePinchBeginEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_pinch_begin(seat, data, event); + } + } + + fn gesture_pinch_update(&mut self, data: &mut D, seat: &Seat, event: &GesturePinchUpdateEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_pinch_update(seat, data, event); + } + } + + fn gesture_pinch_end(&mut self, data: &mut D, seat: &Seat, event: &GesturePinchEndEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_pinch_end(seat, data, event); + } + } + + fn gesture_hold_begin(&mut self, data: &mut D, seat: &Seat, event: &GestureHoldBeginEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_hold_begin(seat, data, event); + } + } + + fn gesture_hold_end(&mut self, data: &mut D, seat: &Seat, event: &GestureHoldEndEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.gesture_hold_end(seat, data, event); + } + } + + fn with_grab(&mut self, data: &mut D, seat: &Seat, f: F) + where + F: FnOnce(&mut D, &mut PointerInnerHandle<'_, D>, &mut dyn PointerGrab), + { + let mut grab = std::mem::replace(&mut self.grab, GrabStatus::Borrowed); + match grab { + GrabStatus::Borrowed => panic!("Accessed a pointer grab from within a pointer grab access."), + GrabStatus::Active(_, ref mut handler) => { + // If this grab is associated with a surface that is no longer alive, discard it + if let Some((ref focus, _)) = handler.start_data().focus { + if !focus.alive() { + handler.unset(data); + self.grab = GrabStatus::None; + f( + data, + &mut PointerInnerHandle { inner: self, seat }, + &mut DefaultGrab, + ); + return; + } + } + f( + data, + &mut PointerInnerHandle { inner: self, seat }, + &mut **handler, + ); + } + GrabStatus::None => { + f( + data, + &mut PointerInnerHandle { inner: self, seat }, + &mut DefaultGrab, + ); + } + } + + if let GrabStatus::Borrowed = self.grab { + // The grab has not been ended nor replaced, put it back in place + self.grab = grab; + } + } +} + +/// Defines the focus behavior for [`PointerHandle::set_grab`] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Focus { + /// Keep the current focus + Keep, + /// Clear the current focus + Clear, +} + +/// Pointer motion event +#[derive(Debug, Clone)] +pub struct MotionEvent { + /// Location of the pointer in compositor space + pub location: Point, + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Relative pointer motion event +#[derive(Debug, Clone)] +pub struct RelativeMotionEvent { + /// Motional vector + pub delta: Point, + /// Unaccelerated motion vector + pub delta_unaccel: Point, + /// Timestamp in microseconds + pub time: InputTime, +} + +/// Pointer button event +/// +/// Mouse button click and release notifications. +/// The location of the click is given by the last motion or enter event. +#[derive(Debug, Clone, Copy)] +pub struct ButtonEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp with microsecond granularity, with an undefined base. + pub time: InputTime, + /// Button that produced the event + /// + /// The button is a button code as defined in the + /// Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT. + /// + /// Any 16-bit button code value is reserved for future additions to the kernel's event code list. All other button codes above 0xFFFF are currently undefined but may be used in future versions of this protocol. + pub button: u32, + /// Physical state of the button + pub state: ButtonState, +} + +/// A frame of pointer axis events. +/// Frames of axis events should be considers as one logical action. +/// +/// Can be used with the builder pattern, e.g.: +/// +/// ```ignore +/// AxisFrame::new() +/// .source(AxisSource::Wheel) +/// .discrete(Axis::Vertical, 6) +/// .value(Axis::Vertical, 30, time) +/// .stop(Axis::Vertical); +/// ``` +#[must_use = "AxisFrame uses a builder-like pattern, so its result must be used"] +#[derive(Copy, Clone, Debug)] +pub struct AxisFrame { + /// Source of the axis event, if known + pub source: Option, + /// Direction of the physical motion that caused axis event + pub relative_direction: (AxisRelativeDirection, AxisRelativeDirection), + /// Time of the axis event + pub time: InputTime, + /// Raw scroll value per axis of the event + pub axis: (f64, f64), + /// Discrete representation of scroll value per axis, if available + pub v120: Option<(i32, i32)>, + /// If the axis is considered having stopped movement + /// + /// Only useful in conjunction of AxisSource::Finger events + pub stop: (bool, bool), +} + +/// Gesture swipe begin event +#[derive(Debug, Clone)] +pub struct GestureSwipeBeginEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Number of fingers of the event + pub fingers: u32, +} + +/// Gesture swipe update event +#[derive(Debug, Clone)] +pub struct GestureSwipeUpdateEvent { + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Offset of the logical center of the gesture relative to the previous event + pub delta: Point, +} + +/// Gesture swipe end event +#[derive(Debug, Clone)] +pub struct GestureSwipeEndEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Whether the gesture was cancelled + pub cancelled: bool, +} + +/// Gesture pinch begin event +#[derive(Debug, Clone)] +pub struct GesturePinchBeginEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Number of fingers of the event + pub fingers: u32, +} + +/// Gesture pinch update event +#[derive(Debug, Clone)] +pub struct GesturePinchUpdateEvent { + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Offset of the logical center of the gesture relative to the previous event + pub delta: Point, + /// Absolute scale compared to the begin event + pub scale: f64, + /// Relative angle in degrees clockwise compared to the previous event + pub rotation: f64, +} + +/// Gesture pinch end event +#[derive(Debug, Clone)] +pub struct GesturePinchEndEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Whether the gesture was cancelled + pub cancelled: bool, +} + +/// Gesture hold begin event +#[derive(Debug, Clone)] +pub struct GestureHoldBeginEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Number of fingers of the event + pub fingers: u32, +} + +/// Gesture hold end event +#[derive(Debug, Clone)] +pub struct GestureHoldEndEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, + /// Whether the gesture was cancelled + pub cancelled: bool, +} + +impl AxisFrame { + /// Create a new frame of axis events + pub fn new(time: InputTime) -> Self { + AxisFrame { + source: None, + relative_direction: (AxisRelativeDirection::Identical, AxisRelativeDirection::Identical), + time, + axis: (0.0, 0.0), + v120: None, + stop: (false, false), + } + } + + /// Specify the source of the axis events + /// + /// This event is optional, if no source is known, you can ignore this call. + /// Only one source event is allowed per frame. + /// + /// Using the [`AxisSource::Finger`] requires a stop event to be send, + /// when the user lifts off the finger (not necessarily in the same frame). + pub fn source(mut self, source: AxisSource) -> Self { + self.source = Some(source); + self + } + + /// Specify the direction of the physical motion, relative to axis direction + pub fn relative_direction(mut self, axis: Axis, relative_direction: AxisRelativeDirection) -> Self { + match axis { + Axis::Horizontal => { + self.relative_direction.0 = relative_direction; + } + Axis::Vertical => { + self.relative_direction.1 = relative_direction; + } + }; + self + } + + /// Specify discrete scrolling steps additionally to the computed value. + /// + /// This event is optional and gives the client additional information about + /// the nature of the axis event. E.g. a scroll wheel might issue separate steps, + /// while a touchpad may never issue this event as it has no steps. + pub fn v120(mut self, axis: Axis, steps: i32) -> Self { + let v120 = self.v120.get_or_insert_with(Default::default); + match axis { + Axis::Horizontal => { + v120.0 = steps; + } + Axis::Vertical => { + v120.1 = steps; + } + }; + self + } + + /// The actual scroll value. This event is the only required one, but can also + /// be send multiple times. The values off one frame will be accumulated by the client. + pub fn value(mut self, axis: Axis, value: f64) -> Self { + match axis { + Axis::Horizontal => { + self.axis.0 += value; + } + Axis::Vertical => { + self.axis.1 += value; + } + }; + self + } + + /// Notification of stop of scrolling on an axis. + /// + /// This event is required for sources of the [`AxisSource::Finger`] type + /// and otherwise optional. + pub fn stop(mut self, axis: Axis) -> Self { + match axis { + Axis::Horizontal => { + self.stop.0 = true; + } + Axis::Vertical => { + self.stop.1 = true; + } + }; + self + } +} diff --git a/vendor/smithay/src/input/tablet/mod.rs b/vendor/smithay/src/input/tablet/mod.rs new file mode 100644 index 0000000..733310d --- /dev/null +++ b/vendor/smithay/src/input/tablet/mod.rs @@ -0,0 +1,588 @@ +//! Graphic tablet related types for smithay's input abstraction +//! +//! This module provides some types loosely resembling instances of wayland tablet seat, tablet and +//! tablet tool. It is however not directly tied to wayland and can be used to multiplex various +//! tablet-related operations between different handlers. +//! +//! If the `wayland_frontend`-feature is enabled, the `smithay::wayland::tablet_manager`-module +//! provides additional functionality for the provided types of this module to map them to +//! advertised wayland globals and objects. +//! +//! ## How to use it +//! +//! To start using this module, you need to create a [`TabletSeat`] from an existing [`Seat`]. +//! Additionally, you want to implement the [`TabletSeatHandler`] trait. +//! +//! ### Initialization +//! +//! ``` +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! use smithay::input::tablet::{TabletSeatHandler, TabletSeatTrait}; +//! use smithay::backend::input::TabletToolDescriptor; +//! # use smithay::backend::input::{InputTime, KeyState}; +//! # use smithay::input::{ +//! # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, +//! # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, +//! # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, +//! # GestureHoldBeginEvent, GestureHoldEndEvent}, +//! # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, +//! # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, +//! # tablet::{Tablet, tool::{self, TabletToolTarget}}, +//! # }; +//! # use smithay::utils::{IsAlive, Serial}; +//! +//! struct State { +//! seat_state: SeatState, +//! // ... +//! }; +//! +//! let mut seat_state = SeatState::::new(); +//! +//! // create the seat +//! let seat = seat_state.new_seat( +//! "seat-0", // the name of the seat, will be advertized to clients +//! ); +//! // create the associated tablet seat. +//! let tablet_seat = seat.tablet_seat(); +//! +//! # #[derive(Debug, Clone, PartialEq)] +//! # struct Target; +//! # impl IsAlive for Target { +//! # fn alive(&self) -> bool { true } +//! # } +//! # impl PointerTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} +//! # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} +//! # fn frame(&self, seat: &Seat, data: &mut State) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} +//! # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} +//! # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} +//! # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} +//! # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} +//! # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} +//! # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} +//! # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} +//! # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} +//! # } +//! # impl KeyboardTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} +//! # fn key( +//! # &self, +//! # seat: &Seat, +//! # data: &mut State, +//! # key: KeysymHandle<'_>, +//! # state: KeyState, +//! # serial: Serial, +//! # time: InputTime, +//! # ) {} +//! # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} +//! # } +//! # impl TouchTarget for Target { +//! # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} +//! # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} +//! # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } +//! # } +//! # impl TabletToolTarget for Target { +//! # fn proximity_in(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, tablet: &Tablet, serial: Serial) {} +//! # fn proximity_out(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor) {} +//! # fn down(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::MotionEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, frame: tool::AxisFrame) {} +//! # fn button(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::ButtonEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, time: InputTime) {} +//! # } +//! +//! // implement the required traits +//! impl SeatHandler for State { +//! type KeyboardFocus = Target; +//! type PointerFocus = Target; +//! type TouchFocus = Target; +//! +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) { +//! // handle focus changes, if you need to ... +//! } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { +//! // handle new images for the cursor ... +//! } +//! } +//! +//! impl TabletSeatHandler for State { +//! type ToolFocus = Target; +//! +//! fn tablet_tool_image(&mut self, tool: &TabletToolDescriptor, image: CursorImageStatus) { +//! // handle new image for the given tool. +//! } +//! } +//! ``` +//! +//! ### Run usage +//! +//! Once the tablet seat is initialized, you can add tablet and tools to it. +//! +//! Currently, pads are unsupported by this module. +//! +//! You can add tablet and tools via methods of the [`TabletSeat`] struct: +//! [`TabletSeat::add_tablet`] and [`TabletSeat::add_tool`]. +//! These method return handles that can be cloned and sent across thread, so you can keep them +//! around in your event-handling code to forward inputs to your clients. +//! + +use std::{ + collections::HashMap, + fmt, + hash::Hash, + path::PathBuf, + sync::{Arc, Mutex, Weak}, +}; + +use crate::{ + backend::input::{Device, TabletToolDescriptor}, + input::{ + Seat, SeatHandler, + pointer::CursorImageStatus, + tablet::tool::{ + DownGrab, GrabStartData as TabletToolGrabStartData, TabletToolGrab, TabletToolHandle, + TabletToolTarget, + }, + }, +}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::Weak as WlWeak; + +pub mod tool; + +/// Description of graphics tablet device +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct TabletDescriptor { + /// Tablet device name + pub name: String, + /// Tablet device USB (product,vendor) id + pub usb_id: Option<(u32, u32)>, + /// Path to the device + pub syspath: Option, +} + +impl From<&D> for TabletDescriptor { + #[inline] + fn from(device: &D) -> Self { + TabletDescriptor { + name: device.name(), + syspath: device.syspath(), + usb_id: device.usb_id(), + } + } +} + +pub(crate) struct TabletRc { + pub(crate) descriptor: TabletDescriptor, + #[cfg(feature = "wayland_frontend")] + pub(crate) wp_tablet: crate::wayland::tablet_manager::tablet::WpTabletHandle, +} + +#[cfg(not(feature = "wayland_frontend"))] +impl fmt::Debug for TabletRc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TabletRc") + .field("descriptor", &self.descriptor) + .finish() + } +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for TabletRc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TabletRc") + .field("descriptor", &self.descriptor) + .field("wp_tablet", &self.wp_tablet) + .finish() + } +} + +/// Handle to a tablet device +/// +/// Tablet represents one graphics tablet device +pub struct Tablet { + pub(crate) arc: Arc, +} + +impl Tablet { + pub(super) fn new(descriptor: TabletDescriptor) -> Self { + Self { + arc: Arc::new(TabletRc { + descriptor, + #[cfg(feature = "wayland_frontend")] + wp_tablet: Default::default(), + }), + } + } + + /// Get a descriptor for this tablet + pub fn descriptor(&self) -> &TabletDescriptor { + &self.arc.descriptor + } +} + +impl fmt::Debug for Tablet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Tablet").field("arc", &self.arc).finish() + } +} + +impl Clone for Tablet { + fn clone(&self) -> Self { + Self { + arc: self.arc.clone(), + } + } +} + +impl PartialEq for Tablet { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} + +impl Eq for Tablet {} + +impl Hash for Tablet { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.arc).hash(state) + } +} + +/// Weak variant of a [`Tablet`] +/// +/// Does not keep associated data alive, and can be used to refer to a potentially already destroyed +/// tablet. +#[derive(Debug)] +pub struct WeakTablet(Weak); + +impl Clone for WeakTablet { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl WeakTablet { + /// Try to retrieve the original `Tablet` if it still exists + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(|arc| Tablet { arc }) + } + + /// Check if this tablet is still alive + pub fn is_alive(&self) -> bool { + self.0.strong_count() != 0 + } +} + +impl Tablet { + /// Create a weak reference to this tablet + pub fn downgrade(&self) -> WeakTablet { + WeakTablet(Arc::downgrade(&self.arc)) + } +} + +/// Extends [`Seat`] with graphic tablet specific functionality +pub trait TabletSeatTrait { + /// Get tablet seat associated with this seat + fn tablet_seat(&self) -> TabletSeat; +} + +/// Extends [`Seat`] with graphic tablet specific functionality. +impl TabletSeatTrait for Seat { + fn tablet_seat(&self) -> TabletSeat { + let user_data = self.user_data(); + user_data.get_or_insert(TabletSeat::default).clone() + } +} + +/// Handler trait for Tablet Seats +pub trait TabletSeatHandler: SeatHandler + Sized { + /// Type used to represent the target currently holding the tablet's tool focus + type ToolFocus: TabletToolTarget + PartialEq + Clone + 'static; + + /// Callback that will be notified whenever a client requests to set a custom tool image. + fn tablet_tool_image(&mut self, tool: &TabletToolDescriptor, image: CursorImageStatus) { + let _ = tool; + let _ = image; + } + + /// Provides the implicit tool grab for down events + /// + /// When the user presses down a tool, an implicit grab is installed. If your + /// compositor needs custom behavior for this grab, you can implement this trait item and + /// return your own [`TabletToolGrab`] implementation. + fn down_grab(&mut self, start_data: TabletToolGrabStartData) -> impl TabletToolGrab { + DownGrab::new(start_data) + } +} + +/// Handle to a tablet seat +/// +/// TabletSeat extends [`Seat`] with graphic tablet specific functionality. They can be used to +/// advertise available graphics tablets and tools. +pub struct TabletSeat { + pub(crate) arc: Arc>>, +} + +impl TabletSeat { + /// Create a weak reference to this tablet seat + pub fn downgrade(&self) -> WeakTabletSeat { + WeakTabletSeat(Arc::downgrade(&self.arc)) + } +} + +impl Default for TabletSeat { + fn default() -> Self { + Self { + arc: Default::default(), + } + } +} + +impl Clone for TabletSeat { + fn clone(&self) -> Self { + Self { + arc: self.arc.clone(), + } + } +} + +impl fmt::Debug for TabletSeat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TabletSeat").field("arc", &self.arc).finish() + } +} + +impl PartialEq for TabletSeat { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} + +impl Eq for TabletSeat {} + +impl Hash for TabletSeat { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.arc).hash(state) + } +} + +/// Weak variant of a [`TabletSeat`]. +/// +/// Does not keep associated user data alive, and can be used to refer to a potentially already +/// destroyed seat. +#[derive(Debug)] +pub struct WeakTabletSeat(Weak>>); + +impl Clone for WeakTabletSeat { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl WeakTabletSeat { + /// Try to retrieve the original `TabletSeat`, if it still exists + pub fn upgrade(&self) -> Option> { + self.0.upgrade().map(|arc| TabletSeat { arc }) + } + + /// Check if the tablet seat is still alive + pub fn is_alive(&self) -> bool { + self.0.strong_count() != 0 + } +} + +pub(crate) struct Inner { + pub(crate) tablets: HashMap, + pub(crate) tools: HashMap>, + #[cfg(feature = "wayland_frontend")] + pub(crate) instances: + Vec>, +} + +#[cfg(not(feature = "wayland_frontend"))] +impl fmt::Debug for Inner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Inner") + .field("tablets", &self.tablets) + .field("tools", &self.tools) + .finish() + } +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for Inner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Inner") + .field("tablets", &self.tablets) + .field("tools", &self.tools) + .field("instances", &self.instances) + .finish() + } +} + +impl Default for Inner { + fn default() -> Self { + Self { + tablets: HashMap::default(), + tools: HashMap::default(), + #[cfg(feature = "wayland_frontend")] + instances: Vec::default(), + } + } +} + +impl Inner { + pub(crate) fn add_tablet( + &mut self, + tablet_desc: &TabletDescriptor, + default: impl FnOnce(TabletDescriptor) -> Tablet, + ) -> Tablet { + self.tablets.remove(tablet_desc); + + self.tablets + .entry(tablet_desc.clone()) + .or_insert_with(|| default(tablet_desc.clone())) + .clone() + } + + pub(crate) fn add_tool( + &mut self, + tool_desc: &TabletToolDescriptor, + default_grab: F, + default: impl FnOnce(TabletToolDescriptor, F) -> TabletToolHandle, + ) -> TabletToolHandle + where + F: Fn() -> Box> + Send + 'static, + { + self.tools.remove(tool_desc); + + self.tools + .entry(tool_desc.clone()) + .or_insert_with(|| default(tool_desc.clone(), default_grab)) + .clone() + } +} + +impl TabletSeat { + /// Add a new tablet to a seat. + /// + /// You can either add tablet on [DeviceAdded] event, or you can add tablet based + /// on tool event, then clients will not know about devices that are not being used. + /// + /// If the tablet was already known it removes it and recreate a new handle. Because + /// [`TabletToolHandle`] will keep a handle to the [`Tablet`] while in proximity, it may appears + /// to clients that the tablet hasn't been removed until the tool leave its proximity. + /// + /// [DeviceAdded]: crate::backend::input::InputEvent::DeviceAdded + pub fn add_tablet(&self, tablet_desc: &TabletDescriptor) -> Tablet { + self.arc.lock().unwrap().add_tablet(tablet_desc, Tablet::new) + } + + /// Get a handler to a tablet + pub fn get_tablet(&self, tablet_desc: &TabletDescriptor) -> Option { + self.arc.lock().unwrap().tablets.get(tablet_desc).cloned() + } + + /// Count all tablet devices + pub fn count_tablets(&self) -> usize { + self.arc.lock().unwrap().tablets.len() + } + + /// Remove tablet device + /// + /// Called when tablet is no longer available, for example on [DeviceRemoved] event. + /// + /// [DeviceRemoved]: crate::backend::input::InputEvent::DeviceRemoved + pub fn remove_tablet(&self, tablet_desc: &TabletDescriptor) { + self.arc.lock().unwrap().tablets.remove(tablet_desc); + } + + /// Remove all tablet devices + pub fn clear_tablets(&self) { + self.arc.lock().unwrap().tablets.clear(); + } + + /// Add a new tool to a seat. + /// + /// Tool are usually added on [TabletToolProximityEvent] event. + /// + /// Calling this method on a seat that already has the same tool will overwrite it, and will be + /// seen by clients as if the tool was removed and a new one was added. + /// + /// [TabletToolProximityEvent]: crate::backend::input::InputEvent::TabletToolProximity + pub fn add_tool(&self, tool_desc: &TabletToolDescriptor) -> TabletToolHandle { + self.add_tool_with_default_grab(tool_desc, || Box::new(tool::DefaultGrab)) + } + + /// Add a new tool to a seat and allows the use of a custom default [`TabletToolGrab`] + /// + /// The default ghrab is used in case no other grab is currently active. When using + /// [`TabletSeat::add_tool`], it will use [`tool::DefaultGrab`] which will install + /// [`tool::DownGrab`] on a down event. [`tool::DownGrab`] makes sure all further event will use + /// the same target until an up or physical proximity out event. + /// + /// See [`TabletSeat::add_tool`] for more information. + pub fn add_tool_with_default_grab( + &self, + tool_desc: &TabletToolDescriptor, + default_grab: F, + ) -> TabletToolHandle + where + F: Fn() -> Box> + Send + 'static, + { + self.arc + .lock() + .unwrap() + .add_tool(tool_desc, default_grab, TabletToolHandle::new) + } + + /// Get a handle to a tablet tool. + pub fn get_tool(&self, tool_desc: &TabletToolDescriptor) -> Option> { + self.arc.lock().unwrap().tools.get(tool_desc).cloned() + } + + /// Count all tablet tool devices + pub fn count_tools(&self) -> usize { + self.arc.lock().unwrap().tools.len() + } + + /// Get a map of all tablet tools known by this [`TabletSeat`]. + pub fn get_tools(&self) -> HashMap> { + self.arc.lock().unwrap().tools.clone() + } + + /// Remove tablet tool device + /// + /// Policy of tool removal is a compositor-specific. + /// + /// One possible policy would be to remove a tool when all tablets the tool was used on are removed. + pub fn remove_tool(&self, tool_desc: &TabletToolDescriptor) { + self.arc.lock().unwrap().tools.remove(tool_desc); + } + + /// Remove all tablet tool devices + pub fn clear_tools(&self) { + self.arc.lock().unwrap().tools.clear(); + } +} diff --git a/vendor/smithay/src/input/tablet/tool/grab.rs b/vendor/smithay/src/input/tablet/tool/grab.rs new file mode 100644 index 0000000..6e786ca --- /dev/null +++ b/vendor/smithay/src/input/tablet/tool/grab.rs @@ -0,0 +1,326 @@ +use std::fmt; + +use downcast_rs::{Downcast, impl_downcast}; + +use crate::{ + backend::input::InputTime, + input::{ + pointer::Focus, + tablet::{ + TabletSeatHandler, + tool::{ + AxisFrame, ButtonEvent, DownEvent, MotionEvent, ProximityInEvent, ProximityOutEvent, + TabletToolInnerHandle, UpEvent, + }, + }, + }, + utils::{Logical, Point}, +}; + +/// A trait to implemenent a tablet tool grab +/// +/// In some context, it is necessary to temporarily change the behavior of the tablet tool. This is +/// typically known as a grab. A typical example would be, during a drag'n'drop operation, the +/// underlying surfaces will no longer receive classic tool event, but rather special events. +/// +/// This trait is the interface to intercept regular tool events and change them as needed. Its +/// interface mimics the [`TabletToolHandle`] interface. +/// +/// Any interaction with [`TabletToolHandle`] should be done using [`TabletToolInnerHandle`] as +/// handle is borrowed;pcled before grab methods are called, so calling methods on +/// [`TabletToolHandle`] would result in a deadlock. +/// +/// Ig your logic decides that the grab should end, both [`TabletToolInnerHandle`] and +/// [`TabletToolHandle`] have a method to change it. +/// +/// When your grab ends (either as you requested it or if it was forcefully cancelled by the +/// server), the struct implemented this trait will be dropped. As such, you should put clean-up +/// logic in the destructor, rather than trying to guess when the grab will end. +/// +/// [`TabletToolHandle`]: super::TabletToolHandle +pub trait TabletToolGrab: Send + Downcast { + /// The data about the event that started the grab. + fn start_data(&self) -> &GrabStartData; + + /// A physical proximity_in was reported. + /// + /// This method allows you to attach additional behavior to a proximity in event, possibly + /// altering it. You generally will want to invoke `TabletToolInnerHandle::proximity_in()` as part of + /// your processing. If you don't the rest of the compositor will behave as if the event never + /// occurred. + /// + /// Some grabs (such as drag'n'drop, shell resize and motion) unset the focus while they are + /// active, this is achieved by just setting the focus to None when invoking + /// `TabletToolInnerHandle::proximity_in()`. + /// + /// This event is only relevant for default grabs. + fn proximity_in( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + focus: Option<(::ToolFocus, Point)>, + event: &ProximityInEvent, + ) { + handle.proximity_in(data, focus, event); + } + + /// A physical proximity_out was reported. + /// + /// This method allows you to attach additional behavior to a proximity out event. You generally + /// want to invoke `TabletToolInnerHandle::proximity_out()` as part of your processing. + fn proximity_out( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + event: &ProximityOutEvent, + ); + + /// A motion was reported + /// + /// This method allows you to attach additional behavior to a motion event, possibly altering + /// it. You generally want to invoke `TabletToolInnerHandle::motion()` as part of your + /// processing. If you don't, the rest of the compositor will behave as if the event never + /// occurred. + /// + /// Some grabs (such as drag'n'drop, shell resize and motion) unset the focus while they are + /// active, this is achieved by just setting the focus to `None` when invoking + /// `TabletToolInnerHandle::motion()`. + fn motion( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + focus: Option<(::ToolFocus, Point)>, + event: &MotionEvent, + ); + + /// A tip down was reported. + /// + /// This method allows you to attach additional behavior to a down event, possibly altering it. + /// You generally will want to invoke `TabletToolInnerHandle::down()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the down event never occurred. + fn down(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &DownEvent); + + /// A tip up was reported. + /// + /// This method allows you to attach additional behavior to an up event, possibly altering it. + /// You generally will want to invoke `TabletToolInnerHandle::up()` as part of your processing. + /// If you don't, the rest of the compositor will behave as if the down event never occurred. + fn up(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &UpEvent); + + /// A button press was reported + /// + /// This method allows you to attach additional behavior to an up event, possibly altering it. + /// You generally will want to invoke `TabletToolInnerHandle::button()` as part of your + /// processing. If you don't, the rest of the compositor will behave as if the down event never + /// occurred. + fn button(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &ButtonEvent); + + /// An axis frame was reported + /// + /// This method allows you to attach additional behavior to an up event, possibly altering it. + /// You generally will want to invoke `TabletToolInnerHandle::axis()` as part of your + /// processing. If you don't, the rest of the compositor will behave as if the down event never + /// occurred. + fn axis(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, frame: AxisFrame); + + /// End of a tablet tool frame + /// + /// A frame groups associated events. This terminate the frame. + fn frame(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, time: InputTime); + + /// The grab has been unset or replaced with another grab. + fn unset(&mut self, data: &mut D); +} + +impl_downcast!(TabletToolGrab where D: TabletSeatHandler); + +/// Event that caused the grab to start. +#[derive(Debug, Clone, Copy)] +pub enum GrabTrigger { + /// Grab was initiated following a ProximityIn event. + Proximity, + /// Grab was initiated following a TipDown event. + Tip, + /// Grab was initiated following a Button event. + Button(u32), +} + +/// Data about the event that started the grab. +pub struct GrabStartData { + /// The focused surface and its location, if any, at the start of the grab. + /// + /// The location coordinates are in the global compositor space. + pub focus: Option<(::ToolFocus, Point)>, + /// The event that triggered the grab. + pub trigger: GrabTrigger, + /// The location of the tool when the grab was initiated, in the global compositor space. + pub location: Point, +} + +impl fmt::Debug for GrabStartData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GrabStartData") + .field("focus", &self.focus.as_ref().map(|_| "...")) + .field("trigger", &self.trigger) + .field("location", &self.location) + .finish() + } +} + +impl Clone for GrabStartData { + fn clone(&self) -> Self { + GrabStartData { + focus: self.focus.clone(), + trigger: self.trigger, + location: self.location, + } + } +} + +/// Tablet Tool default grab. +#[derive(Debug)] +pub struct DefaultGrab; + +impl TabletToolGrab for DefaultGrab { + fn start_data(&self) -> &GrabStartData { + unreachable!(); + } + + fn proximity_out( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + event: &ProximityOutEvent, + ) { + handle.proximity_out(data, event); + } + + fn motion( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + focus: Option<(::ToolFocus, Point)>, + event: &MotionEvent, + ) { + handle.motion(data, focus, event); + } + + fn down(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &DownEvent) { + handle.down(data, event); + + let grab = data.down_grab(GrabStartData { + focus: handle.current_focus(), + trigger: GrabTrigger::Tip, + location: handle.current_location(), + }); + handle.set_grab(self, data, event.time, event.serial, Focus::Keep, grab); + } + + fn up(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &UpEvent) { + handle.up(data, event); + } + + fn button(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &ButtonEvent) { + handle.button(data, event); + } + + fn axis(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, frame: AxisFrame) { + handle.axis(data, frame); + } + + fn frame(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, time: InputTime) { + handle.frame(data, time); + } + + fn unset(&mut self, _data: &mut D) {} +} + +/// A down grab, basic grab started when a user touch the tablet with a tool above a surface, to +/// maintain it focused until the user lift the tool. +pub struct DownGrab { + start_data: GrabStartData, + focus: Option<(::ToolFocus, Point)>, +} + +impl DownGrab { + pub(in crate::input) fn new(start_data: GrabStartData) -> Self { + Self { + focus: start_data.focus.clone(), + start_data, + } + } +} + +impl fmt::Debug for DownGrab { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DownGrab") + .field("start_data", &self.start_data) + .finish() + } +} + +impl TabletToolGrab for DownGrab { + fn start_data(&self) -> &GrabStartData { + &self.start_data + } + + fn proximity_in( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + focus: Option<(::ToolFocus, Point)>, + event: &ProximityInEvent, + ) { + handle.proximity_in(data, focus, event); + } + + fn proximity_out( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + event: &ProximityOutEvent, + ) { + handle.proximity_out(data, event); + handle.unset_grab(self, data, event.serial, event.time, false); + } + + fn motion( + &mut self, + data: &mut D, + handle: &mut TabletToolInnerHandle<'_, D>, + focus: Option<(::ToolFocus, Point)>, + event: &MotionEvent, + ) { + if let (Some((new_target, new_location)), Some((grab_target, grab_location))) = + (&focus, self.focus.as_mut()) + { + if *new_target == *grab_target { + *grab_location = *new_location; + } + } + handle.motion(data, self.focus.clone(), event); + } + + fn down(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &DownEvent) { + handle.down(data, event); + } + + fn up(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &UpEvent) { + handle.up(data, event); + handle.unset_grab(self, data, event.serial, event.time, true); + } + + fn button(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, event: &ButtonEvent) { + handle.button(data, event); + } + + fn axis(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, frame: AxisFrame) { + handle.axis(data, frame); + } + + fn frame(&mut self, data: &mut D, handle: &mut TabletToolInnerHandle<'_, D>, time: InputTime) { + handle.frame(data, time); + } + + fn unset(&mut self, _data: &mut D) {} +} diff --git a/vendor/smithay/src/input/tablet/tool/mod.rs b/vendor/smithay/src/input/tablet/tool/mod.rs new file mode 100644 index 0000000..d66d198 --- /dev/null +++ b/vendor/smithay/src/input/tablet/tool/mod.rs @@ -0,0 +1,1257 @@ +//! Tablet Tool related types for smithay's input abstraction + +use core::fmt; +use std::sync::{Arc, Mutex}; +use std::{hash::Hash, sync::Weak}; + +use crate::{ + backend::input::{ButtonState, InputTime, TabletToolDescriptor}, + input::{ + GrabStatus, Seat, SeatHandler, + pointer::Focus, + tablet::{Tablet, TabletSeat, TabletSeatHandler}, + }, + utils::{IsAlive, Logical, Point, Serial}, +}; + +pub(crate) mod grab; +pub use grab::{DefaultGrab, DownGrab, GrabStartData, GrabTrigger, TabletToolGrab}; + +pub(crate) struct TabletToolRc { + pub(crate) descriptor: TabletToolDescriptor, + // We want to drop the wp_tablet_tool before the tablet stored in inner, so clients receive + // tool's destroy events before tablet's destroy. + #[cfg(feature = "wayland_frontend")] + pub(crate) wp_tablet_tool: crate::wayland::tablet_manager::tablet_tool::WpTabletToolHandle, + pub(crate) inner: Mutex>, +} + +#[cfg(not(feature = "wayland_frontend"))] +impl fmt::Debug for TabletToolRc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TabletToolRc") + .field("descriptor", &self.descriptor) + .field("inner", &self.inner) + .finish() + } +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for TabletToolRc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TabletToolRc") + .field("descriptor", &self.descriptor) + .field("wp_tablet_tool", &self.wp_tablet_tool) + .field("inner", &self.inner) + .finish() + } +} + +/// Handle to a tablet tool +/// +/// It can be cloned and all clones manipulate the same internal state. +/// +/// This handle gives you access to an interface to sent tablet tool events to your clients. When +/// sending events using this handle, they will be intercepted by a tablet tool grab if any is +/// active. See [`TabletToolGrab`] trait for details. +/// +/// As a tablet tool must be in proximity to a tablet, most methods on this object expect a prior +/// call to [`TabletToolHandle::proximity_in`]. +pub struct TabletToolHandle { + pub(crate) arc: Arc>, +} + +impl fmt::Debug for TabletToolHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TabletToolHandle") + .field("arc", &self.arc) + .finish() + } +} + +impl Clone for TabletToolHandle { + fn clone(&self) -> Self { + Self { + arc: self.arc.clone(), + } + } +} + +impl PartialEq for TabletToolHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} + +impl Eq for TabletToolHandle {} + +impl Hash for TabletToolHandle { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.arc).hash(state); + } +} + +impl TabletToolHandle { + pub(super) fn new(descriptor: TabletToolDescriptor, default_grab: F) -> Self + where + F: Fn() -> Box> + Send + 'static, + { + Self { + arc: Arc::new(TabletToolRc { + descriptor, + inner: Mutex::new(TabletToolInternal::new(default_grab)), + #[cfg(feature = "wayland_frontend")] + wp_tablet_tool: Default::default(), + }), + } + } + + /// Change the current grab on this tablet tool to the provided grab. + /// + /// If focus is set to [`Focus::Clear`], any currently focused client object will be unfocused. + /// + /// Overwrites any current grab. + /// + /// Calling this function prior to calling proximity_in will not set the grab. + pub fn set_grab + 'static>( + &self, + data: &mut D, + grab: G, + time: InputTime, + serial: Serial, + focus: Focus, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + + inner.set_grab(data, &seat, &self.arc.descriptor, grab, time, serial, focus); + } + + /// Remove any current grab on this tablet tool, resetting it to the default behavior. + pub fn unset_grab(&self, data: &mut D, serial: Serial, time: InputTime) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + + inner.unset_grab(data, &seat, &self.arc.descriptor, serial, time, true); + } + + /// Check if this tablet tool is currently grabbed with this serial + pub fn has_grab(&self, serial: Serial) -> bool { + self.with_grab(|s, _| s == serial).unwrap_or(false) + } + + /// Check if this tablet tool is currently being grabbed. + pub fn is_grabbed(&self) -> bool { + let guard = self.arc.inner.lock().unwrap(); + !matches!(guard.grab, GrabStatus::None) + } + + /// Return the start data for the grab, if any. + pub fn grab_start_data(&self) -> Option> { + self.with_grab(|_, g| g.start_data().clone()) + } + + /// Call `f` with the active grab, if any. + pub fn with_grab(&self, f: impl FnOnce(Serial, &dyn TabletToolGrab) -> T) -> Option { + let guard = self.arc.inner.lock().unwrap(); + if let GrabStatus::Active(s, g) = &guard.grab { + Some(f(*s, &**g)) + } else { + None + } + } + + /// Notify that a tool entered physical proximity with a tablet. + /// + /// You must invoke this method before any other events. Failing to do so will make the + /// compositor ignore all other calls. + /// + /// You provide the location of the tool in the form of: + /// - The coordinates of the tool in the global compositor space + /// - The surface on top of which the tool is, and the coordinates of its origin in the global + /// compositor space (or `None` if the tool is not over a client surface). + /// + /// This will internally generate the following event to the client object, if any: + /// - ProximityIn + /// - Axis, if an axis frame is available + /// - Motion + /// - Button press, if any button were held down. + /// + /// The [`Tablet`] will be stored until a call to `TabletToolHandle::proximity_out``. + /// + /// If this event is invoked while the tool is already in proximity with a tablet, a synthetic + /// ProximityOut will be generated to reset the instance to a sane state. + pub fn proximity_in( + &self, + data: &mut D, + focus: Option<(::ToolFocus, Point)>, + tablet: Tablet, + event: &ProximityInEvent, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + + if inner.in_proximity() { + tracing::warn!("proximity_in was called, but the tool is already in proximity with a tablet."); + inner.proximity_out_impl( + data, + &seat, + &self.arc.descriptor, + &ProximityOutEvent { + serial: event.serial, + time: event.time, + }, + ); + } + + inner.tablet = Some(tablet); + inner.location = Some(event.location); + inner.pending_focus.clone_from(&focus); + + inner.with_grab(data, &seat, &self.arc.descriptor, |data, handle, grab| { + grab.proximity_in(data, handle, focus, event); + }); + } + + /// Notify that a tool left physical proximity with a tablet. + /// + /// This will internally generate the following events: + /// - Button release, for any pressed button + /// - Up, if the tool tip was down + /// - ProximityOut + pub fn proximity_out(&self, data: &mut D, event: &ProximityOutEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + if !inner.in_proximity() { + tracing::warn!("proximity_out was called, but the tool hasn't entered tablet proximity."); + return; + } + + let seat = self.get_seat(data); + + inner.proximity_out_impl(data, &seat, &self.arc.descriptor, event); + } + + /// Notify that a tool moved above a tablet. + /// + /// You provide the new location of the tool, in the form of: + /// - The coordinates of the tool in the global compositor space + /// - The target on top of which the tool is, and the coordinates of its origin in the global + /// compositor space (or `None` if the tool is not above a client's target.). + /// + /// This will internally take care of notifying the appropriate client objects of + /// proximity_in/motion/proximity_out events. + pub fn motion( + &self, + data: &mut D, + focus: Option<(::ToolFocus, Point)>, + event: &MotionEvent, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + if !inner.in_proximity() { + tracing::warn!("motion was called, but the tool hasn't entered tablet proximity."); + return; + } + + inner.pending_focus.clone_from(&focus); + let seat = self.get_seat(data); + + inner.with_grab(data, &seat, &self.arc.descriptor, |data, handle, grab| { + grab.motion(data, handle, focus, event); + }); + } + + /// Notify that a tool start touching the tablet surface. + /// + /// This will internally send the appropriate down event to the client objects matching the + /// currently focused target. + pub fn down(&self, data: &mut D, event: &DownEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + if !inner.in_proximity() { + tracing::warn!("down was called, but the tool hasn't entered tablet proximity."); + return; + } + + let seat = self.get_seat(data); + + inner.with_grab(data, &seat, &self.arc.descriptor, |data, handle, grab| { + grab.down(data, handle, event); + }); + } + + /// Notify that a tool sopped touching the tablet surface. + /// + /// This will internally send the appropriate up event to the client objects matching the + /// currently focused target. + pub fn up(&self, data: &mut D, event: &UpEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + if !inner.in_proximity() { + tracing::warn!("up was called, but the tool hasn't entered tablet proximity."); + return; + } + + let seat = self.get_seat(data); + + inner.with_grab(data, &seat, &self.arc.descriptor, |data, handle, grab| { + grab.up(data, handle, event); + }); + } + + /// Notify that a button state changed. + /// + /// This will internally send the appropriate button event to the client objects matching with + /// the currently focused target. + pub fn button(&self, data: &mut D, event: &ButtonEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + if !inner.in_proximity() { + tracing::warn!("button was called, but the tool hasn't entered tablet proximity."); + return; + } + + let seat = self.get_seat(data); + + if matches!(event.state, ButtonState::Pressed) { + inner.current_buttons.push(event.button); + } else { + inner.current_buttons.retain(|b| *b != event.button); + } + + inner.with_grab(data, &seat, &self.arc.descriptor, |data, handle, grab| { + grab.button(data, handle, event); + }); + } + + /// Start an axis frame + /// + /// A single frame will group multiple axis events as if they happened in the same instance. + pub fn axis(&self, data: &mut D, frame: AxisFrame) { + let mut inner = self.arc.inner.lock().unwrap(); + if !inner.in_proximity() { + tracing::warn!("axis was called, but the tool hasn't entered tablet proximity."); + return; + } + + let seat = self.get_seat(data); + + inner.with_grab(data, &seat, &self.arc.descriptor, |data, handle, grab| { + grab.axis(data, handle, frame); + }); + } + + /// End of a tool frame. + /// + /// A frame groups associated events. This terminates the frame. + pub fn frame(&self, data: &mut D, time: InputTime) { + let mut inner = self.arc.inner.lock().unwrap(); + if !inner.in_proximity() && !inner.frame_pending { + tracing::warn!("frame was called, but the tool hasn't entered tablet proximity."); + return; + } + + // If we don't have a pending frame, skip. + if !inner.frame_pending { + return; + } + + let seat = self.get_seat(data); + + inner.with_grab(data, &seat, &self.arc.descriptor, |data, handle, grab| { + grab.frame(data, handle, time); + }); + } + + /// Access the current location of this tablet tool in the global space + /// + /// This function will return None if the tool isn't above any tablet. + pub fn current_location(&self) -> Option> { + self.arc.inner.lock().unwrap().location + } + + /// Access the current tablet this tool is hovering over. + /// + /// This function will return None if the tool isn't above any tablet. + pub fn current_tablet(&self) -> Option { + self.arc.inner.lock().unwrap().tablet.clone() + } + + /// Return the tablet tool descriptor. + pub fn descriptor(&self) -> &TabletToolDescriptor { + &self.arc.descriptor + } + + fn get_seat(&self, data: &mut D) -> Seat { + let seat_state = data.seat_state(); + seat_state + .seats + .iter() + .find(|seat| { + let Some(tablet_seat) = seat.user_data().get::>() else { + return false; + }; + + tablet_seat.get_tool(&self.arc.descriptor).as_ref() == Some(self) + }) + .cloned() + .unwrap() + } +} + +/// Weak variant of a [`TabletToolHandle`] +/// +/// Does not keep associated data alive, and can be used to refer to a potentially already destroyed +/// tablet tool. +#[derive(Debug)] +pub struct WeakTabletToolHandle(Weak>); + +impl Clone for WeakTabletToolHandle { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl WeakTabletToolHandle { + /// Try to retrieve the original `TabletToolHandle` if it still exists + pub fn upgrade(&self) -> Option> { + self.0.upgrade().map(|arc| TabletToolHandle { arc }) + } + + /// Check if this tablet tool is still alive + pub fn is_alive(&self) -> bool { + self.0.strong_count() != 0 + } +} + +impl TabletToolHandle { + /// Create a weak reference to this tablet tool + pub fn downgrade(&self) -> WeakTabletToolHandle { + WeakTabletToolHandle(Arc::downgrade(&self.arc)) + } +} + +/// Trait representing object that can receive tablet tool interactions +pub trait TabletToolTarget: IsAlive + fmt::Debug + Send +where + D: TabletSeatHandler + SeatHandler, +{ + /// A tool has entered proximity above this target. + /// + /// This event can be received when the tool has moved from one target to another, or when the + /// tool has come back into proximity above the target. + fn proximity_in( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &TabletToolDescriptor, + tablet: &Tablet, + serial: Serial, + ); + + /// A tool has left this target. + fn proximity_out(&self, seat: &Seat, data: &mut D, tool_descriptor: &TabletToolDescriptor); + + /// The tool is in contact with the surface of the tablet. + fn down(&self, seat: &Seat, data: &mut D, tool_descriptor: &TabletToolDescriptor, event: &DownEvent); + /// The tool is no longer in contact with the surface of the tablet. + fn up(&self, seat: &Seat, data: &mut D, tool_descriptor: &TabletToolDescriptor, event: &UpEvent); + + /// A tool has moved over this target. + fn motion( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &TabletToolDescriptor, + event: &MotionEvent, + ); + + /// One or more of the tool axis has changed. + fn axis(&self, seat: &Seat, data: &mut D, tool_descriptor: &TabletToolDescriptor, frame: AxisFrame); + + /// One of the tool button has changed state. + fn button( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &TabletToolDescriptor, + event: &ButtonEvent, + ); + + /// End of a tablet tool frame. + fn frame(&self, seat: &Seat, data: &mut D, tool_descriptor: &TabletToolDescriptor, time: InputTime); +} + +/// This inner handle is accessed from inside a tablet tool grab logic, and directly sends event to +/// the client. +pub struct TabletToolInnerHandle<'a, D: TabletSeatHandler> { + inner: &'a mut TabletToolInternal, + descriptor: &'a TabletToolDescriptor, + seat: &'a Seat, +} + +impl fmt::Debug for TabletToolInnerHandle<'_, D> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TabletToolInnerHandle") + .field("inner", &self.inner) + .field("descriptor", &self.descriptor) + .field("seat", &self.seat) + .finish() + } +} + +impl TabletToolInnerHandle<'_, D> { + /// Change the current grab on this tool to the provided grab + /// + /// Overwrites any current grab. + pub fn set_grab + 'static>( + &mut self, + handler: &mut dyn TabletToolGrab, + data: &mut D, + time: InputTime, + serial: Serial, + focus: Focus, + grab: G, + ) { + handler.unset(data); + self.inner + .set_grab(data, self.seat, self.descriptor, grab, time, serial, focus); + } + + /// Remove any current grab on this tool, resetting it to the default behavior + /// + /// This will also restore the focus of the tool if `restore_focus` is true. + pub fn unset_grab( + &mut self, + handler: &mut dyn TabletToolGrab, + data: &mut D, + serial: Serial, + time: InputTime, + restore_focus: bool, + ) { + handler.unset(data); + self.inner + .unset_grab(data, self.seat, self.descriptor, serial, time, restore_focus); + } + + /// Access the current focus of this pointer + pub fn current_focus(&self) -> Option<(::ToolFocus, Point)> { + self.inner.focus.clone() + } + + /// Access the current location of this tablet tool in the global space + pub fn current_location(&self) -> Point { + self.inner.location.unwrap() + } + + /// Access the current tablet this tool is hovering over. + pub fn current_tablet(&self) -> Tablet { + self.inner.tablet.clone().unwrap() + } + + /// A list of the currently physically pressed buttons + /// + /// This still includes buttons that your grab have intercepted and not sent to the target. + pub fn current_pressed(&self) -> &[u32] { + &self.inner.current_buttons + } + + /// Notify that a tool entered proximity with a tablet. + /// + /// You provide the location of the tool in the form of: + /// - The coordinates of the tool in the global compositor space + /// - The surface on top of which the tool is, and the coordinates of its origin in the global + /// compositor space (or `None` if the tool is not over a client surface). + /// + /// This will internally generate the following event to the client object, if any: + /// - ProximityIn + /// - Axis, if an axis frame is available + /// - Motion + pub fn proximity_in( + &mut self, + data: &mut D, + focus: Option<(::ToolFocus, Point)>, + event: &ProximityInEvent, + ) { + self.inner + .proximity_in(data, self.seat, self.descriptor, focus, event); + } + + /// Notify that a tool left physical proximity with a tablet. + /// + /// This will internally generate the following events: + /// - Button release, for any pressed button + /// - Up, if the tool tip was down + /// - ProximityOut + pub fn proximity_out(&mut self, data: &mut D, event: &ProximityOutEvent) { + self.inner.proximity_out(data, self.seat, self.descriptor, event); + } + + /// Notify that a tool moved above a tablet. + /// + /// You provide the new location of the tool, in the form of: + /// - The coordinates of the tool in the global compositor space + /// - The target on top of which the tool is, and the coordinates of its origin in the global + /// compositor space (or `None` if the tool is not above a client's target.). + /// + /// This will internally take care of notifying the appropriate client objects of + /// proximity_in/motion/proximity_out events. + pub fn motion( + &mut self, + data: &mut D, + focus: Option<(::ToolFocus, Point)>, + event: &MotionEvent, + ) { + self.inner.motion(data, self.seat, self.descriptor, focus, event); + } + + /// Notify that a tool start touching the tablet surface. + /// + /// This will internally send the appropriate down event to the client objects matching the + /// currently focused target. + pub fn down(&mut self, data: &mut D, event: &DownEvent) { + self.inner.down(data, self.seat, self.descriptor, event); + } + + /// Notify that a tool sopped touching the tablet surface. + /// + /// This will internally send the appropriate up event to the client objects matching the + /// currently focused target. + pub fn up(&mut self, data: &mut D, event: &UpEvent) { + self.inner.up(data, self.seat, self.descriptor, event); + } + + /// Notify that a button state changed. + /// + /// This will internally send the appropriate button event to the client objects matching with + /// the currently focused target. + pub fn button(&mut self, data: &mut D, event: &ButtonEvent) { + self.inner.button(data, self.seat, self.descriptor, event); + } + + /// Start an axis frame + /// + /// A single frame will group multiple axis events as if they happened in the same instance. + pub fn axis(&mut self, data: &mut D, frame: AxisFrame) { + self.inner.axis(data, self.seat, self.descriptor, frame); + } + + /// End of a tool frame. + /// + /// A frame groups associated events. This terminates the frame. + pub fn frame(&mut self, data: &mut D, time: InputTime) { + self.inner.frame(data, self.seat, self.descriptor, time); + } + + /// Return the tablet tool descriptor. + pub fn descriptor(&self) -> &TabletToolDescriptor { + self.descriptor + } +} + +pub(crate) struct TabletToolInternal { + pub(crate) focus: Option<(::ToolFocus, Point)>, + pending_focus: Option<(::ToolFocus, Point)>, + previous_focus: Option<::ToolFocus>, + + location: Option>, + pub(crate) tablet: Option, + current_buttons: Vec, + pressed_buttons: Vec, + tip_down: bool, + frame_pending: bool, + + default_grab: Box Box> + Send + 'static>, + grab: GrabStatus>, +} + +impl fmt::Debug for TabletToolInternal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TabletToolHandle") + .field("focus", &self.focus) + .field("grab", &self.grab) + .finish() + } +} + +impl TabletToolInternal { + pub(crate) fn new(default_grab: F) -> Self + where + F: Fn() -> Box> + Send + 'static, + { + Self { + focus: Default::default(), + pending_focus: Default::default(), + previous_focus: Default::default(), + + location: None, + tablet: None, + current_buttons: Default::default(), + pressed_buttons: Default::default(), + tip_down: false, + frame_pending: false, + + default_grab: Box::new(default_grab), + grab: GrabStatus::None, + } + } + + #[allow(clippy::too_many_arguments)] + fn set_grab + 'static>( + &mut self, + data: &mut D, + seat: &Seat, + descriptor: &TabletToolDescriptor, + grab: G, + time: InputTime, + serial: Serial, + focus: Focus, + ) { + if let GrabStatus::Active(_, handler) = &mut self.grab { + handler.unset(data) + } + + if self.tablet.is_none() || self.location.is_none() { + tracing::warn!("set_grab called with an out of proximity tool."); + return; + } + + self.grab = GrabStatus::Active(serial, Box::new(grab)); + + if matches!(focus, Focus::Clear) { + let location = self.location.unwrap(); + + self.motion( + data, + seat, + descriptor, + None, + &MotionEvent { + location, + serial, + time, + }, + ); + } + } + + fn unset_grab( + &mut self, + data: &mut D, + seat: &Seat, + descriptor: &TabletToolDescriptor, + serial: Serial, + time: InputTime, + restore_focus: bool, + ) { + if let GrabStatus::Active(_, handler) = &mut self.grab { + handler.unset(data) + } + self.grab = GrabStatus::None; + + if let Some(location) = self.location { + if restore_focus { + let focus = self.pending_focus.clone(); + + self.motion( + data, + seat, + descriptor, + focus, + &MotionEvent { + location, + serial, + time, + }, + ); + } + } + } + + fn with_grab(&mut self, data: &mut D, seat: &Seat, descriptor: &TabletToolDescriptor, f: F) + where + F: FnOnce(&mut D, &mut TabletToolInnerHandle<'_, D>, &mut dyn TabletToolGrab), + { + let mut grab = std::mem::replace(&mut self.grab, GrabStatus::Borrowed); + match grab { + GrabStatus::Borrowed => { + panic!("Accessed a tablet tool grab from within a tablet tool grab access.") + } + GrabStatus::Active(_, ref mut handler) => { + if let Some((ref focus, _)) = handler.start_data().focus { + if !focus.alive() { + handler.unset(data); + self.grab = GrabStatus::None; + let mut default_grab = (self.default_grab)(); + f( + data, + &mut TabletToolInnerHandle { + inner: self, + descriptor, + seat, + }, + &mut *default_grab, + ); + return; + } + } + + f( + data, + &mut TabletToolInnerHandle { + inner: self, + descriptor, + seat, + }, + &mut **handler, + ); + } + GrabStatus::None => { + let mut default_grab = (self.default_grab)(); + f( + data, + &mut TabletToolInnerHandle { + inner: self, + descriptor, + seat, + }, + &mut *default_grab, + ); + } + } + + if let GrabStatus::Borrowed = self.grab { + self.grab = grab; + } + } + + fn in_proximity(&self) -> bool { + self.tablet.is_some() && self.location.is_some() + } + + fn proximity_in( + &mut self, + data: &mut D, + seat: &Seat, + descriptor: &TabletToolDescriptor, + focus: Option<(::ToolFocus, Point)>, + event: &ProximityInEvent, + ) { + self.focus = focus; + if let Some((focused, loc)) = self.focus.as_mut() { + let location = event.location - *loc; + let serial = event.serial; + let time = event.time; + + focused.proximity_in(seat, data, descriptor, self.tablet.as_ref().unwrap(), serial); + + if let Some(axis) = event.axis.clone() { + focused.axis(seat, data, descriptor, axis); + } + + focused.motion( + seat, + data, + descriptor, + &MotionEvent { + location, + serial, + time, + }, + ); + + if self.tip_down { + focused.down(seat, data, descriptor, &DownEvent { serial, time }); + } + + for button in self.pressed_buttons.iter() { + focused.button( + seat, + data, + descriptor, + &ButtonEvent { + serial: event.serial, + button: *button, + state: ButtonState::Pressed, + time: event.time, + }, + ); + } + } + + self.frame_pending = true; + } + + fn proximity_out( + &mut self, + data: &mut D, + seat: &Seat, + descriptor: &TabletToolDescriptor, + event: &ProximityOutEvent, + ) { + if let Some((focused, _)) = self.focus.take() { + let time = event.time; + + for button in self.pressed_buttons.iter() { + focused.button( + seat, + data, + descriptor, + &ButtonEvent { + serial: event.serial, + button: *button, + state: ButtonState::Released, + time, + }, + ); + } + + if self.tip_down { + // Compat with other compositors, more on this in https://github.com/Smithay/smithay/pull/1851 + // tldr: Spec-wise there is no reason to send this, but some clients depend on this + focused.axis( + seat, + data, + descriptor, + AxisFrame { + pressure: Some(0.0), + ..Default::default() + }, + ); + focused.up( + seat, + data, + descriptor, + &UpEvent { + serial: event.serial, + time, + }, + ); + } + + focused.proximity_out(seat, data, descriptor); + + // This should not happen, but in case a previous proximity_out wasn't acknowledged by a + // frame, we need to send one now. + if let Some(old_focus) = self.previous_focus.take_if(|f| f != &focused) { + old_focus.frame(seat, data, descriptor, time); + } + + self.previous_focus = Some(focused); + } + + self.frame_pending = true; + } + + fn motion( + &mut self, + data: &mut D, + seat: &Seat, + descriptor: &TabletToolDescriptor, + focus: Option<(::ToolFocus, Point)>, + event: &MotionEvent, + ) { + self.location = Some(event.location); + + let Some((focused, loc)) = focus else { + self.proximity_out( + data, + seat, + descriptor, + &ProximityOutEvent { + serial: event.serial, + time: event.time, + }, + ); + return; + }; + + if self + .focus + .as_ref() + .is_some_and(|(old_focused, _)| &focused == old_focused) + { + let event = MotionEvent { + location: event.location - loc, + serial: event.serial, + time: event.time, + }; + // We were on top of a target and remained on it. + focused.motion(seat, data, descriptor, &event); + } else { + self.proximity_out( + data, + seat, + descriptor, + &ProximityOutEvent { + serial: event.serial, + time: event.time, + }, + ); + self.proximity_in( + data, + seat, + descriptor, + Some((focused, loc)), + &ProximityInEvent { + location: event.location, + axis: None, + serial: event.serial, + time: event.time, + }, + ); + } + + self.frame_pending = true; + } + + fn down(&mut self, data: &mut D, seat: &Seat, descriptor: &TabletToolDescriptor, event: &DownEvent) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.down(seat, data, descriptor, event); + } + + self.tip_down = true; + self.frame_pending = true; + } + + fn up(&mut self, data: &mut D, seat: &Seat, descriptor: &TabletToolDescriptor, event: &UpEvent) { + if self.tip_down { + if let Some((focused, _)) = self.focus.as_mut() { + // Compat with other compositors, more on this in https://github.com/Smithay/smithay/pull/1851 + // tldr: Spec-wise there is no reason to send this, but some clients depend on this + focused.axis( + seat, + data, + descriptor, + AxisFrame { + pressure: Some(0.0), + ..Default::default() + }, + ); + focused.up(seat, data, descriptor, event); + } + } + + self.tip_down = false; + self.frame_pending = true; + } + + fn button( + &mut self, + data: &mut D, + seat: &Seat, + descriptor: &TabletToolDescriptor, + event: &ButtonEvent, + ) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.button(seat, data, descriptor, event); + } + + if matches!(event.state, ButtonState::Pressed) { + self.pressed_buttons.push(event.button); + } else { + self.pressed_buttons.retain(|b| b != &event.button); + } + + self.frame_pending = true; + } + + fn axis(&mut self, data: &mut D, seat: &Seat, descriptor: &TabletToolDescriptor, frame: AxisFrame) { + if let Some((focused, _)) = self.focus.as_mut() { + focused.axis(seat, data, descriptor, frame); + } + + self.frame_pending = true; + } + + fn frame(&mut self, data: &mut D, seat: &Seat, descriptor: &TabletToolDescriptor, time: InputTime) { + if self.frame_pending { + if let Some((focused, _)) = self.focus.as_mut() { + focused.frame(seat, data, descriptor, time); + + // We don't want to send the frame twice. + self.previous_focus.take_if(|f| f == focused); + } + + if let Some(focused) = self.previous_focus.take() { + focused.frame(seat, data, descriptor, time); + } + } + + self.frame_pending = false; + } + + fn proximity_out_impl( + &mut self, + data: &mut D, + seat: &Seat, + descriptor: &TabletToolDescriptor, + event: &ProximityOutEvent, + ) { + self.with_grab(data, seat, descriptor, |data, handle, grab| { + grab.proximity_out(data, handle, event); + }); + + // Now, reset everything so we don't get stale data later. + self.tip_down = false; + self.pressed_buttons.clear(); + self.current_buttons.clear(); + self.focus = None; + self.pending_focus = None; + // We don't reset last_focus just yet as it's needed for the frame event. + self.tablet = None; + self.location = None; + } +} + +/// Proximity in event. +#[derive(Debug, Clone)] +pub struct ProximityInEvent { + /// Location of the tool in compositor space + pub location: Point, + /// Axis frame to send along with the proximity in + pub axis: Option, + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Proximity out event. +#[derive(Debug, Clone)] +pub struct ProximityOutEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Tablet tool motion event +#[derive(Debug, Clone)] +pub struct MotionEvent { + /// Location of the tool in compositor space + pub location: Point, + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Tablet tool button event +/// +/// Tool button click and release event. The location of the click is given by the last motion or +/// proximity in event. +#[derive(Debug, Clone)] +pub struct ButtonEvent { + /// Serial of the event + pub serial: Serial, + /// Button that produced the event + pub button: u32, + /// Physical state of the button + pub state: ButtonState, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Tip down event. +/// +/// The location of the tool is given by the last motion or proximity in event. +#[derive(Debug, Clone)] +pub struct DownEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Tip up event. +/// +/// The location of the tool is given by the last motion or proximity in event. +#[derive(Debug, Clone)] +pub struct UpEvent { + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// A frame of tablet tool axis events. +/// Frames of axis events should be considered as one logical action. +/// +/// Can be used with the builder pattern, e.g.: +/// +/// ```ignore +/// AxisFrame::new() +/// .pressure(0.5) +/// .distance(0.0) +/// .tilt(15.0, 25.0); +/// ``` +#[must_use = "AxisFrame uses a builder-like pattern, so its result must be used"] +#[derive(Clone, Debug, Default)] +pub struct AxisFrame { + /// Changes in the pressure axis of the tool + pub pressure: Option, + /// Changes in the distance axis + pub distance: Option, + /// Changes in the tool tilt + pub tilt: Option<(f64, f64)>, + /// Changes in the tool's z-rotation + pub rotation: Option, + /// Changes in the tool's slider position + pub slider: Option, + /// Changes in the tool's wheel position + pub wheel: Option<(f64, i32)>, +} + +impl AxisFrame { + /// Create a new frame of axis events + pub fn new() -> Self { + Default::default() + } + + /// Value of the pressure event. + pub fn pressure(self, pressure: f64) -> Self { + Self { + pressure: Some(pressure), + ..self + } + } + + /// Value of the distance event. + pub fn distance(self, distance: f64) -> Self { + Self { + distance: Some(distance), + ..self + } + } + + /// Value of the tool tilt + pub fn tilt(self, tilt_x: f64, tilt_y: f64) -> Self { + Self { + tilt: Some((tilt_x, tilt_y)), + ..self + } + } + + /// Value in the tool's z-rotation + pub fn rotation(self, rotation: f64) -> Self { + Self { + rotation: Some(rotation), + ..self + } + } + + /// Value of the tool's slider position + pub fn slider(self, slider: f64) -> Self { + Self { + slider: Some(slider), + ..self + } + } + + /// Value of the tool's wheel + pub fn wheel(self, degrees: f64, clicks: i32) -> Self { + Self { + wheel: Some((degrees, clicks)), + ..self + } + } +} diff --git a/vendor/smithay/src/input/touch/grab.rs b/vendor/smithay/src/input/touch/grab.rs new file mode 100644 index 0000000..f7ab7cf --- /dev/null +++ b/vendor/smithay/src/input/touch/grab.rs @@ -0,0 +1,281 @@ +use std::fmt; + +use downcast_rs::{Downcast, impl_downcast}; + +use crate::{ + backend::input::TouchSlot, + input::SeatHandler, + utils::{Logical, Point}, +}; + +use super::{DownEvent, MotionEvent, OrientationEvent, ShapeEvent, TouchInnerHandle, UpEvent}; + +/// A trait to implement a touch grab +/// +/// In some context, it is necessary to temporarily change the behavior of the touch handler. This is +/// typically known as a touch grab. A typical example would be, during a drag'n'drop operation, +/// the underlying surfaces will no longer receive classic pointer event, but rather special events. +/// +/// This trait is the interface to intercept regular touch events and change them as needed, its +/// interface mimics the [`TouchHandle`](super::TouchHandle) interface. +/// +/// Any interactions with [`TouchHandle`](super::TouchHandle) +/// should be done using [`TouchInnerHandle`], as handle is borrowed/locked before grab methods are called, +/// so calling methods on [`TouchHandle`](super::TouchHandle) would result in a deadlock. +/// +/// If your logic decides that the grab should end, both [`TouchInnerHandle`] +/// and [`TouchHandle`](super::TouchHandle) have +/// a method to change it. +/// +/// When your grab ends (either as you requested it or if it was forcefully cancelled by the server), +/// the struct implementing this trait will be dropped. As such you should put clean-up logic in the destructor, +/// rather than trying to guess when the grab will end. +pub trait TouchGrab: Send + Downcast { + /// A new touch point appeared + /// + /// This method allows you attach additional behavior to a down event, possibly altering it. + /// You generally will want to invoke [`TouchInnerHandle::down`] as part of your processing. If you + /// don't, the rest of the compositor will behave as if the down event never occurred. + /// + /// Some grabs (such as drag'n'drop, shell resize and motion) drop down events while they are active, + /// the default touch grab will keep the focus on the surface that started the grab. + fn down( + &mut self, + data: &mut D, + handle: &mut TouchInnerHandle<'_, D>, + focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + ); + /// A touch point disappeared + /// + /// This method allows you attach additional behavior to a up event, possibly altering it. + /// You generally will want to invoke [`TouchInnerHandle::up`] as part of your processing. + /// If you don't, the rest of the compositor will behave as if the up event never occurred. + /// + /// Some grabs (such as drag'n'drop, shell resize and motion) drop up events while they are active, + /// but will end when the touch point that initiated the grab disappeared. + fn up(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &UpEvent); + + /// A touch point has changed coordinates. + /// + /// This method allows you attach additional behavior to a motion event, possibly altering it. + /// You generally will want to invoke [`TouchInnerHandle::motion`] as part of your processing. + /// If you don't, the rest of the compositor will behave as if the motion event never occurred. + /// + /// **Note** that this is **not** intended to update the focus of the touch point, the focus + /// is only set on a down event. The focus provided to this function can be used to find DnD + /// targets during touch motion. + fn motion( + &mut self, + data: &mut D, + handle: &mut TouchInnerHandle<'_, D>, + focus: Option<(::TouchFocus, Point)>, + event: &MotionEvent, + ); + + /// Marks the end of a set of events that logically belong together. + /// + /// This method allows you attach additional behavior to a frame event, possibly altering it. + /// You generally will want to invoke [`TouchInnerHandle::frame`] as part of your processing. + /// If you don't, the rest of the compositor will behave as if the frame event never occurred. + /// + /// This will to be called after one or more calls to down/motion events. + fn frame(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>); + + /// A touch session has been cancelled. + /// + /// This method allows you attach additional behavior to a cancel event, possibly altering it. + /// You generally will want to invoke [`TouchInnerHandle::cancel`] as part of your processing. + /// If you don't, the rest of the compositor will behave as if the cancel event never occurred. + /// + /// Usually called in case the compositor decides the touch stream is a global gesture. + fn cancel(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>); + + /// A touch point has changed its shape. + fn shape(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &ShapeEvent); + + /// A touch point has changed its orientation. + fn orientation(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &OrientationEvent); + + /// The data about the event that started the grab. + fn start_data(&self) -> &GrabStartData; + + /// The grab has been unset or replaced with another grab. + fn unset(&mut self, data: &mut D); +} + +impl_downcast!(TouchGrab where D: SeatHandler); + +/// Data about the event that started the grab. +pub struct GrabStartData { + /// The focused surface and its location, if any, at the start of the grab. + /// + /// The location coordinates are in the global compositor space. + pub focus: Option<(::TouchFocus, Point)>, + /// The touch point that initiated the grab. + pub slot: TouchSlot, + /// The location of the down event that initiated the grab, in the global compositor space. + pub location: Point, +} + +impl fmt::Debug for GrabStartData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GrabStartData") + .field("focus", &self.focus.as_ref().map(|_| "...")) + .field("slot", &self.slot) + .field("location", &self.location) + .finish() + } +} + +impl Clone for GrabStartData { + fn clone(&self) -> Self { + GrabStartData { + focus: self.focus.clone(), + slot: self.slot, + location: self.location, + } + } +} + +/// The default grab, the behavior when no particular grab is in progress +#[derive(Debug)] +pub struct DefaultGrab; + +impl TouchGrab for DefaultGrab { + fn down( + &mut self, + data: &mut D, + handle: &mut TouchInnerHandle<'_, D>, + focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + ) { + handle.down(data, focus.clone(), event); + let grab = data.touch_down_grab(GrabStartData { + focus, + slot: event.slot, + location: event.location, + }); + handle.set_grab(self, data, event.serial, grab); + } + + fn up(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &UpEvent) { + handle.up(data, event) + } + + fn motion( + &mut self, + data: &mut D, + handle: &mut TouchInnerHandle<'_, D>, + focus: Option<(::TouchFocus, Point)>, + event: &MotionEvent, + ) { + handle.motion(data, focus, event) + } + + fn frame(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>) { + handle.frame(data) + } + + fn cancel(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>) { + handle.cancel(data) + } + + fn shape(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &ShapeEvent) { + handle.shape(data, event) + } + + fn orientation(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &OrientationEvent) { + handle.orientation(data, event) + } + + fn start_data(&self) -> &GrabStartData { + unreachable!() + } + + fn unset(&mut self, _data: &mut D) {} +} + +/// A touch down grab, basic grab started when an user touches a surface +/// to maintain it focused until the user releases the touch. +/// +/// In case the user maintains several simultaneous touch points, release +/// the grab once all are released. +pub struct TouchDownGrab { + /// Start date for this grab + pub start_data: GrabStartData, + /// Currently active touch points + pub touch_points: usize, +} + +impl TouchDownGrab { + pub(in crate::input) fn new(start_data: GrabStartData) -> Self { + Self { + start_data, + touch_points: 1, + } + } +} + +impl fmt::Debug for TouchDownGrab { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TouchDownGrab") + .field("start_data", &self.start_data) + .field("touch_points", &self.touch_points) + .finish() + } +} + +impl TouchGrab for TouchDownGrab { + fn down( + &mut self, + data: &mut D, + handle: &mut TouchInnerHandle<'_, D>, + _focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + ) { + handle.down(data, self.start_data.focus.clone(), event); + self.touch_points += 1; + } + + fn up(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &UpEvent) { + handle.up(data, event); + self.touch_points = self.touch_points.saturating_sub(1); + if self.touch_points == 0 { + handle.unset_grab(self, data); + } + } + + fn motion( + &mut self, + data: &mut D, + handle: &mut TouchInnerHandle<'_, D>, + _focus: Option<(::TouchFocus, Point)>, + event: &MotionEvent, + ) { + handle.motion(data, self.start_data.focus.clone(), event) + } + + fn frame(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>) { + handle.frame(data) + } + + fn cancel(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>) { + handle.cancel(data); + handle.unset_grab(self, data); + } + + fn shape(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &ShapeEvent) { + handle.shape(data, event) + } + + fn orientation(&mut self, data: &mut D, handle: &mut TouchInnerHandle<'_, D>, event: &OrientationEvent) { + handle.orientation(data, event) + } + + fn start_data(&self) -> &GrabStartData { + &self.start_data + } + + fn unset(&mut self, _data: &mut D) {} +} diff --git a/vendor/smithay/src/input/touch/mod.rs b/vendor/smithay/src/input/touch/mod.rs new file mode 100644 index 0000000..b5d040a --- /dev/null +++ b/vendor/smithay/src/input/touch/mod.rs @@ -0,0 +1,763 @@ +//! Touch-related types for smithay's input abstraction + +use std::collections::HashMap; +use std::fmt; +use std::sync::{Arc, Mutex}; + +use tracing::{info_span, instrument}; +#[cfg(feature = "wayland_frontend")] +use wayland_server::Weak; + +use crate::backend::input::{InputTime, TouchSlot}; +use crate::utils::{IsAlive, Logical, Point, Serial}; + +pub use grab::{DefaultGrab, GrabStartData, TouchDownGrab, TouchGrab}; + +use super::{GrabStatus, Seat, SeatHandler}; + +mod grab; + +crate::utils::ids::id_gen!(frame_marker); + +/// A marker to identify a given touch frame. +/// +/// This marker is sent to [`TouchTarget`] during `frame` and `cancel`, and can be returned via the +/// [`TouchTarget::last_frame`] function. They are used internally by [`TouchHandle`] to avoid +/// sending `frame` and `cancel` event more than once. +/// +/// FrameMarker are used instead of comparing [`TouchTarget`] as different [`TouchTarget`] could +/// refer to the same underlying object (e.g. multiple surfaces belonging to the same wayland +/// `Client`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FrameMarker(usize); + +/// An handle to a touch handler +/// +/// It can be cloned and all clones manipulate the same internal state. +/// +/// This handle gives you access to an interface to send touch events to your +/// clients. +/// +/// When sending events using this handle, they will be intercepted by a touch +/// grab if any is active. See the [`TouchGrab`] trait for details. +pub struct TouchHandle { + pub(crate) arc: Arc>, +} + +pub(crate) struct TouchRc { + pub(crate) inner: Mutex>, + #[cfg(feature = "wayland_frontend")] + pub(crate) known_instances: Mutex>>, + #[cfg(feature = "wayland_frontend")] + pub(crate) last_down: Mutex>, + pub(crate) span: tracing::Span, +} + +#[cfg(not(feature = "wayland_frontend"))] +impl fmt::Debug for TouchHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TouchHandle") + .field("inner", &self.arc.inner) + .finish() + } +} + +#[cfg(feature = "wayland_frontend")] +impl fmt::Debug for TouchHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TouchHandle") + .field("inner", &self.arc.inner) + .field("known_instances", &self.arc.known_instances) + .field("last_down", &self.arc.last_down) + .finish() + } +} + +impl Clone for TouchHandle { + #[inline] + fn clone(&self) -> Self { + Self { + arc: self.arc.clone(), + } + } +} + +impl std::hash::Hash for TouchHandle { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.arc).hash(state) + } +} + +impl std::cmp::PartialEq for TouchHandle { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.arc, &other.arc) + } +} + +impl std::cmp::Eq for TouchHandle {} + +pub(crate) struct TouchInternal { + pub(crate) focus: HashMap>, + pending_frame: Option, + default_grab: Box Box> + Send + 'static>, + grab: GrabStatus>, +} + +impl Drop for TouchInternal { + fn drop(&mut self) { + if let Some(marker) = self.pending_frame.take() { + frame_marker::remove(marker.0); + } + } +} + +pub(crate) struct TouchSlotState { + pub(crate) focus: Option<(::TouchFocus, Point)>, + frame_pending: Option<::TouchFocus>, + pub(crate) location: Point, + pending: FrameMarker, + current: Option, +} + +impl fmt::Debug for TouchSlotState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TouchSlotState") + .field("focus", &self.focus) + .field("frame_pending", &self.frame_pending) + .field("location", &self.location) + .field("pending", &self.pending) + .field("current", &self.current) + .finish() + } +} + +// image_callback does not implement debug, so we have to impl Debug manually +impl fmt::Debug for TouchInternal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TouchInternal") + .field("focus", &self.focus) + .field("grab", &self.grab) + .finish() + } +} + +/// Pointer motion event +#[derive(Debug, Clone)] +pub struct DownEvent { + /// Slot of this event + pub slot: TouchSlot, + /// Location of the touch in compositor space + pub location: Point, + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Pointer motion event +#[derive(Debug, Clone)] +pub struct UpEvent { + /// Slot of this event + pub slot: TouchSlot, + /// Serial of the event + pub serial: Serial, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Pointer motion event +#[derive(Debug, Clone)] +pub struct MotionEvent { + /// Slot of this event + pub slot: TouchSlot, + /// Location of the touch in compositor space + pub location: Point, + /// Timestamp of the event, with microsecond granularity + pub time: InputTime, +} + +/// Pointer motion event +#[derive(Debug, Clone, Copy)] +pub struct ShapeEvent { + /// Slot of this event + pub slot: TouchSlot, + /// Length of the major axis in surface-local coordinates + pub major: f64, + /// Length of the minor axis in surface-local coordinates + pub minor: f64, +} + +/// Pointer motion event +#[derive(Debug, Clone, Copy)] +pub struct OrientationEvent { + /// Slot of this event + pub slot: TouchSlot, + /// Angle between major axis and positive surface y-axis in degrees + pub orientation: f64, +} + +/// Trait representing object that can receive touch interactions +pub trait TouchTarget: IsAlive + fmt::Debug + Send +where + D: SeatHandler, +{ + /// A new touch point has appeared on the target. + /// + /// This touch point is assigned a unique ID. Future events from this touch point reference this ID. + /// The ID ceases to be valid after a touch up event and may be reused in the future. + fn down(&self, seat: &Seat, data: &mut D, event: &DownEvent); + + /// The touch point has disappeared. + /// + /// No further events will be sent for this touch point and the touch point's ID + /// is released and may be reused in a future touch down event. + fn up(&self, seat: &Seat, data: &mut D, event: &UpEvent); + + /// A touch point has changed coordinates. + fn motion(&self, seat: &Seat, data: &mut D, event: &MotionEvent); + + /// Indicates the end of a set of events that logically belong together. + /// + /// The `marker` parameter is used to avoid re-sending the cancel event if the [`TouchTarget`] + /// has multiple active touch slot, or if multiple [`TouchTarget`] belong to the same underlying + /// target (e.g. a single client). See [`TouchTarget::last_frame`]. + fn frame(&self, seat: &Seat, data: &mut D, marker: FrameMarker); + + /// Touch session cancelled. + /// + /// Touch cancellation applies to all touch points currently active on this target. + /// The client is responsible for finalizing the touch points, future touch points on + /// this target may reuse the touch point ID. + /// + /// The `marker` parameter is used to avoid re-sending the cancel event if the [`TouchTarget`] + /// has multiple active touch slot, or if multiple [`TouchTarget`] belong to the same underlying + /// target (e.g. a single client). See [`TouchTarget::last_frame`]. + fn cancel(&self, seat: &Seat, data: &mut D, marker: FrameMarker); + + /// Sent when a touch point has changed its shape. + /// + /// A touch point shape is approximated by an ellipse through the major and minor axis length. + /// The major axis length describes the longer diameter of the ellipse, while the minor axis + /// length describes the shorter diameter. Major and minor are orthogonal and both are specified + /// in surface-local coordinates. The center of the ellipse is always at the touch point location + /// as reported by [`TouchTarget::down`] or [`TouchTarget::motion`]. + fn shape(&self, seat: &Seat, data: &mut D, event: &ShapeEvent); + + /// Sent when a touch point has changed its orientation. + /// + /// The orientation describes the clockwise angle of a touch point's major axis to the positive surface + /// y-axis and is normalized to the -180 to +180 degree range. The granularity of orientation depends + /// on the touch device, some devices only support binary rotation values between 0 and 90 degrees. + fn orientation(&self, seat: &Seat, data: &mut D, event: &OrientationEvent); + + /// Returns last know [`FrameMarker`]. + /// + /// If this function returns `Some(marker)`, [`TouchHandle`] will use that value to avoid + /// sending the `frame` or `cancel` event more than once. + fn last_frame(&self, seat: &Seat, data: &mut D) -> Option; +} + +impl TouchHandle { + pub(crate) fn new(default_grab: F) -> TouchHandle + where + F: Fn() -> Box> + Send + 'static, + { + TouchHandle { + arc: Arc::new(TouchRc { + inner: Mutex::new(TouchInternal::new(default_grab)), + #[cfg(feature = "wayland_frontend")] + known_instances: Mutex::new(Vec::new()), + #[cfg(feature = "wayland_frontend")] + last_down: Mutex::new(HashMap::new()), + span: info_span!("input_touch"), + }), + } + } + + /// Change the current grab on this touch to the provided grab + /// + /// Overwrites any current grab. + #[instrument(level = "debug", parent = &self.arc.span, skip(self, data, grab))] + pub fn set_grab + 'static>(&self, data: &mut D, grab: G, serial: Serial) { + let seat = self.get_seat(data); + self.arc.inner.lock().unwrap().set_grab(data, &seat, serial, grab); + } + + /// Remove any current grab on this touch, resetting it to the default behavior + #[instrument(level = "debug", parent = &self.arc.span, skip(self, data))] + pub fn unset_grab(&self, data: &mut D) { + let seat = self.get_seat(data); + self.arc.inner.lock().unwrap().unset_grab(data, &seat); + } + + /// Check if this touch is currently grabbed with this serial + pub fn has_grab(&self, serial: Serial) -> bool { + self.with_grab(|s, _| s == serial).unwrap_or(false) + } + + /// Check if this touch is currently being grabbed + pub fn is_grabbed(&self) -> bool { + let guard = self.arc.inner.lock().unwrap(); + !matches!(guard.grab, GrabStatus::None) + } + + /// Returns the start data for the grab, if any. + pub fn grab_start_data(&self) -> Option> { + self.with_grab(|_, g| g.start_data().clone()) + } + + /// Calls `f` with the active grab, if any. + pub fn with_grab(&self, f: impl FnOnce(Serial, &dyn TouchGrab) -> T) -> Option { + let guard = self.arc.inner.lock().unwrap(); + if let GrabStatus::Active(s, g) = &guard.grab { + Some(f(*s, &**g)) + } else { + None + } + } + + /// Notify that a new touch point appeared + /// + /// You provide the location of the touch, in the form of: + /// + /// - The coordinates of the touch in the global compositor space + /// - The surface on top of which the touch point is, and the coordinates of its + /// origin in the global compositor space (or `None` of the touch is not + /// on top of a client surface). + pub fn down( + &self, + data: &mut D, + focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.down(data, handle, focus, event); + }); + } + + /// Notify that a touch point disappeared + pub fn up(&self, data: &mut D, event: &UpEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.up(data, handle, event); + }); + } + + /// Notify that a touch point has changed coordinates. + /// + /// You provide the location of the touch, in the form of: + /// + /// - The coordinates of the touch in the global compositor space + /// - The surface on top of which the touch point is, and the coordinates of its + /// origin in the global compositor space (or `None` of the touch is not + /// on top of a client surface). + /// + /// **Note** that this will **not** update the focus of the touch point, the focus + /// is only set on [`TouchHandle::down`]. The focus provided to this function + /// can be used to find DnD targets during touch motion. + pub fn motion( + &self, + data: &mut D, + focus: Option<(::TouchFocus, Point)>, + event: &MotionEvent, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.motion(data, handle, focus, event); + }); + } + + /// Notify about the end of a set of events that logically belong together. + /// + /// This needs to be called after one or move calls to [`TouchHandle::down`] or [`TouchHandle::motion`] + pub fn frame(&self, data: &mut D) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.frame(data, handle); + }); + } + + /// Notify that the touch session has been cancelled. + /// + /// Use in case you decide the touch stream is a global gesture. + /// This will remove all current focus targets, and no further events will be sent + /// until a new touch point appears. + pub fn cancel(&self, data: &mut D) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.cancel(data, handle); + }); + } + + /// Notify that a touch point has changed its shape. + pub fn shape(&self, data: &mut D, event: &ShapeEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.shape(data, handle, event); + }); + } + + /// Notify that a touch point has changed its orientation. + pub fn orientation(&self, data: &mut D, event: &OrientationEvent) { + let mut inner = self.arc.inner.lock().unwrap(); + let seat = self.get_seat(data); + inner.with_grab(data, &seat, |data, handle, grab| { + grab.orientation(data, handle, event); + }); + } + + fn get_seat(&self, data: &mut D) -> Seat { + let seat_state = data.seat_state(); + seat_state + .seats + .iter() + .find(|seat| seat.get_touch().map(|h| &h == self).unwrap_or(false)) + .cloned() + .unwrap() + } +} + +/// This inner handle is accessed from inside a pointer grab logic, and directly +/// sends event to the client +pub struct TouchInnerHandle<'a, D: SeatHandler> { + inner: &'a mut TouchInternal, + seat: &'a Seat, +} + +impl fmt::Debug for TouchInnerHandle<'_, D> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TouchInnerHandle") + .field("inner", &self.inner) + .field("seat", &self.seat.arc.name) + .finish() + } +} + +impl TouchInnerHandle<'_, D> { + /// Change the current grab on this pointer to the provided grab + /// + /// Overwrites any current grab. + pub fn set_grab + 'static>( + &mut self, + handler: &mut dyn TouchGrab, + data: &mut D, + serial: Serial, + grab: G, + ) { + handler.unset(data); + self.inner.set_grab(data, self.seat, serial, grab); + } + + /// Remove any current grab on this pointer, resetting it to the default behavior + /// + /// This will also restore the focus of the underlying pointer if restore_focus + /// is [`true`] + pub fn unset_grab(&mut self, handler: &mut dyn TouchGrab, data: &mut D) { + handler.unset(data); + self.inner.unset_grab(data, self.seat); + } + + /// Notify that a new touch point appeared + /// + /// You provide the location of the touch, in the form of: + /// + /// - The coordinates of the touch in the global compositor space + /// - The surface on top of which the touch point is, and the coordinates of its + /// origin in the global compositor space (or `None` of the touch is not + /// on top of a client surface). + pub fn down( + &mut self, + data: &mut D, + focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + ) { + self.inner.down(data, self.seat, focus, event) + } + + /// Notify that a touch point disappeared + pub fn up(&mut self, data: &mut D, event: &UpEvent) { + self.inner.up(data, self.seat, event) + } + + /// Notify that a touch point has changed coordinates. + /// + /// You provide the location of the touch, in the form of: + /// + /// - The coordinates of the touch in the global compositor space + /// - The surface on top of which the touch point is, and the coordinates of its + /// origin in the global compositor space (or `None` of the touch is not + /// on top of a client surface). + /// + /// **Note** that this will **not** update the focus of the touch point, the focus + /// is only set on [`TouchHandle::down`]. The focus provided to this function + /// can be used to find DnD targets during touch motion. + pub fn motion( + &mut self, + data: &mut D, + focus: Option<(::TouchFocus, Point)>, + event: &MotionEvent, + ) { + self.inner.motion(data, self.seat, focus, event) + } + + /// Notify about the end of a set of events that logically belong together. + /// + /// This needs to be called after one or move calls to [`TouchHandle::down`] or [`TouchHandle::motion`] + pub fn frame(&mut self, data: &mut D) { + self.inner.frame(data, self.seat) + } + + /// Notify that a touch point has changed its shape. + pub fn shape(&mut self, data: &mut D, event: &ShapeEvent) { + self.inner.shape(data, self.seat, event) + } + + /// Notify that a touch point has changed its orientation. + pub fn orientation(&mut self, data: &mut D, event: &OrientationEvent) { + self.inner.orientation(data, self.seat, event) + } + + /// Notify that the touch session has been cancelled. + /// + /// Use in case you decide the touch stream is a global gesture. + /// This will remove all current focus targets, and no further events will be sent + /// until a new touch point appears. + pub fn cancel(&mut self, data: &mut D) { + self.inner.cancel(data, self.seat) + } +} + +impl TouchInternal { + fn new(default_grab: F) -> Self + where + F: Fn() -> Box> + Send + 'static, + { + Self { + focus: Default::default(), + pending_frame: None, + default_grab: Box::new(default_grab), + grab: GrabStatus::None, + } + } + + fn set_grab + 'static>( + &mut self, + data: &mut D, + _seat: &Seat, + serial: Serial, + grab: G, + ) { + if let GrabStatus::Active(_, handler) = &mut self.grab { + handler.unset(data); + } + self.grab = GrabStatus::Active(serial, Box::new(grab)); + } + + fn unset_grab(&mut self, data: &mut D, _seat: &Seat) { + if let GrabStatus::Active(_, handler) = &mut self.grab { + handler.unset(data); + } + self.grab = GrabStatus::None; + } + + fn down( + &mut self, + data: &mut D, + seat: &Seat, + focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + ) { + let marker = self.frame_marker(); + self.focus + .entry(event.slot) + .and_modify(|state| { + state.pending = marker; + state.frame_pending = None; + state.location = event.location; + state.focus.clone_from(&focus); + }) + .or_insert_with(|| TouchSlotState { + focus, + frame_pending: None, + location: event.location, + pending: marker, + current: None, + }); + + let state = self.focus.get(&event.slot).unwrap(); + if let Some((focus, loc)) = state.focus.as_ref() { + let mut new_event = event.clone(); + new_event.location -= *loc; + focus.down(seat, data, &new_event); + } + } + + fn up(&mut self, data: &mut D, seat: &Seat, event: &UpEvent) { + let marker = self.frame_marker(); + let Some(state) = self.focus.get_mut(&event.slot) else { + return; + }; + state.pending = marker; + if let Some((focus, _)) = state.focus.take() { + focus.up(seat, data, event); + + // Keep the focus around to be able to send a frame event after up, but move + // it out of the current focus to prevent sending other events. + state.frame_pending = Some(focus); + } + } + + fn motion( + &mut self, + data: &mut D, + seat: &Seat, + _focus: Option<(::TouchFocus, Point)>, + event: &MotionEvent, + ) { + let marker = self.frame_marker(); + let Some(state) = self.focus.get_mut(&event.slot) else { + return; + }; + state.pending = marker; + state.location = event.location; + if let Some((focus, loc)) = state.focus.as_ref() { + let mut new_event = event.clone(); + new_event.location -= *loc; + focus.motion(seat, data, &new_event); + } + } + + fn frame(&mut self, data: &mut D, seat: &Seat) { + let Some(marker) = self.pending_frame.take() else { + tracing::warn!("frame called without prior events"); + return; + }; + + for state in self.focus.values_mut() { + if state.current.map(|c| c == state.pending).unwrap_or(false) { + continue; + } + state.current = Some(marker); + + // Send the frame event for any stored focus in the up handler + if let Some(focus) = state.frame_pending.take() { + if focus.last_frame(seat, data) != Some(marker) { + focus.frame(seat, data, marker); + } + } + + if let Some((focus, _)) = state.focus.as_ref() { + if focus.last_frame(seat, data) != Some(marker) { + focus.frame(seat, data, marker); + } + } + } + + frame_marker::remove(marker.0); + } + + fn cancel(&mut self, data: &mut D, seat: &Seat) { + let Some(marker) = self.pending_frame.take() else { + tracing::warn!("cancel called without prior events"); + return; + }; + + for state in self.focus.values_mut() { + if state.current.map(|c| c == state.pending).unwrap_or(false) { + continue; + } + + state.current = Some(marker); + + if let Some((focus, _)) = state.focus.take() { + if focus.last_frame(seat, data) != Some(marker) { + focus.cancel(seat, data, marker); + } + } + } + + frame_marker::remove(marker.0); + } + + fn shape(&mut self, data: &mut D, seat: &Seat, event: &ShapeEvent) { + let marker = self.frame_marker(); + + let Some(state) = self.focus.get_mut(&event.slot) else { + return; + }; + + state.pending = marker; + if let Some((focus, _)) = state.focus.as_ref() { + focus.shape(seat, data, event); + } + } + + fn orientation(&mut self, data: &mut D, seat: &Seat, event: &OrientationEvent) { + let marker = self.frame_marker(); + + let Some(state) = self.focus.get_mut(&event.slot) else { + return; + }; + state.pending = marker; + if let Some((focus, _)) = state.focus.as_ref() { + focus.orientation(seat, data, event); + } + } + + fn with_grab(&mut self, data: &mut D, seat: &Seat, f: F) + where + F: FnOnce(&mut D, &mut TouchInnerHandle<'_, D>, &mut dyn TouchGrab), + { + let mut grab = std::mem::replace(&mut self.grab, GrabStatus::Borrowed); + match grab { + GrabStatus::Borrowed => panic!("Accessed a touch grab from within a touch grab access."), + GrabStatus::Active(_, ref mut handler) => { + // If this grab is associated with a surface that is no longer alive, discard it + if let Some((ref focus, _)) = handler.start_data().focus { + if !focus.alive() { + handler.unset(data); + self.grab = GrabStatus::None; + let mut default_grab = (self.default_grab)(); + f( + data, + &mut TouchInnerHandle { inner: self, seat }, + &mut *default_grab, + ); + return; + } + } + f(data, &mut TouchInnerHandle { inner: self, seat }, &mut **handler); + } + GrabStatus::None => { + let mut default_grab = (self.default_grab)(); + f( + data, + &mut TouchInnerHandle { inner: self, seat }, + &mut *default_grab, + ); + } + } + + if let GrabStatus::Borrowed = self.grab { + // the grab has not been ended nor replaced, put it back in place + self.grab = grab; + } + } + + fn frame_marker(&mut self) -> FrameMarker { + if self.pending_frame.is_none() { + self.pending_frame = Some(FrameMarker(frame_marker::next())); + }; + + self.pending_frame.unwrap() + } +} diff --git a/vendor/smithay/src/lib.rs b/vendor/smithay/src/lib.rs new file mode 100644 index 0000000..3039db3 --- /dev/null +++ b/vendor/smithay/src/lib.rs @@ -0,0 +1,78 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)] +// Allow acronyms like EGL +#![allow(clippy::upper_case_acronyms, clippy::collapsible_match)] + +//! # Smithay: the Wayland compositor smithy +//! +//! This crate is a general framework for building wayland compositors. It currently focuses on low-level, +//! helpers and abstractions, handling most of the system-level and wayland protocol interactions. +//! The window management and drawing logic is however at this time not provided (but helpers for this +//! are planned for future version). +//! +//! ## Structure of the crate +//! +//! The provided helpers are split into two main modules: +//! +//! - [`backend`] contains helpers for interacting with the operating +//! system, such as session management, interactions with the graphic stack +//! and input processing. +//! - [`wayland`] contains helpers for interacting with wayland clients +//! according to the wayland protocol. +//! +//! In addition, the [`xwayland`] module contains helpers for managing an +//! XWayland instance if you want to support it. See the documentation of +//! these respective modules for information about their usage. +//! +//! ## General principles for using Smithay +//! +//! ### The event loop and state handling +//! +//! Smithay is built around [`calloop`], a callback-oriented event loop, which fits naturally with the +//! general behavior of a wayland compositor: waiting for events to occur and react to them (be it +//! client requests, user input, or hardware events such as `vblank`). +//! +//! Using a callback-heavy structure however poses the question of state management: a lot of state needs +//! to be accessed from many different callbacks. To avoid a heavy requirement on shared pointers such +//! as `Rc` and `Arc` and the synchronization they require, [`calloop`] allows you to provide a mutable +//! reference to a value that will be passed down to most callbacks. This structure provides +//! easy access to a centralized mutable state without synchronization (as the callback invocation is +//! *always* sequential), and is the recommended way of structuring your compositor. +//! TODO: Add a section here how this links to wayland-server's `Dispatch` and link to the wayland-server +//! docs, once they exist for 0.30. +//! +//! Several objects, in particular on the wayland clients side, can exist as multiple instances where each +//! instance has its own associated state. For these situations, these objects provide an interface allowing +//! you to associate an arbitrary value to them, that you can access at any time from the object itself +//! (rather than having your own container in which you search for the appropriate value when you need it). +//! +//! ### Logging +//! +//! Smithay makes extensive use of [`tracing`] for its internal logging. +//! +//! For release builds it is recommended to limit the log level during compile time. +//! This can be done by adding a dependency to [`tracing`] and enabling the corresponding features. +//! For example to enable `trace` messages for debug builds, but limit release builds to `debug` add +//! the following in your binary crate `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_debug"] } +//! ``` +//! +//! If you do not want to use [`tracing`] for your compositor, refer to [`log compatibility`](tracing#log-compatibility) +//! for how to forward smithays debug output to other `log` compatible frameworks. + +pub mod backend; +#[cfg(feature = "desktop")] +pub mod desktop; +pub mod input; +pub mod output; +pub mod utils; +#[cfg(feature = "wayland_frontend")] +pub mod wayland; + +#[cfg(feature = "xwayland")] +pub mod xwayland; + +pub mod reexports; diff --git a/vendor/smithay/src/output.rs b/vendor/smithay/src/output.rs new file mode 100644 index 0000000..327e862 --- /dev/null +++ b/vendor/smithay/src/output.rs @@ -0,0 +1,558 @@ +//! Output +//! +//! This module provides a type helping you to abstract over various +//! properties, that make up an [`Output`] of your compositor. +//! +//! # How to use it +//! +//! You need to instantiate an [`Output`]. +//! +//! To advertise a new output global to wayland clients take a look at [`crate::wayland::output`], +//! you need to have the `wayland_frontend` feature enabled to use it. +//! +//! Additionally outputs are used to by the desktop abstractions (see [`crate::desktop`], needs the +//! `desktop` feature) to represent views into your window grid. +//! +//! You can use the returned [`Output`] to change +//! the properties of your output (if the current resolution mode changes for example). +//! These may influence how contents will be rendered to your output, when used in conjunction +//! with the desktop abstractions. +//! +//! You can attach additional properties to your `Output`s by using [`Output::user_data`]. +//! +//! ``` +//! # extern crate smithay; +//! use smithay::output::{Output, PhysicalProperties, Scale, Mode, Subpixel}; +//! use smithay::utils::Transform; +//! +//! // Create the Output with given name and physical properties. +//! let output = Output::new( +//! "output-0".into(), // the name of this output, +//! PhysicalProperties { +//! size: (200, 150).into(), // dimensions (width, height) in mm +//! subpixel: Subpixel::HorizontalRgb, // subpixel information +//! make: "Screens Inc".into(), // make of the monitor +//! model: "Monitor Ultra".into(), // model of the monitor +//! serial_number: "KL11C28J04XJ".into(), // serial number of the monitor +//! }, +//! ); +//! // Now you can configure it +//! output.change_current_state( +//! Some(Mode { size: (1920, 1080).into(), refresh: 60000 }), // the resolution mode, +//! Some(Transform::Normal), // global screen transformation +//! Some(Scale::Integer(1)), // global screen scaling factor +//! Some((0,0).into()) // output position +//! ); +//! // set the preferred mode +//! output.set_preferred(Mode { size: (1920, 1080).into(), refresh: 60000 }); +//! // add other supported modes +//! output.add_mode(Mode { size: (800, 600).into(), refresh: 60000 }); +//! output.add_mode(Mode { size: (1024, 768).into(), refresh: 60000 }); +//! ``` + +use std::{ + hash::{Hash, Hasher}, + sync::{Arc, Mutex, Weak}, +}; + +use tracing::{info, instrument}; + +#[cfg(feature = "wayland_frontend")] +use crate::wayland::output::xdg::XdgOutput; +#[cfg(feature = "backend_drm")] +use drm::control::{Mode as DrmMode, ModeFlags, connector::SubPixel as DrmSubPixel}; +#[cfg(feature = "wayland_frontend")] +use std::collections::HashSet; +#[cfg(feature = "wayland_frontend")] +use wayland_server::{ + Weak as WlWeak, backend::WeakHandle, protocol::wl_output::WlOutput, protocol::wl_surface::WlSurface, +}; + +use crate::utils::{self, Logical, Physical, Point, Raw, Size, Transform, user_data::UserDataMap}; + +/// An output mode +/// +/// A possible combination of dimensions and refresh rate for an output. +/// +/// This should only describe the characteristics of the video driver, +/// not taking into account any global scaling. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Mode { + /// The size of the mode, in pixels + pub size: Size, + /// The refresh rate in millihertz + /// + /// `1000` is one fps (frame per second), `2000` is 2 fps, etc... + pub refresh: i32, +} + +#[cfg(feature = "backend_drm")] +impl From for Mode { + #[inline] + fn from(mode: DrmMode) -> Self { + let clock = mode.clock() as u64; + let htotal = mode.hsync().2 as u64; + let vtotal = mode.vsync().2 as u64; + + let mut refresh = (clock * 1_000_000 / htotal + vtotal / 2) / vtotal; + + if mode.flags().contains(ModeFlags::INTERLACE) { + refresh *= 2; + } + + if mode.flags().contains(ModeFlags::DBLSCAN) { + refresh /= 2; + } + + if mode.vscan() > 1 { + refresh /= mode.vscan() as u64; + } + + let (w, h) = mode.size(); + + Self { + size: (w as i32, h as i32).into(), + refresh: refresh as i32, + } + } +} + +/// Subpixel geometry information +/// +/// This enumeration describes how the physical pixels on an output are laid out. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Subpixel { + /// Unknown subpixel geometry + Unknown, + /// No subpixel geometry + None, + /// Subpixels are arranged horizontally starting with + /// red, then green, last blue + HorizontalRgb, + /// Subpixels are arranged horizontally starting with + /// blue, then green, last red + HorizontalBgr, + /// Subpixels are arranged vertically starting with + /// red, then green, last blue + VerticalRgb, + /// Subpixels are arranged vertically starting with + /// blue, then green, last red + VerticalBgr, +} + +#[cfg(feature = "backend_drm")] +impl From for Subpixel { + #[inline] + fn from(mode: DrmSubPixel) -> Self { + match mode { + DrmSubPixel::Unknown => Self::Unknown, + DrmSubPixel::HorizontalRgb => Self::HorizontalRgb, + DrmSubPixel::HorizontalBgr => Self::HorizontalBgr, + DrmSubPixel::VerticalRgb => Self::VerticalRgb, + DrmSubPixel::VerticalBgr => Self::VerticalBgr, + DrmSubPixel::None => Self::None, + DrmSubPixel::NotImplemented => Self::Unknown, + _ => Self::Unknown, + } + } +} + +/// The physical properties of an output +#[derive(Debug, Clone)] +pub struct PhysicalProperties { + /// The size of the monitor, in millimeters + pub size: Size, + /// The subpixel geometry + pub subpixel: Subpixel, + /// Textual representation of the make + pub make: String, + /// Textual representation of the model + pub model: String, + /// Textual representation of the serial number + pub serial_number: String, +} + +/// Describes the scale advertised to clients. +#[derive(Debug, Clone, Copy)] +pub enum Scale { + /// Integer based scaling + Integer(i32), + /// Fractional scaling + /// + /// *Note*: Protocols only supporting integer-scales (e.g. wl_output), + /// will be rounded **up** to the next integer value. To control the value + /// send to those clients use `Scale::Custom` instead. + Fractional(f64), + /// Split scale values advertised to clients + Custom { + /// Integer value for protocols not supporting fractional scaling + advertised_integer: i32, + /// Fractional scaling value used elsewhere + fractional: f64, + }, +} + +impl Scale { + /// Returns the integer scale as advertised for this `Scale` variant + pub fn integer_scale(&self) -> i32 { + match self { + Scale::Integer(scale) => *scale, + Scale::Fractional(scale) => scale.ceil() as i32, + Scale::Custom { + advertised_integer, .. + } => *advertised_integer, + } + } + + /// Returns the fractional scale (calculated if necessary) + pub fn fractional_scale(&self) -> f64 { + match self { + Scale::Integer(scale) => *scale as f64, + Scale::Fractional(scale) => *scale, + Scale::Custom { fractional, .. } => *fractional, + } + } +} + +#[derive(Debug)] +pub(crate) struct Inner { + pub(crate) name: String, + pub(crate) description: String, + pub(crate) physical: PhysicalProperties, + pub(crate) location: Point, + pub(crate) transform: Transform, + pub(crate) scale: Scale, + pub(crate) modes: Vec, + pub(crate) current_mode: Option, + pub(crate) preferred_mode: Option, + + // used by the wayland::output module. + #[cfg(feature = "wayland_frontend")] + pub(crate) instances: Vec>, + #[cfg(feature = "wayland_frontend")] + pub(crate) handle: Option, + #[cfg(feature = "wayland_frontend")] + pub(crate) xdg_output: Option, + #[cfg(feature = "wayland_frontend")] + pub(crate) surfaces: HashSet>, +} + +/// An abstract output. +/// +/// This handle is stored in the event loop, and allows you to notify clients +/// about any change in the properties of this output. +#[derive(Debug, Clone)] +pub struct Output { + pub(crate) inner: Arc<(Mutex, UserDataMap)>, +} + +/// Weak variant of an [`Output`] +/// +/// Does not keep associated user data alive, +/// and can be used to refer to a potentially already destroyed output. +#[derive(Debug, Default, Clone)] +pub struct WeakOutput { + pub(crate) inner: Weak<(Mutex, UserDataMap)>, +} + +impl Output { + /// Create a new output with given name and physical properties. + #[instrument] + pub fn new(name: String, physical: PhysicalProperties) -> Output { + info!(name, "Creating new Output"); + + let data = Arc::new(( + Mutex::new(Inner { + name: name.clone(), + description: format!("{} - {} - {}", physical.make, physical.model, name), + #[cfg(feature = "wayland_frontend")] + instances: Vec::new(), + #[cfg(feature = "wayland_frontend")] + handle: None, + physical, + location: (0, 0).into(), + transform: Transform::Normal, + scale: Scale::Integer(1), + modes: Vec::new(), + current_mode: None, + preferred_mode: None, + #[cfg(feature = "wayland_frontend")] + xdg_output: None, + #[cfg(feature = "wayland_frontend")] + surfaces: HashSet::new(), + }), + UserDataMap::default(), + )); + + Output { inner: data } + } + + /// Sets the preferred mode of this output + /// + /// If the provided mode was not previously known to this output, it is added to its + /// internal list. + pub fn set_preferred(&self, mode: Mode) { + let mut inner = self.inner.0.lock().unwrap(); + inner.preferred_mode = Some(mode); + if inner.modes.iter().all(|&m| m != mode) { + inner.modes.push(mode); + } + } + + /// Adds a mode to the list of known modes to this output + pub fn add_mode(&self, mode: Mode) { + let mut inner = self.inner.0.lock().unwrap(); + if inner.modes.iter().all(|&m| m != mode) { + inner.modes.push(mode); + } + } + + /// Returns the currently advertised mode of the output + pub fn current_mode(&self) -> Option { + self.inner.0.lock().unwrap().current_mode + } + + /// Returns the preferred mode of the output + pub fn preferred_mode(&self) -> Option { + self.inner.0.lock().unwrap().preferred_mode + } + + /// Returns the currently advertised transformation of the output + pub fn current_transform(&self) -> Transform { + self.inner.0.lock().unwrap().transform + } + + /// Returns the currently set scale of the output + pub fn current_scale(&self) -> Scale { + self.inner.0.lock().unwrap().scale + } + + /// Returns the currently advertised location of the output + pub fn current_location(&self) -> Point { + self.inner.0.lock().unwrap().location + } + + /// Returns the name of the output + pub fn name(&self) -> String { + self.inner.0.lock().unwrap().name.clone() + } + + /// Returns the description of the output, if xdg-output is initialized + pub fn description(&self) -> String { + self.inner.0.lock().unwrap().description.clone() + } + + /// Returns the physical properties of the output + pub fn physical_properties(&self) -> PhysicalProperties { + self.inner.0.lock().unwrap().physical.clone() + } + + /// Returns the currently advertised modes of the output + pub fn modes(&self) -> Vec { + self.inner.0.lock().unwrap().modes.clone() + } + + /// Removes a mode from the list of known modes + /// + /// It will not de-advertise it from existing clients (the protocol does not + /// allow it), but it won't be advertised to now clients from now on. + pub fn delete_mode(&self, mode: Mode) { + let mut inner = self.inner.0.lock().unwrap(); + inner.modes.retain(|&m| m != mode); + if inner.current_mode == Some(mode) { + inner.current_mode = None; + } + if inner.preferred_mode == Some(mode) { + inner.preferred_mode = None; + } + } + + /// Change the current state of this output + /// + /// You can changed the current mode, transform status, location or scale of this output. Providing + /// `None` to any of these field means that the value does not change. + /// + /// If the provided mode was not previously known to this output, it is added to its + /// internal list. + /// + /// By default, transform status is `Normal`, and scale is `1`. + #[instrument(skip(self), fields(output = self.name()))] + pub fn change_current_state( + &self, + new_mode: Option, + new_transform: Option, + new_scale: Option, + new_location: Option>, + ) { + { + let mut inner = self.inner.0.lock().unwrap(); + if let Some(mode) = new_mode { + if inner.modes.iter().all(|&m| m != mode) { + inner.modes.push(mode); + } + inner.current_mode = new_mode; + } + if let Some(transform) = new_transform { + inner.transform = transform; + } + if let Some(scale) = new_scale { + inner.scale = scale; + } + if let Some(new_location) = new_location { + inner.location = new_location; + } + } + + #[cfg(feature = "wayland_frontend")] + self.wl_change_current_state(new_mode, new_transform.map(Into::into), new_scale, new_location) + } + + /// Returns the user data of this output + pub fn user_data(&self) -> &UserDataMap { + &self.inner.1 + } + + /// Create a weak reference to this output + pub fn downgrade(&self) -> WeakOutput { + WeakOutput { + inner: Arc::downgrade(&self.inner), + } + } + + /// Cleanup some internal resource. + /// + /// Needs to be called periodically, at best before every + /// wayland socket flush. + /// + /// Note: [`Space::refresh`](crate::desktop::space::Space) already calls this + /// function internally. So in case you already use the desktop module you can + /// skip calling this function explicitly. + #[profiling::function] + pub fn cleanup(&self) { + #[cfg(feature = "wayland_frontend")] + self.cleanup_surfaces(); + } +} + +impl PartialEq for Output { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + } +} + +impl Eq for Output {} + +impl Hash for Output { + #[inline] + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.inner).hash(state); + } +} + +impl WeakOutput { + /// Try to retrieve the original `Output`, if it still exists + #[inline] + pub fn upgrade(&self) -> Option { + self.inner.upgrade().map(|inner| Output { inner }) + } + + /// Check if the output is still alive + #[inline] + pub fn is_alive(&self) -> bool { + self.inner.strong_count() != 0 + } +} + +impl PartialEq for WeakOutput { + #[inline] + fn eq(&self, other: &Self) -> bool { + Weak::ptr_eq(&self.inner, &other.inner) + } +} + +impl Eq for WeakOutput {} + +impl Hash for WeakOutput { + #[inline] + fn hash(&self, state: &mut H) { + Weak::as_ptr(&self.inner).hash(state); + } +} + +impl PartialEq for Output { + #[inline] + fn eq(&self, other: &WeakOutput) -> bool { + other.upgrade().map(|o| &o == self).unwrap_or(false) + } +} + +impl PartialEq for WeakOutput { + #[inline] + fn eq(&self, other: &Output) -> bool { + self.upgrade().map(|o| &o == other).unwrap_or(false) + } +} + +/// Source for determining output mode information. +#[derive(PartialEq, Clone, Debug)] +pub enum OutputModeSource { + /// Automatic mode based on an [`Output`]. + Auto(WeakOutput), + /// Static output mode. + Static { + /// Size of the static output + size: Size, + /// Scale of the static output + scale: utils::Scale, + /// Transform of the static output + transform: Transform, + }, +} + +impl From<&Output> for OutputModeSource { + #[inline] + fn from(output: &Output) -> Self { + Self::Auto(output.downgrade()) + } +} + +impl TryFrom<&OutputModeSource> for (Size, utils::Scale, Transform) { + type Error = OutputNoMode; + + #[inline] + fn try_from(mode: &OutputModeSource) -> Result { + match mode { + OutputModeSource::Auto(output) => { + let Some(output) = output.upgrade() else { + return Err(OutputNoMode); + }; + let guard = output.inner.0.lock().unwrap(); + Ok(( + guard.current_mode.as_ref().ok_or(OutputNoMode)?.size, + guard.scale.fractional_scale().into(), + guard.transform, + )) + } + OutputModeSource::Static { + size, + scale, + transform, + } => Ok((*size, *scale, *transform)), + } + } +} + +impl TryFrom for (Size, utils::Scale, Transform) { + type Error = OutputNoMode; + + #[inline] + fn try_from(mode: OutputModeSource) -> Result { + Self::try_from(&mode) + } +} + +/// Output has no active mode +#[derive(Debug, thiserror::Error)] +#[error("Output has no active mode")] +pub struct OutputNoMode; diff --git a/vendor/smithay/src/reexports.rs b/vendor/smithay/src/reexports.rs new file mode 100644 index 0000000..59cc60f --- /dev/null +++ b/vendor/smithay/src/reexports.rs @@ -0,0 +1,33 @@ +//! Reexports of crates, that are part of the public api, for convenience + +#[cfg(feature = "backend_vulkan")] +pub use ash; +pub use calloop; +#[cfg(feature = "backend_drm")] +pub use drm; +#[cfg(feature = "backend_gbm")] +pub use gbm; +#[cfg(feature = "renderer_glow")] +pub use glow; +#[cfg(feature = "backend_libinput")] +pub use input; +#[cfg(feature = "renderer_pixman")] +pub use pixman; +pub use reis; +pub use rustix; +#[cfg(feature = "tracy_gpu_profiling")] +pub use tracy_client; +#[cfg(feature = "backend_udev")] +pub use udev; +#[cfg(feature = "wayland_frontend")] +pub use wayland_protocols; +#[cfg(feature = "wayland_frontend")] +pub use wayland_protocols_misc; +#[cfg(feature = "wayland_frontend")] +pub use wayland_protocols_wlr; +#[cfg(feature = "wayland_frontend")] +pub use wayland_server; +#[cfg(feature = "backend_winit")] +pub use winit; +#[cfg(feature = "x11rb_event_source")] +pub use x11rb; diff --git a/vendor/smithay/src/utils/alive_tracker.rs b/vendor/smithay/src/utils/alive_tracker.rs new file mode 100644 index 0000000..aab4a70 --- /dev/null +++ b/vendor/smithay/src/utils/alive_tracker.rs @@ -0,0 +1,48 @@ +//! Utilities to track object's life cycle + +// All our AliveTracker usage is internal and only +// used, when the wayland_frontend feature is enabled. +// So we need to silence some warnings in other cases. +#![allow(dead_code)] + +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Util to track wayland object's life time +#[derive(Debug)] +pub struct AliveTracker { + is_alive: AtomicBool, +} + +impl Default for AliveTracker { + fn default() -> Self { + Self { + is_alive: AtomicBool::new(true), + } + } +} + +impl AliveTracker { + /// Notify the tracker that object is dead + pub fn destroy_notify(&self) { + self.is_alive.store(false, Ordering::Release); + } + + /// Check if object is alive + #[inline] + pub fn alive(&self) -> bool { + self.is_alive.load(Ordering::Acquire) + } +} + +/// Trait that is implemented on wayland objects tracked by Smithay +pub trait IsAlive { + /// Check if object is alive + fn alive(&self) -> bool; +} + +impl IsAlive for &T { + #[inline] + fn alive(&self) -> bool { + IsAlive::alive(*self) + } +} diff --git a/vendor/smithay/src/utils/clock.rs b/vendor/smithay/src/utils/clock.rs new file mode 100644 index 0000000..1a521ed --- /dev/null +++ b/vendor/smithay/src/utils/clock.rs @@ -0,0 +1,239 @@ +use rustix::time::{ClockId, Timespec}; +use std::{cmp::Ordering, marker::PhantomData, ops::Add, time::Duration}; + +/// Marker for clock source that never returns a negative [`Time`] +pub trait NonNegativeClockSource: ClockSource {} + +/// Monotonic clock +#[derive(Debug)] +pub struct Monotonic; + +impl ClockSource for Monotonic { + const ID: ClockId = ClockId::Monotonic; +} + +impl NonNegativeClockSource for Monotonic {} + +/// Realtime clock +#[derive(Debug)] +pub struct Realtime; + +impl ClockSource for Realtime { + const ID: ClockId = ClockId::Realtime; +} + +/// Id for a clock according to unix clockid_t +pub trait ClockSource { + /// Gets the id of the clock source + const ID: ClockId; +} + +/// Defines a clock with a specific kind +#[derive(Debug)] +pub struct Clock { + _kind: PhantomData, +} + +impl Clock { + /// Initialize a new clock + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Clock { _kind: PhantomData } + } + + /// Returns the current time + pub fn now(&self) -> Time { + rustix::time::clock_gettime(Kind::ID).into() + } + + /// Gets the id of the clock + pub fn id(&self) -> ClockId { + Kind::ID + } +} + +/// A point in time for a clock with a specific kind +pub struct Time { + tp: Timespec, + _kind: PhantomData, +} + +impl Time { + /// Gets the duration from self until a later time + pub fn elapsed(elapsed: &Time, later: Time) -> Duration { + saturating_sub_timespec(later.tp, elapsed.tp).unwrap_or(Duration::ZERO) + } +} + +impl Time { + /// Returns the time in milliseconds + /// + /// This should match timestamps from libinput: + /// + pub fn as_millis(&self) -> u32 { + debug_assert!(self.tp.tv_sec >= 0); + debug_assert!(self.tp.tv_nsec >= 0); + + // The monotonic clock does not fit as milliseconds in 32-bit after ~50 days of uptime. We + // do a modulo conversion which should match what happens in libinput. + (self.as_micros() / 1000) as u32 + } + + /// Returns the time in microseconds + /// + /// This should match timestamps from libinput: + /// + pub fn as_micros(&self) -> u64 { + // Assume monotonic clock (but not realtime) fits as microseconds in 64-bit + debug_assert!(self.tp.tv_sec >= 0); + debug_assert!(self.tp.tv_nsec >= 0); + self.tp.tv_sec as u64 * 1000000 + self.tp.tv_nsec as u64 / 1000 + } +} + +impl Clone for Time { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Time {} + +impl From> for Duration { + #[inline] + fn from(time: Time) -> Self { + debug_assert!(time.tp.tv_sec >= 0); + debug_assert!(time.tp.tv_nsec >= 0); + Duration::new(time.tp.tv_sec as u64, time.tp.tv_nsec as u32) + } +} + +impl std::fmt::Debug for Time { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Time") + .field("tp", &self.tp) + .field("kind", &self._kind) + .finish() + } +} + +impl PartialEq for Time { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.tp == other.tp && self._kind == other._kind + } +} + +impl Eq for Time {} + +impl PartialOrd for Time { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Time { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let tv_sec = self.tp.tv_sec.cmp(&other.tp.tv_sec); + + if tv_sec == Ordering::Equal { + self.tp.tv_nsec.cmp(&other.tp.tv_nsec) + } else { + tv_sec + } + } +} + +impl From for Time { + #[inline] + fn from(tp: Duration) -> Self { + let tp = Timespec { + tv_sec: tp.as_secs() as rustix::time::Secs, + tv_nsec: tp.subsec_nanos() as rustix::time::Nsecs, + }; + Time { + tp, + _kind: PhantomData, + } + } +} + +impl From for Time { + #[inline] + fn from(tp: Timespec) -> Self { + Time { + tp, + _kind: PhantomData, + } + } +} + +impl From> for Timespec { + fn from(value: Time) -> Self { + value.tp + } +} + +const NANOS_PER_SEC: rustix::time::Nsecs = 1_000_000_000; + +fn saturating_sub_timespec(lhs: Timespec, rhs: Timespec) -> Option { + debug_assert!(!lhs.tv_sec.is_negative()); + debug_assert!(!lhs.tv_nsec.is_negative()); + debug_assert!(!rhs.tv_sec.is_negative()); + debug_assert!(!rhs.tv_nsec.is_negative()); + debug_assert!(lhs.tv_nsec < NANOS_PER_SEC); + debug_assert!(rhs.tv_nsec < NANOS_PER_SEC); + + let lhs_tv_sec = lhs.tv_sec as u64; + let lhs_tv_nsec = lhs.tv_nsec as u64; + let rhs_tv_sec = rhs.tv_sec as u64; + let rhs_tv_nsec = rhs.tv_nsec as u64; + + if let Some(mut secs) = lhs_tv_sec.checked_sub(rhs_tv_sec) { + let nanos = if lhs_tv_nsec >= rhs_tv_nsec { + lhs_tv_nsec - rhs_tv_nsec + } else { + let sub_secs = secs.checked_sub(1)?; + secs = sub_secs; + lhs_tv_nsec + (NANOS_PER_SEC as u64) - rhs_tv_nsec + }; + debug_assert!(nanos < (NANOS_PER_SEC as u64)); + Some(Duration::new(secs, nanos as u32)) + } else { + None + } +} + +impl Add for Time +where + T: Into>, +{ + type Output = Time; + + fn add(self, rhs: T) -> Self::Output { + let rhs = rhs.into(); + let tv_nsec = (self.tp.tv_nsec + rhs.tp.tv_nsec) % NANOS_PER_SEC; + #[allow(clippy::useless_conversion)] // rust-clippy/issues/6466, PR 2103 + let tv_sec = + (self.tp.tv_sec + rhs.tp.tv_sec) + i64::from((self.tp.tv_nsec + rhs.tp.tv_nsec) / NANOS_PER_SEC); + Self::from(Timespec { tv_sec, tv_nsec }) + } +} + +#[cfg(test)] +mod test { + use std::time::Duration; + + use crate::utils::{Clock, Monotonic, Time}; + + #[test] + fn monotonic() { + let clock_source: Clock = Clock::new(); + let now = clock_source.now(); + let zero = Time::::from(Duration::ZERO); + assert_eq!(Time::::elapsed(&zero, now), now.into()); + } +} diff --git a/vendor/smithay/src/utils/fd.rs b/vendor/smithay/src/utils/fd.rs new file mode 100644 index 0000000..489cbb0 --- /dev/null +++ b/vendor/smithay/src/utils/fd.rs @@ -0,0 +1,70 @@ +use std::{ + os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}, + path::PathBuf, + sync::Arc, +}; + +/// Ref-counted file descriptor of an open device node +#[derive(Debug, Clone)] +pub struct DeviceFd(Arc); + +impl PartialEq for DeviceFd { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.as_raw_fd() == other.0.as_raw_fd() + } +} + +impl AsFd for DeviceFd { + #[inline] + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.as_fd() + } +} + +// TODO: drop impl once not needed anymore by smithay or dependencies +impl AsRawFd for DeviceFd { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.0.as_raw_fd() + } +} + +impl FromRawFd for DeviceFd { + /// SAFETY: + /// Make sure that `fd` is a valid value! + #[inline] + unsafe fn from_raw_fd(fd: RawFd) -> Self { + DeviceFd(Arc::new(unsafe { OwnedFd::from_raw_fd(fd) })) + } +} + +impl From for DeviceFd { + #[inline] + fn from(fd: OwnedFd) -> Self { + DeviceFd(Arc::new(fd)) + } +} + +impl TryInto for DeviceFd { + type Error = DeviceFd; + + #[inline] + fn try_into(self) -> Result { + Arc::try_unwrap(self.0).map_err(DeviceFd) + } +} + +/// Trait representing open devices that *may* return a `Path` +pub trait DevPath { + /// Returns the path of the open device if possible + fn dev_path(&self) -> Option; +} + +impl DevPath for A { + fn dev_path(&self) -> Option { + use std::fs; + + fs::read_link(format!("/proc/self/fd/{:?}", self.as_fd().as_raw_fd())).ok() + } +} diff --git a/vendor/smithay/src/utils/geometry.rs b/vendor/smithay/src/utils/geometry.rs new file mode 100644 index 0000000..f97c328 --- /dev/null +++ b/vendor/smithay/src/utils/geometry.rs @@ -0,0 +1,2285 @@ +use std::fmt; +use std::num::Saturating; +use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; + +#[cfg(feature = "wayland_frontend")] +use wayland_server::protocol::wl_output::Transform as WlTransform; + +/// Type-level marker for the logical coordinate space +#[derive(Debug)] +pub struct Logical; + +/// Type-level marker for the client logical coordinate space +#[derive(Debug)] +#[cfg(feature = "wayland_frontend")] +pub(crate) struct Client; + +/// Type-level marker for the physical coordinate space +#[derive(Debug)] +pub struct Physical; + +/// Type-level marker for the buffer coordinate space +#[derive(Debug)] +pub struct Buffer; + +/// Type-level marker for raw coordinate space, provided by input devices +#[derive(Debug)] +pub struct Raw; + +/// Trait for types serving as a coordinate for other geometry utils +pub trait Coordinate: + Sized + Add + Sub + PartialOrd + Default + Copy + fmt::Debug +{ + /// A Coordinate that is 0 + const ZERO: Self; + /// Downscale the coordinate + #[must_use = "this returns the result of the operation, without modifying the original"] + fn downscale(self, scale: Self) -> Self; + /// Upscale the coordinate + #[must_use = "this returns the result of the operation, without modifying the original"] + fn upscale(self, scale: Self) -> Self; + /// Convert the coordinate to a f64 + #[must_use = "this returns the result of the operation, without modifying the original"] + fn to_f64(self) -> f64; + /// Convert to this coordinate from a f64 + fn from_f64(v: f64) -> Self; + /// Compare and return the smaller one + #[must_use = "this returns the result of the operation, without modifying the original"] + fn min(self, other: Self) -> Self { + if self < other { self } else { other } + } + /// Compare and return the larger one + #[must_use = "this returns the result of the operation, without modifying the original"] + fn max(self, other: Self) -> Self { + if self > other { self } else { other } + } + /// Test if the coordinate is not negative + fn non_negative(self) -> bool; + /// Returns the absolute value of this coordinate + #[must_use = "this returns the result of the operation, without modifying the original"] + fn abs(self) -> Self; + + /// Saturating integer addition. Computes self + other, saturating at the numeric bounds instead of overflowing. + #[must_use = "this returns the result of the operation, without modifying the original"] + fn saturating_add(self, other: Self) -> Self; + /// Saturating integer subtraction. Computes self - other, saturating at the numeric bounds instead of overflowing. + #[must_use = "this returns the result of the operation, without modifying the original"] + fn saturating_sub(self, other: Self) -> Self; + /// Saturating integer multiplication. Computes self * other, saturating at the numeric bounds instead of overflowing. + #[must_use = "this returns the result of the operation, without modifying the original"] + fn saturating_mul(self, other: Self) -> Self; +} + +/// Implements Coordinate for an unsigned numerical type. +macro_rules! unsigned_coordinate_impl { + ($ty:ty, $ ($tys:ty),* ) => { + unsigned_coordinate_impl!($ty); + $( + unsigned_coordinate_impl!($tys); + )* + }; + + ($ty:ty) => { + impl Coordinate for $ty { + const ZERO: $ty = 0; + + #[inline] + fn downscale(self, scale: Self) -> Self { + self / scale + } + + #[inline] + fn upscale(self, scale: Self) -> Self { + self.saturating_mul(scale) + } + + #[inline] + fn to_f64(self) -> f64 { + self as f64 + } + + #[inline] + fn from_f64(v: f64) -> Self { + v as Self + } + + #[inline] + fn non_negative(self) -> bool { + true + } + + #[inline] + fn abs(self) -> Self { + self + } + + #[inline] + fn saturating_add(self, other: Self) -> Self { + self.saturating_add(other) + } + #[inline] + fn saturating_sub(self, other: Self) -> Self { + self.saturating_sub(other) + } + #[inline] + fn saturating_mul(self, other: Self) -> Self { + self.saturating_mul(other) + } + } + }; +} + +unsigned_coordinate_impl! { + u8, + u16, + u32, + u64, + u128 +} + +/// Implements Coordinate for an signed numerical type. +macro_rules! signed_coordinate_impl { + ($ty:ty, $ ($tys:ty),* ) => { + signed_coordinate_impl!($ty); + $( + signed_coordinate_impl!($tys); + )* + }; + + ($ty:ty) => { + impl Coordinate for $ty { + const ZERO: $ty = 0; + + #[inline] + fn downscale(self, scale: Self) -> Self { + self / scale + } + + #[inline] + fn upscale(self, scale: Self) -> Self { + self.saturating_mul(scale) + } + + #[inline] + fn to_f64(self) -> f64 { + self as f64 + } + + #[inline] + fn from_f64(v: f64) -> Self { + v as Self + } + + #[inline] + fn non_negative(self) -> bool { + self >= 0 + } + + #[inline] + fn abs(self) -> Self { + self.abs() + } + + #[inline] + fn saturating_add(self, other: Self) -> Self { + self.saturating_add(other) + } + #[inline] + fn saturating_sub(self, other: Self) -> Self { + self.saturating_sub(other) + } + #[inline] + fn saturating_mul(self, other: Self) -> Self { + self.saturating_mul(other) + } + } + + impl Coordinate for Saturating<$ty> { + const ZERO: Saturating<$ty> = Saturating(0); + + #[inline] + fn downscale(self, scale: Self) -> Self { + self / scale + } + + #[inline] + fn upscale(self, scale: Self) -> Self { + self * scale + } + + #[inline] + fn to_f64(self) -> f64 { + self.0 as f64 + } + + #[inline] + fn from_f64(v: f64) -> Self { + Saturating(v as $ty) + } + + #[inline] + fn non_negative(self) -> bool { + self.0 >= 0 + } + + #[inline] + fn abs(self) -> Self { + self.abs() + } + + #[inline] + fn saturating_add(self, other: Self) -> Self { + self + other + } + #[inline] + fn saturating_sub(self, other: Self) -> Self { + self - other + } + #[inline] + fn saturating_mul(self, other: Self) -> Self { + self * other + } + } + }; +} + +signed_coordinate_impl! { + i8, + i16, + i32, + i64, + i128 +} + +macro_rules! floating_point_coordinate_impl { + ($ty:ty, $ ($tys:ty),* ) => { + floating_point_coordinate_impl!($ty); + $( + floating_point_coordinate_impl!($tys); + )* + }; + + ($ty:ty) => { + impl Coordinate for $ty { + const ZERO: $ty = 0.0; + + #[inline] + fn downscale(self, scale: Self) -> Self { + self / scale + } + + #[inline] + fn upscale(self, scale: Self) -> Self { + self * scale + } + + #[inline] + fn to_f64(self) -> f64 { + self as f64 + } + + #[inline] + fn from_f64(v: f64) -> Self { + v as Self + } + + #[inline] + fn non_negative(self) -> bool { + self >= 0.0 + } + + #[inline] + fn abs(self) -> Self { + self.abs() + } + + #[inline] + fn saturating_add(self, other: Self) -> Self { + self + other + } + #[inline] + fn saturating_sub(self, other: Self) -> Self { + self - other + } + #[inline] + fn saturating_mul(self, other: Self) -> Self { + self * other + } + } + }; +} + +floating_point_coordinate_impl! { + f32, + f64 +} + +/* + * Scale + */ + +/// A two-dimensional scale that can be +/// used to scale [`Point`]s, [`Size`]s and +/// [`Rectangle`]s +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Scale { + /// The scale on the x axis + pub x: N, + /// The scale on the y axis + pub y: N, +} + +impl Scale { + /// Convert the underlying numerical type to f64 for floating point manipulations + #[inline] + pub fn to_f64(self) -> Scale { + Scale { + x: self.x.to_f64(), + y: self.y.to_f64(), + } + } +} + +impl From for Scale { + #[inline] + fn from(scale: N) -> Self { + Scale { x: scale, y: scale } + } +} + +impl From<(N, N)> for Scale { + #[inline] + fn from((scale_x, scale_y): (N, N)) -> Self { + Scale { + x: scale_x, + y: scale_y, + } + } +} + +impl Mul for Scale +where + N: Coordinate, + T: Into>, +{ + type Output = Scale; + + #[inline] + fn mul(self, rhs: T) -> Self::Output { + let rhs = rhs.into(); + Scale { + x: self.x.upscale(rhs.x), + y: self.y.upscale(rhs.y), + } + } +} + +/* + * Point + */ + +/// A point as defined by its x and y coordinates +/// +/// Operations on points are saturating. +#[repr(C)] +pub struct Point { + /// horizontal coordinate + pub x: N, + /// vertical coordinate + pub y: N, + _kind: std::marker::PhantomData, +} + +impl Point { + /// Create a new Point + pub const fn new(x: N, y: N) -> Point { + Point { + x, + y, + _kind: std::marker::PhantomData, + } + } +} + +impl Point { + /// Convert this [`Point`] to a [`Size`] with the same coordinates + /// + /// Checks that the coordinates are positive with a `debug_assert!()`. + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_size(self) -> Size { + debug_assert!( + self.x.non_negative() && self.y.non_negative(), + "Attempting to create a `Size` of negative size: {:?}", + (self.x, self.y) + ); + Size { + w: self.x, + h: self.y, + _kind: std::marker::PhantomData, + } + } + + /// Convert this [`Point`] to a [`Size`] with the same coordinates + /// + /// Ensures that the coordinates are positive by taking their absolute value + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_size_abs(self) -> Size { + Size { + w: self.x.abs(), + h: self.y.abs(), + _kind: std::marker::PhantomData, + } + } + + /// Upscale this [`Point`] by a specified [`Scale`] + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn upscale(self, scale: impl Into>) -> Point { + let scale = scale.into(); + Point { + x: self.x.upscale(scale.x), + y: self.y.upscale(scale.y), + _kind: std::marker::PhantomData, + } + } + + /// Downscale this [`Point`] by a specified [`Scale`] + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn downscale(self, scale: impl Into>) -> Point { + let scale = scale.into(); + Point { + x: self.x.downscale(scale.x), + y: self.y.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +impl Point { + /// Constrain this [`Point`] within a [`Rectangle`] with the same coordinates + /// + /// The [`Point`] returned is guaranteed to be not smaller than the [`Rectangle`] + /// location and not greater than the [`Rectangle`] location plus size. + #[inline] + pub fn constrain(self, rect: impl Into>) -> Point { + let rect = rect.into(); + + Point { + x: self.x.max(rect.loc.x).min(rect.loc.x + rect.size.w), + y: self.y.max(rect.loc.y).min(rect.loc.y + rect.size.h), + _kind: std::marker::PhantomData, + } + } +} + +impl Point { + /// Convert the underlying numerical type to f64 for floating point manipulations + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_f64(self) -> Point { + Point { + x: self.x.to_f64(), + y: self.y.to_f64(), + _kind: std::marker::PhantomData, + } + } +} + +impl Point { + /// Convert to i32 for integer-space manipulations by rounding float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_round(self) -> Point { + Point { + x: N::from_f64(self.x.round()), + y: N::from_f64(self.y.round()), + _kind: std::marker::PhantomData, + } + } + + /// Convert to i32 for integer-space manipulations by flooring float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_floor(self) -> Point { + Point { + x: N::from_f64(self.x.floor()), + y: N::from_f64(self.y.floor()), + _kind: std::marker::PhantomData, + } + } + + /// Convert to i32 for integer-space manipulations by ceiling float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_ceil(self) -> Point { + Point { + x: N::from_f64(self.x.ceil()), + y: N::from_f64(self.y.ceil()), + _kind: std::marker::PhantomData, + } + } +} + +impl fmt::Debug for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("Point<{}>", std::any::type_name::()))?; + f.debug_struct("") + .field("x", &self.x) + .field("y", &self.y) + .finish() + } +} + +impl Point { + #[inline] + #[cfg(feature = "wayland_frontend")] + pub(crate) fn to_client(self, scale: impl Into>) -> Point { + let scale: Scale = scale.into(); + Point { + x: self.x.upscale(scale.x), + y: self.y.upscale(scale.y), + _kind: std::marker::PhantomData, + } + } + + #[inline] + #[cfg(feature = "wayland_frontend")] + pub(crate) fn to_client_precise_round( + self, + scale: impl Into>, + ) -> Point { + self.to_f64().to_client(scale.into().to_f64()).to_i32_round() + } + + /// Convert this logical point to physical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical(self, scale: impl Into>) -> Point { + let scale = scale.into(); + Point { + x: self.x.upscale(scale.x), + y: self.y.upscale(scale.y), + _kind: std::marker::PhantomData, + } + } + + /// Convert this logical point to physical coordinate space according to given scale factor + /// and round the result + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_round( + self, + scale: impl Into>, + ) -> Point { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_round() + } + + /// Convert this logical point to physical coordinate space according to given scale factor + /// and ceil the result + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_ceil( + &self, + scale: impl Into>, + ) -> Point { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_ceil() + } + + /// Convert this logical point to physical coordinate space according to given scale factor + /// and floor the result + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_floor( + &self, + scale: impl Into>, + ) -> Point { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_floor() + } + + /// Convert this logical point to buffer coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_buffer( + self, + scale: impl Into>, + transformation: Transform, + area: &Size, + ) -> Point { + let point = transformation.transform_point_in(self, area); + let scale = scale.into(); + Point { + x: point.x.upscale(scale.x), + y: point.y.upscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +#[cfg(feature = "wayland_frontend")] +impl Point { + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub(crate) fn to_logical(self, scale: impl Into>) -> Point { + let scale = scale.into(); + Point { + x: self.x.downscale(scale.x), + y: self.y.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +impl Point { + /// Convert this physical point to logical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_logical(self, scale: impl Into>) -> Point { + let scale = scale.into(); + Point { + x: self.x.downscale(scale.x), + y: self.y.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +impl Point { + /// Convert this physical point to logical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_logical( + self, + scale: impl Into>, + transform: Transform, + area: &Size, + ) -> Point { + let point = transform.invert().transform_point_in(self, area); + let scale = scale.into(); + Point { + x: point.x.downscale(scale.x), + y: point.y.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +impl From<(N, N)> for Point { + #[inline] + fn from((x, y): (N, N)) -> Point { + Point::new(x, y) + } +} + +impl From> for (N, N) { + #[inline] + fn from(point: Point) -> (N, N) { + (point.x, point.y) + } +} + +impl Add for Point { + type Output = Point; + #[inline] + fn add(self, other: Point) -> Point { + Point { + x: self.x.saturating_add(other.x), + y: self.y.saturating_add(other.y), + _kind: std::marker::PhantomData, + } + } +} + +impl AddAssign for Point { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.x = self.x.saturating_add(rhs.x); + self.y = self.y.saturating_add(rhs.y); + } +} + +impl SubAssign for Point { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.x = self.x.saturating_sub(rhs.x); + self.y = self.y.saturating_sub(rhs.y); + } +} + +impl Sub for Point { + type Output = Point; + #[inline] + fn sub(self, other: Point) -> Point { + Point { + x: self.x.saturating_sub(other.x), + y: self.y.saturating_sub(other.y), + _kind: std::marker::PhantomData, + } + } +} + +impl Clone for Point { + #[inline] + fn clone(&self) -> Self { + Point { + x: self.x.clone(), + y: self.y.clone(), + _kind: std::marker::PhantomData, + } + } +} + +impl Copy for Point {} + +impl PartialEq for Point { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.x == other.x && self.y == other.y + } +} + +impl Eq for Point {} + +impl Default for Point { + #[inline] + fn default() -> Self { + Point { + x: N::default(), + y: N::default(), + _kind: std::marker::PhantomData, + } + } +} + +/* + * Size + */ + +/// A size as defined by its width and height +/// +/// Constructors of this type ensure that the values are always positive via +/// `debug_assert!()`, however manually changing the values of the fields +/// can break this invariant. +/// +/// Operations on sizes are saturating. +#[repr(C)] +pub struct Size { + /// horizontal coordinate + pub w: N, + /// vertical coordinate + pub h: N, + _kind: std::marker::PhantomData, +} + +impl Size { + /// Create a new Size + pub fn new(w: N, h: N) -> Size { + debug_assert!( + w.non_negative() && h.non_negative(), + "Attempting to create a `Size` of negative size: {:?}", + (w, h) + ); + Size { + w, + h, + _kind: std::marker::PhantomData, + } + } +} + +impl Size { + /// Convert this [`Size`] to a [`Point`] with the same coordinates + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_point(self) -> Point { + Point { + x: self.w, + y: self.h, + _kind: std::marker::PhantomData, + } + } +} + +impl Size { + /// Restrict this [`Size`] to min and max [`Size`] with the same coordinates + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn clamp(self, min: impl Into>, max: impl Into>) -> Size { + let min = min.into(); + let max = max.into(); + + Size { + w: self.w.max(min.w).min(max.w), + h: self.h.max(min.h).min(max.h), + _kind: std::marker::PhantomData, + } + } +} + +impl Size { + /// Convert the underlying numerical type to f64 for floating point manipulations + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_f64(self) -> Size { + Size { + w: self.w.to_f64(), + h: self.h.to_f64(), + _kind: std::marker::PhantomData, + } + } +} + +impl Size { + /// Upscale this [`Size`] by a specified [`Scale`] + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn upscale(self, scale: impl Into>) -> Size { + let scale = scale.into(); + Size { + w: self.w.upscale(scale.x), + h: self.h.upscale(scale.y), + _kind: std::marker::PhantomData, + } + } + + /// Downscale this [`Size`] by a specified [`Scale`] + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn downscale(self, scale: impl Into>) -> Size { + let scale = scale.into(); + Size { + w: self.w.downscale(scale.x), + h: self.h.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } + + /// Check if this [`Size`] is empty + /// + /// Returns true if either the width or the height is zero + #[inline] + pub fn is_empty(&self) -> bool { + self.w == N::default() || self.h == N::default() + } +} + +impl Size { + /// Convert to i32 for integer-space manipulations by rounding float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_round(self) -> Size { + Size { + w: N::from_f64(self.w.round()), + h: N::from_f64(self.h.round()), + _kind: std::marker::PhantomData, + } + } + + /// Convert to i32 for integer-space manipulations by flooring float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_floor(self) -> Size { + Size { + w: N::from_f64(self.w.floor()), + h: N::from_f64(self.h.floor()), + _kind: std::marker::PhantomData, + } + } + + /// Convert to i32 for integer-space manipulations by ceiling float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_ceil(self) -> Size { + Size { + w: N::from_f64(self.w.ceil()), + h: N::from_f64(self.h.ceil()), + _kind: std::marker::PhantomData, + } + } +} + +impl fmt::Debug for Size { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("Size<{}>", std::any::type_name::()))?; + f.debug_struct("") + .field("w", &self.w) + .field("h", &self.h) + .finish() + } +} + +impl Size { + #[inline] + #[cfg(feature = "wayland_frontend")] + pub(crate) fn to_client(self, scale: impl Into>) -> Size { + let scale = scale.into(); + Size { + w: self.w.upscale(scale.x), + h: self.h.upscale(scale.y), + _kind: std::marker::PhantomData, + } + } + + /// Convert this logical size to physical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical(self, scale: impl Into>) -> Size { + let scale = scale.into(); + Size { + w: self.w.upscale(scale.x), + h: self.h.upscale(scale.y), + _kind: std::marker::PhantomData, + } + } + + /// Convert this logical size to physical coordinate space according to given scale factor + /// and round the result + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_round( + self, + scale: impl Into>, + ) -> Size { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_round() + } + + /// Convert this logical size to physical coordinate space according to given scale factor + /// and ceil the result + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_ceil( + &self, + scale: impl Into>, + ) -> Size { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_ceil() + } + + /// Convert this logical size to physical coordinate space according to given scale factor + /// and floor the result + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_floor( + &self, + scale: impl Into>, + ) -> Size { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_floor() + } + + /// Convert this logical size to buffer coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_buffer(self, scale: impl Into>, transformation: Transform) -> Size { + let scale = scale.into(); + transformation.transform_size(Size { + w: self.w.upscale(scale.x), + h: self.h.upscale(scale.y), + _kind: std::marker::PhantomData, + }) + } +} + +#[cfg(feature = "wayland_frontend")] +impl Size { + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub(crate) fn to_logical(self, scale: impl Into>) -> Size { + let scale = scale.into(); + Size { + w: self.w.downscale(scale.x), + h: self.h.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +impl Size { + /// Convert this physical point to logical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_logical(self, scale: impl Into>) -> Size { + let scale = scale.into(); + Size { + w: self.w.downscale(scale.x), + h: self.h.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +impl Size { + /// Convert this physical point to logical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_logical(self, scale: impl Into>, transformation: Transform) -> Size { + let scale = scale.into(); + transformation.invert().transform_size(Size { + w: self.w.downscale(scale.x), + h: self.h.downscale(scale.y), + _kind: std::marker::PhantomData, + }) + } +} + +impl From<(N, N)> for Size { + #[inline] + fn from((w, h): (N, N)) -> Size { + Size::new(w, h) + } +} + +impl From> for (N, N) { + #[inline] + fn from(point: Size) -> (N, N) { + (point.w, point.h) + } +} + +impl Add for Size { + type Output = Size; + #[inline] + fn add(self, other: Size) -> Size { + Size { + w: self.w.saturating_add(other.w), + h: self.h.saturating_add(other.h), + _kind: std::marker::PhantomData, + } + } +} + +impl AddAssign for Size { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.w = self.w.saturating_add(rhs.w); + self.h = self.h.saturating_add(rhs.h); + } +} + +impl Sub for Size { + type Output = Size; + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Size { + w: self.w.saturating_sub(rhs.w).max(N::ZERO), + h: self.h.saturating_sub(rhs.h).max(N::ZERO), + _kind: std::marker::PhantomData, + } + } +} + +impl SubAssign for Size { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.w = self.w.saturating_sub(rhs.w).max(N::ZERO); + self.h = self.h.saturating_sub(rhs.h).max(N::ZERO); + } +} + +impl, KindLhs, KindRhs> Div> for Size { + type Output = Scale; + + #[inline] + fn div(self, rhs: Size) -> Self::Output { + Scale { + x: self.w / rhs.w, + y: self.h / rhs.h, + } + } +} + +impl Div for Size { + type Output = Size<::Output, Kind>; + + #[inline] + fn div(self, rhs: N) -> Self::Output { + Size { + w: self.w / rhs, + h: self.h / rhs, + _kind: std::marker::PhantomData, + } + } +} + +impl Mul for Size { + type Output = Size<::Output, Kind>; + + #[inline] + fn mul(self, rhs: N) -> Self::Output { + Size { + w: self.w * rhs, + h: self.h * rhs, + _kind: std::marker::PhantomData, + } + } +} + +impl Clone for Size { + #[inline] + fn clone(&self) -> Self { + Size { + w: self.w.clone(), + h: self.h.clone(), + _kind: std::marker::PhantomData, + } + } +} + +impl Copy for Size {} + +impl PartialEq for Size { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.w == other.w && self.h == other.h + } +} + +impl Eq for Size {} + +impl Default for Size { + #[inline] + fn default() -> Self { + Size { + w: N::default(), + h: N::default(), + _kind: std::marker::PhantomData, + } + } +} + +impl Add> for Point { + type Output = Point; + #[inline] + fn add(self, other: Size) -> Point { + Point { + x: self.x.saturating_add(other.w), + y: self.y.saturating_add(other.h), + _kind: std::marker::PhantomData, + } + } +} + +impl Sub> for Point { + type Output = Point; + #[inline] + fn sub(self, other: Size) -> Point { + Point { + x: self.x.saturating_sub(other.w), + y: self.y.saturating_sub(other.h), + _kind: std::marker::PhantomData, + } + } +} + +/// A rectangle defined by its top-left corner and dimensions +/// +/// Operations on rectangles are saturating. +#[repr(C)] +pub struct Rectangle { + /// Location of the top-left corner of the rectangle + pub loc: Point, + /// Size of the rectangle, as (width, height) + pub size: Size, +} + +impl Rectangle { + /// Convert the underlying numerical type to another + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_f64(self) -> Rectangle { + Rectangle { + loc: self.loc.to_f64(), + size: self.size.to_f64(), + } + } +} + +impl Rectangle { + /// Upscale this [`Rectangle`] by the supplied [`Scale`] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn upscale(self, scale: impl Into>) -> Rectangle { + let scale = scale.into(); + Rectangle { + loc: self.loc.upscale(scale), + size: self.size.upscale(scale), + } + } + + /// Downscale this [`Rectangle`] by the supplied [`Scale`] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn downscale(self, scale: impl Into>) -> Rectangle { + let scale = scale.into(); + Rectangle { + loc: self.loc.downscale(scale), + size: self.size.downscale(scale), + } + } + + /// Check if this [`Rectangle`] is empty + /// + /// Returns true if either the width or the height + /// of the [`Size`] is zero + pub fn is_empty(&self) -> bool { + self.size.is_empty() + } +} + +impl Rectangle { + /// Convert to i32 for integer-space manipulations by rounding float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_round(self) -> Rectangle { + Rectangle { + loc: self.loc.to_i32_round(), + size: self.size.to_i32_round(), + } + } + + /// Convert to i32 by returning the largest integer-space rectangle fitting into the float-based rectangle + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_down(self) -> Rectangle { + Rectangle::from_extremities(self.loc.to_i32_ceil(), (self.loc + self.size).to_i32_floor()) + } + + /// Convert to i32 by returning the smallest integet-space rectangle encapsulating the float-based rectangle + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_up(self) -> Rectangle { + Rectangle::from_extremities(self.loc.to_i32_floor(), (self.loc + self.size).to_i32_ceil()) + } +} + +impl Rectangle { + /// Create a new [`Rectangle`] from the coordinates of its top-left corner and its dimensions + #[deprecated(note = "use new or from_size")] + #[inline] + pub fn from_loc_and_size(loc: impl Into>, size: impl Into>) -> Self { + Rectangle { + loc: loc.into(), + size: size.into(), + } + } + + /// Create a new [`Rectangle`] from the coordinates of its top-left corner and its dimensions + #[inline] + pub fn new(loc: Point, size: Size) -> Self { + Rectangle { loc, size } + } + + /// Create a new [`Rectangle`] from its dimensions, with location zero + #[inline] + pub fn from_size(size: Size) -> Self { + Rectangle { + loc: (N::ZERO, N::ZERO).into(), + size, + } + } + + /// Create a new [`Rectangle`] with location and size zero + #[inline] + pub fn zero() -> Self { + Rectangle { + loc: (N::ZERO, N::ZERO).into(), + size: (N::ZERO, N::ZERO).into(), + } + } + + /// Create a new [`Rectangle`] from the coordinates of its top-left corner and its bottom-right corner + #[inline] + #[deprecated = "use Rectangle::from_extremities instead"] + #[doc(hidden)] + pub fn from_extemities( + topleft: impl Into>, + bottomright: impl Into>, + ) -> Self { + Rectangle::from_extremities(topleft, bottomright) + } + + /// Create a new [`Rectangle`] from the coordinates of its top-left corner and its bottom-right corner + #[inline] + pub fn from_extremities( + topleft: impl Into>, + bottomright: impl Into>, + ) -> Self { + let topleft = topleft.into(); + let bottomright = bottomright.into(); + Rectangle { + loc: topleft, + size: (bottomright - topleft).to_size(), + } + } + + /// Checks whether given [`Point`] is inside the rectangle + #[inline] + pub fn contains>>(self, point: P) -> bool { + let p: Point = point.into(); + (p.x >= self.loc.x) + && (p.x < self.loc.x.saturating_add(self.size.w)) + && (p.y >= self.loc.y) + && (p.y < self.loc.y.saturating_add(self.size.h)) + } + + /// Checks whether given [`Rectangle`] is inside the rectangle + /// + /// A rectangle is considered inside another rectangle + /// if its location is inside the other rectangle and it does not + /// extend outside the other rectangle. + /// This includes rectangles with the same location and size + #[inline] + pub fn contains_rect>>(self, rect: R) -> bool { + let r: Rectangle = rect.into(); + r.loc.x >= self.loc.x + && r.loc.y >= self.loc.y + && r.loc.x.saturating_add(r.size.w) <= self.loc.x.saturating_add(self.size.w) + && r.loc.y.saturating_add(r.size.h) <= self.loc.y.saturating_add(self.size.h) + } + + /// Checks whether a given [`Rectangle`] overlaps with this one + /// + /// Note: This operation is exclusive, touching only rectangles will return `false`. + /// For inclusive overlap test see [`overlaps_or_touches`](Rectangle::overlaps_or_touches) + #[inline] + pub fn overlaps(self, other: impl Into>) -> bool { + let other = other.into(); + + self.loc.x < other.loc.x.saturating_add(other.size.w) + && other.loc.x < self.loc.x.saturating_add(self.size.w) + && self.loc.y < other.loc.y.saturating_add(other.size.h) + && other.loc.y < self.loc.y.saturating_add(self.size.h) + } + + /// Checks whether a given [`Rectangle`] overlaps with this one or touches it + /// + /// Note: This operation is inclusive, touching only rectangles will return `true`. + /// For exclusive overlap test see [`overlaps`](Rectangle::overlaps) + #[inline] + pub fn overlaps_or_touches(self, other: impl Into>) -> bool { + let other = other.into(); + + self.loc.x <= other.loc.x.saturating_add(other.size.w) + && other.loc.x <= self.loc.x.saturating_add(self.size.w) + && self.loc.y <= other.loc.y.saturating_add(other.size.h) + && other.loc.y <= self.loc.y.saturating_add(self.size.h) + } + + /// Clamp rectangle to min and max corners resulting in the overlapping area of two rectangles + /// + /// Returns `None` if the two rectangles don't overlap + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn intersection(self, other: impl Into>) -> Option { + let other = other.into(); + if !self.overlaps(other) { + return None; + } + Some(Rectangle::from_extremities( + (self.loc.x.max(other.loc.x), self.loc.y.max(other.loc.y)), + ( + (self.loc.x.saturating_add(self.size.w)).min(other.loc.x.saturating_add(other.size.w)), + (self.loc.y.saturating_add(self.size.h)).min(other.loc.y.saturating_add(other.size.h)), + ), + )) + } + + /// Compute the bounding box of a given set of points + pub fn bounding_box(points: impl IntoIterator>) -> Self { + let ret = points.into_iter().fold(None, |acc, point| match acc { + None => Some((point, point)), + Some((min_point, max_point)) => Some(( + (point.x.min(min_point.x), point.y.min(min_point.y)).into(), + (point.x.max(max_point.x), point.y.max(max_point.y)).into(), + )), + }); + + match ret { + None => Rectangle::default(), + Some((min_point, max_point)) => Rectangle::from_extremities(min_point, max_point), + } + } + + /// Merge two [`Rectangle`] by producing the smallest rectangle that contains both + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn merge(self, other: Self) -> Self { + Self::bounding_box([self.loc, self.loc + self.size, other.loc, other.loc + other.size]) + } + + /// Subtract another [`Rectangle`] from this [`Rectangle`] + /// + /// If the rectangles to not overlap the original rectangle will + /// be returned. + /// If the other rectangle contains self no rectangle will be returned, + /// otherwise up to 4 rectangles will be returned. + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn subtract_rect(self, other: Self) -> Vec { + self.subtract_rects([other]) + } + + /// Subtract a set of [`Rectangle`]s from this [`Rectangle`] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn subtract_rects(self, others: impl IntoIterator) -> Vec { + let mut remaining = Vec::with_capacity(4); + remaining.push(self); + Self::subtract_rects_many_in_place(remaining, others) + } + + /// Subtract a set of [`Rectangle`]s from a set [`Rectangle`]s + #[must_use = "to modify the results in-place, use `subtract_rects_many_in_place`"] + pub fn subtract_rects_many( + rects: impl IntoIterator, + others: impl IntoIterator, + ) -> Vec { + let remaining = rects.into_iter().collect::>(); + Self::subtract_rects_many_in_place(remaining, others) + } + + /// Subtract a set of [`Rectangle`]s from a set [`Rectangle`]s in-place + pub fn subtract_rects_many_in_place( + mut rects: Vec, + others: impl IntoIterator, + ) -> Vec { + for other in others { + let items = rects.len(); + let mut checked = 0usize; + let mut index = 0usize; + + // If there is nothing left we can stop, + // we won't be able to subtract any further + if items == 0 { + return rects; + } + + while checked != items { + checked += 1; + + // If there is no overlap there is nothing to subtract + let Some(intersection) = rects[index].intersection(other) else { + index += 1; + continue; + }; + + // We now know that we have to subtract the other rect + let item = rects.swap_remove(index); + + // If we are completely contained then nothing is left + if other.contains_rect(item) { + continue; + } + + let top_rect = Rectangle::new( + item.loc, + (item.size.w, intersection.loc.y.saturating_sub(item.loc.y)).into(), + ); + let left_rect: Rectangle = Rectangle::new( + (item.loc.x, intersection.loc.y).into(), + (intersection.loc.x.saturating_sub(item.loc.x), intersection.size.h).into(), + ); + let right_rect: Rectangle = Rectangle::new( + ( + intersection.loc.x.saturating_add(intersection.size.w), + intersection.loc.y, + ) + .into(), + ( + (item.loc.x.saturating_add(item.size.w)) + .saturating_sub(intersection.loc.x.saturating_add(intersection.size.w)), + intersection.size.h, + ) + .into(), + ); + let bottom_rect: Rectangle = Rectangle::new( + (item.loc.x, intersection.loc.y.saturating_add(intersection.size.h)).into(), + ( + item.size.w, + (item.loc.y.saturating_add(item.size.h)) + .saturating_sub(intersection.loc.y.saturating_add(intersection.size.h)), + ) + .into(), + ); + + if !top_rect.is_empty() { + rects.push(top_rect); + } + + if !left_rect.is_empty() { + rects.push(left_rect); + } + + if !right_rect.is_empty() { + rects.push(right_rect); + } + + if !bottom_rect.is_empty() { + rects.push(bottom_rect); + } + } + } + + rects + } +} + +impl Rectangle { + #[inline] + #[cfg(feature = "xwayland")] + pub(crate) fn to_client(self, scale: impl Into>) -> Rectangle { + let scale = scale.into(); + Rectangle { + loc: self.loc.to_client(scale), + size: self.size.to_client(scale), + } + } + + #[inline] + #[cfg(feature = "xwayland")] + pub(crate) fn to_client_precise_round( + self, + scale: impl Into>, + ) -> Rectangle { + self.to_f64().to_client(scale.into().to_f64()).to_i32_round() + } + + /// Convert this logical rectangle to physical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical(self, scale: impl Into>) -> Rectangle { + let scale = scale.into(); + Rectangle { + loc: self.loc.to_physical(scale), + size: self.size.to_physical(scale), + } + } + + /// Convert this logical rectangle to physical coordinate space according to given scale factor + /// and round the result + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_round( + self, + scale: impl Into>, + ) -> Rectangle { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_round() + } + + /// Convert this logical rectangle to physical coordinate space according to given scale factor, + /// returning the largest N-space rectangle fitting into the N-based rectangle + /// + /// This will ceil the location and floor the size after applying the scale + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_down( + &self, + scale: impl Into>, + ) -> Rectangle { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_down() + } + + /// Convert this logical rectangle to physical coordinate space according to given scale factor, + /// returning the smallest N-space rectangle encapsulating the N-based rectangle + /// + /// This will floor the location and ceil the size after applying the scale + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_physical_precise_up( + &self, + scale: impl Into>, + ) -> Rectangle { + self.to_f64().to_physical(scale.into().to_f64()).to_i32_up() + } + + /// Convert this logical rectangle to buffer coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_buffer( + self, + scale: impl Into>, + transformation: Transform, + area: &Size, + ) -> Rectangle { + let rect = transformation.transform_rect_in(self, area); + let scale = scale.into(); + Rectangle { + loc: Point { + x: rect.loc.x.upscale(scale.x), + y: rect.loc.y.upscale(scale.y), + _kind: std::marker::PhantomData, + }, + size: Size { + w: rect.size.w.upscale(scale.x), + h: rect.size.h.upscale(scale.y), + _kind: std::marker::PhantomData, + }, + } + } +} + +#[cfg(feature = "wayland_frontend")] +impl Rectangle { + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub(crate) fn to_logical(self, scale: impl Into>) -> Rectangle { + let scale = scale.into(); + Rectangle { + loc: self.loc.to_logical(scale), + size: self.size.to_logical(scale), + } + } +} + +impl Rectangle { + /// Convert this physical rectangle to logical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_logical(self, scale: impl Into>) -> Rectangle { + let scale = scale.into(); + Rectangle { + loc: self.loc.to_logical(scale), + size: self.size.to_logical(scale), + } + } +} + +impl Rectangle { + /// Convert this physical rectangle to logical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_logical( + self, + scale: impl Into>, + transformation: Transform, + area: &Size, + ) -> Rectangle { + let rect = transformation.invert().transform_rect_in(self, area); + let scale = scale.into(); + Rectangle { + loc: Point { + x: rect.loc.x.downscale(scale.x), + y: rect.loc.y.downscale(scale.y), + _kind: std::marker::PhantomData, + }, + size: Size { + w: rect.size.w.downscale(scale.x), + h: rect.size.h.downscale(scale.y), + _kind: std::marker::PhantomData, + }, + } + } +} + +impl fmt::Debug for Rectangle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("Rectangle<{}>", std::any::type_name::()))?; + f.debug_struct("") + .field("x", &self.loc.x) + .field("y", &self.loc.y) + .field("width", &self.size.w) + .field("height", &self.size.h) + .finish() + } +} + +impl Clone for Rectangle { + #[inline] + fn clone(&self) -> Self { + Rectangle { + loc: self.loc.clone(), + size: self.size.clone(), + } + } +} + +impl Copy for Rectangle {} + +impl PartialEq for Rectangle { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.loc == other.loc && self.size == other.size + } +} + +impl Eq for Rectangle {} + +impl Default for Rectangle { + #[inline] + fn default() -> Self { + Rectangle { + loc: Default::default(), + size: Default::default(), + } + } +} + +/// Extra pixels around a window/surface, such as for a decorations frame or drop shadow +#[repr(C)] +pub struct FrameExtents { + /// Frame pixels on the left + pub left: N, + /// Frame pixels on the right + pub right: N, + /// Frame pixels on top + pub top: N, + /// Frame pixels on the bottom + pub bottom: N, + _kind: std::marker::PhantomData, +} + +impl FrameExtents { + /// Create a new FrameExtents + pub const fn new(left: N, right: N, top: N, bottom: N) -> Self { + Self { + left, + right, + top, + bottom, + _kind: std::marker::PhantomData, + } + } +} + +impl FrameExtents { + /// Convert the underlying numerical type to f64 for floating point manipulations + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_f64(self) -> FrameExtents { + FrameExtents { + left: self.left.to_f64(), + right: self.right.to_f64(), + top: self.top.to_f64(), + bottom: self.bottom.to_f64(), + _kind: std::marker::PhantomData, + } + } +} + +impl FrameExtents { + /// Convert this physical rectangle to logical coordinate space according to given scale factor + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_logical(&self, scale: impl Into>) -> FrameExtents { + let scale = scale.into(); + FrameExtents { + left: self.left.downscale(scale.x), + right: self.right.downscale(scale.x), + top: self.top.downscale(scale.y), + bottom: self.bottom.downscale(scale.y), + _kind: std::marker::PhantomData, + } + } +} + +impl FrameExtents { + /// Convert to i32 for integer-space manipulations by rounding float values + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn to_i32_round(self) -> FrameExtents { + FrameExtents { + left: N::from_f64(self.left.round()), + right: N::from_f64(self.right.round()), + top: N::from_f64(self.top.round()), + bottom: N::from_f64(self.bottom.round()), + _kind: std::marker::PhantomData, + } + } +} + +impl Default for FrameExtents { + #[inline] + fn default() -> Self { + Self { + left: N::default(), + right: N::default(), + top: N::default(), + bottom: N::default(), + _kind: std::marker::PhantomData, + } + } +} + +impl fmt::Debug for FrameExtents { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("FrameExtents<{}>", std::any::type_name::()))?; + f.debug_struct("") + .field("left", &self.left) + .field("right", &self.right) + .field("top", &self.top) + .field("bottom", &self.bottom) + .finish() + } +} + +impl Clone for FrameExtents { + #[inline] + fn clone(&self) -> Self { + Self { + left: self.left.clone(), + right: self.right.clone(), + top: self.top.clone(), + bottom: self.bottom.clone(), + _kind: std::marker::PhantomData, + } + } +} + +impl Copy for FrameExtents {} + +impl PartialEq for FrameExtents { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.left == other.left + && self.right == other.right + && self.top == other.top + && self.bottom == other.bottom + } +} + +impl Eq for FrameExtents {} + +impl Add> for Rectangle { + type Output = Rectangle; + + fn add(self, rhs: FrameExtents) -> Self::Output { + Rectangle::new( + (self.loc.x - rhs.left, self.loc.y - rhs.top).into(), + ( + (self.size.w + rhs.left + rhs.right).max(N::ZERO), + (self.size.h + rhs.top + rhs.bottom).max(N::ZERO), + ) + .into(), + ) + } +} + +impl Sub> for Rectangle { + type Output = Rectangle; + + fn sub(self, rhs: FrameExtents) -> Self::Output { + Rectangle::new( + (self.loc.x + rhs.left, self.loc.y + rhs.top).into(), + ( + (self.size.w - rhs.left - rhs.right).max(N::ZERO), + (self.size.h - rhs.top - rhs.bottom).max(N::ZERO), + ) + .into(), + ) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +/// Possible transformations to two-dimensional planes +#[derive(Default)] +pub enum Transform { + /// Identity transformation (plane is unaltered when applied) + #[default] + Normal, + /// Plane is rotated by 90 degrees + _90, + /// Plane is rotated by 180 degrees + _180, + /// Plane is rotated by 270 degrees + _270, + /// Plane is flipped vertically + Flipped, + /// Plane is flipped vertically and rotated by 90 degrees + Flipped90, + /// Plane is flipped vertically and rotated by 180 degrees + Flipped180, + /// Plane is flipped vertically and rotated by 270 degrees + Flipped270, +} + +impl Transform { + /// Inverts any 90-degree transformation into 270-degree transformations and vise versa. + /// + /// Flipping is preserved and 180/Normal transformation are uneffected. + #[inline] + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn invert(&self) -> Transform { + match self { + Transform::Normal => Transform::Normal, + Transform::Flipped => Transform::Flipped, + Transform::_90 => Transform::_270, + Transform::_180 => Transform::_180, + Transform::_270 => Transform::_90, + Transform::Flipped90 => Transform::Flipped270, + Transform::Flipped180 => Transform::Flipped180, + Transform::Flipped270 => Transform::Flipped90, + } + } + + /// Transforms a point inside an area of a given size by applying this transformation. + pub fn transform_point_in( + &self, + point: Point, + area: &Size, + ) -> Point { + match *self { + Transform::Normal => point, + Transform::_90 => (area.h - point.y, point.x).into(), + Transform::_180 => (area.w - point.x, area.h - point.y).into(), + Transform::_270 => (point.y, area.w - point.x).into(), + Transform::Flipped => (area.w - point.x, point.y).into(), + Transform::Flipped90 => (point.y, point.x).into(), + Transform::Flipped180 => (point.x, area.h - point.y).into(), + Transform::Flipped270 => (area.h - point.y, area.w - point.x).into(), + } + } + + /// Transformed size after applying this transformation. + pub fn transform_size(&self, size: Size) -> Size { + if *self == Transform::_90 + || *self == Transform::_270 + || *self == Transform::Flipped90 + || *self == Transform::Flipped270 + { + (size.h, size.w).into() + } else { + size + } + } + + /// Transforms a rectangle inside an area of a given size by applying this transformation. + pub fn transform_rect_in( + &self, + rect: Rectangle, + area: &Size, + ) -> Rectangle { + let size = self.transform_size(rect.size); + + let loc = match *self { + Transform::Normal => rect.loc, + Transform::_90 => (area.h - rect.loc.y - rect.size.h, rect.loc.x).into(), + Transform::_180 => ( + area.w - rect.loc.x - rect.size.w, + area.h - rect.loc.y - rect.size.h, + ) + .into(), + Transform::_270 => (rect.loc.y, area.w - rect.loc.x - rect.size.w).into(), + Transform::Flipped => (area.w - rect.loc.x - rect.size.w, rect.loc.y).into(), + Transform::Flipped90 => ( + area.h - rect.loc.y - rect.size.h, + area.w - rect.loc.x - rect.size.w, + ) + .into(), + Transform::Flipped180 => (rect.loc.x, area.h - rect.loc.y - rect.size.h).into(), + Transform::Flipped270 => (rect.loc.y, rect.loc.x).into(), + }; + + Rectangle::new(loc, size) + } + + /// Returns true if the transformation would flip contents + pub fn flipped(&self) -> bool { + !matches!( + self, + Transform::Normal | Transform::_90 | Transform::_180 | Transform::_270 + ) + } + + /// Returns the angle (in degrees) of the transformation + #[inline] + pub fn degrees(&self) -> u32 { + match self { + Transform::Normal | Transform::Flipped => 0, + Transform::_90 | Transform::Flipped90 => 90, + Transform::_180 | Transform::Flipped180 => 180, + Transform::_270 | Transform::Flipped270 => 270, + } + } +} + +impl std::ops::Add for Transform { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + let flipped = matches!((self.flipped(), other.flipped()), (true, false) | (false, true)); + let degrees = (self.degrees() + other.degrees()) % 360; + match (flipped, degrees) { + (false, 0) => Transform::Normal, + (false, 90) => Transform::_90, + (false, 180) => Transform::_180, + (false, 270) => Transform::_270, + (true, 0) => Transform::Flipped, + (true, 90) => Transform::Flipped90, + (true, 180) => Transform::Flipped180, + (true, 270) => Transform::Flipped270, + _ => unreachable!(), + } + } +} + +#[cfg(feature = "wayland_frontend")] +impl From for WlTransform { + #[inline] + fn from(transform: Transform) -> Self { + match transform { + Transform::Normal => WlTransform::Normal, + Transform::_90 => WlTransform::_90, + Transform::_180 => WlTransform::_180, + Transform::_270 => WlTransform::_270, + Transform::Flipped => WlTransform::Flipped, + Transform::Flipped90 => WlTransform::Flipped90, + Transform::Flipped180 => WlTransform::Flipped180, + Transform::Flipped270 => WlTransform::Flipped270, + } + } +} + +#[cfg(test)] +mod tests { + use super::{Logical, Rectangle, Size, Transform}; + + #[test] + fn transform_rect_ident() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 90)); + let transform = Transform::Normal; + + assert_eq!(rect, transform.transform_rect_in(rect, &size)) + } + + #[test] + fn transform_rect_90() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 90)); + let transform = Transform::_90; + + assert_eq!( + Rectangle::new((30, 10).into(), (40, 30).into()), + transform.transform_rect_in(rect, &size) + ) + } + + #[test] + fn transform_rect_180() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 90)); + let transform = Transform::_180; + + assert_eq!( + Rectangle::new((30, 30).into(), (30, 40).into()), + transform.transform_rect_in(rect, &size) + ) + } + + #[test] + fn transform_rect_270() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 90)); + let transform = Transform::_270; + + assert_eq!( + Rectangle::new((20, 30).into(), (40, 30).into()), + transform.transform_rect_in(rect, &size) + ) + } + + #[test] + fn transform_rect_f() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 90)); + let transform = Transform::Flipped; + + assert_eq!( + Rectangle::new((30, 20).into(), (30, 40).into()), + transform.transform_rect_in(rect, &size) + ) + } + + #[test] + fn transform_rect_f90() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 80)); + let transform = Transform::Flipped90; + + assert_eq!( + Rectangle::new((20, 30).into(), (40, 30).into()), + transform.transform_rect_in(rect, &size) + ) + } + + #[test] + fn transform_rect_f180() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 90)); + let transform = Transform::Flipped180; + + assert_eq!( + Rectangle::new((10, 30).into(), (30, 40).into()), + transform.transform_rect_in(rect, &size) + ) + } + + #[test] + fn transform_rect_f270() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + let size = Size::from((70, 90)); + let transform = Transform::Flipped270; + + assert_eq!( + Rectangle::new((20, 10).into(), (40, 30).into()), + transform.transform_rect_in(rect, &size) + ) + } + + #[test] + fn rectangle_contains_rect_itself() { + let rect = Rectangle::::new((10, 20).into(), (30, 40).into()); + assert!(rect.contains_rect(rect)); + } + + #[test] + fn rectangle_contains_rect_outside() { + let first = Rectangle::::new((10, 20).into(), (30, 40).into()); + let second = Rectangle::::new((41, 61).into(), (30, 40).into()); + assert!(!first.contains_rect(second)); + } + + #[test] + fn rectangle_contains_rect_extends() { + let first = Rectangle::::new((10, 20).into(), (30, 40).into()); + let second = Rectangle::::new((10, 20).into(), (30, 45).into()); + assert!(!first.contains_rect(second)); + } + + #[test] + fn rectangle_subtract_full() { + let outer = Rectangle::::from_size((100, 100).into()); + let inner = Rectangle::::new((-10, -10).into(), (1000, 1000).into()); + + let rects = outer.subtract_rect(inner); + assert_eq!(rects, vec![]) + } + + #[test] + fn rectangle_subtract_center_hole() { + let outer = Rectangle::::from_size((100, 100).into()); + let inner = Rectangle::::new((10, 10).into(), (80, 80).into()); + + let rects = outer.subtract_rect(inner); + assert_eq!( + rects, + vec![ + // Top rect + Rectangle::::from_size((100, 10).into()), + // Left rect + Rectangle::::new((0, 10).into(), (10, 80).into()), + // Right rect + Rectangle::::new((90, 10).into(), (10, 80).into()), + // Bottom rect + Rectangle::::new((0, 90).into(), (100, 10).into()), + ] + ) + } + + #[test] + fn rectangle_subtract_full_top() { + let outer = Rectangle::::from_size((100, 100).into()); + let inner = Rectangle::::new((0, -20).into(), (100, 100).into()); + + let rects = outer.subtract_rect(inner); + assert_eq!( + rects, + vec![ + // Bottom rect + Rectangle::::new((0, 80).into(), (100, 20).into()), + ] + ) + } + + #[test] + fn rectangle_subtract_full_bottom() { + let outer = Rectangle::::from_size((100, 100).into()); + let inner = Rectangle::::new((0, 20).into(), (100, 100).into()); + + let rects = outer.subtract_rect(inner); + assert_eq!( + rects, + vec![ + // Top rect + Rectangle::::from_size((100, 20).into()), + ] + ) + } + + #[test] + fn rectangle_subtract_full_left() { + let outer = Rectangle::::from_size((100, 100).into()); + let inner = Rectangle::::new((-20, 0).into(), (100, 100).into()); + + let rects = outer.subtract_rect(inner); + assert_eq!( + rects, + vec![ + // Right rect + Rectangle::::new((80, 0).into(), (20, 100).into()), + ] + ) + } + + #[test] + fn rectangle_subtract_full_right() { + let outer = Rectangle::::from_size((100, 100).into()); + let inner = Rectangle::::new((20, 0).into(), (100, 100).into()); + + let rects = outer.subtract_rect(inner); + assert_eq!( + rects, + vec![ + // Left rect + Rectangle::::from_size((20, 100).into()), + ] + ) + } + + #[test] + fn rectangle_overlaps_or_touches_top() { + let top = Rectangle::::new((0, -24).into(), (800, 24).into()); + let main = Rectangle::::from_size((800, 600).into()); + assert!(main.overlaps_or_touches(top)); + } + + #[test] + fn rectangle_overlaps_or_touches_left() { + let left = Rectangle::::new((-4, -24).into(), (4, 624).into()); + let main = Rectangle::::from_size((800, 600).into()); + assert!(main.overlaps_or_touches(left)); + } + + #[test] + fn rectangle_overlaps_or_touches_right() { + let right = Rectangle::::new((800, -24).into(), (4, 624).into()); + let main = Rectangle::::from_size((800, 600).into()); + assert!(main.overlaps_or_touches(right)); + } + + #[test] + fn rectangle_no_overlap_top() { + let top = Rectangle::::new((0, -24).into(), (800, 24).into()); + let main = Rectangle::::from_size((800, 600).into()); + assert!(!main.overlaps(top)); + } + + #[test] + fn rectangle_no_overlap_left() { + let left = Rectangle::::new((-4, -24).into(), (4, 624).into()); + let main = Rectangle::::from_size((800, 600).into()); + assert!(!main.overlaps(left)); + } + + #[test] + fn rectangle_no_overlap_right() { + let right = Rectangle::::new((800, -24).into(), (4, 624).into()); + let main = Rectangle::::from_size((800, 600).into()); + assert!(!main.overlaps(right)); + } + + #[test] + fn size_sub_saturate_zero() { + let bigger = Size::<_, Logical>::from((10, 10)); + let mut smaller = Size::<_, Logical>::from((5, 5)); + + assert_eq!(smaller - bigger, Size::from((0, 0))); + smaller -= bigger; + assert_eq!(smaller, Size::from((0, 0))); + } +} diff --git a/vendor/smithay/src/utils/hook.rs b/vendor/smithay/src/utils/hook.rs new file mode 100644 index 0000000..72ac9bc --- /dev/null +++ b/vendor/smithay/src/utils/hook.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +crate::utils::ids::id_gen!(hooks_id); + +/// Unique hook identifier used to unregister commit/descruction hooks +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct HookId(Arc); + +pub(crate) struct Hook { + pub id: HookId, + pub cb: Arc, +} + +impl std::fmt::Debug for Hook { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Hook") + .field("id", &self.id) + .finish_non_exhaustive() + } +} + +impl Clone for Hook { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + cb: self.cb.clone(), + } + } +} + +impl Hook { + pub fn new(cb: Arc) -> Self { + Self { + id: HookId(Arc::new(InnerId::new())), + cb, + } + } +} + +#[derive(Debug, Eq, PartialEq)] +struct InnerId(usize); + +impl InnerId { + fn new() -> Self { + Self(hooks_id::next()) + } +} + +impl Drop for InnerId { + fn drop(&mut self) { + hooks_id::remove(self.0); + } +} diff --git a/vendor/smithay/src/utils/ids.rs b/vendor/smithay/src/utils/ids.rs new file mode 100644 index 0000000..745664a --- /dev/null +++ b/vendor/smithay/src/utils/ids.rs @@ -0,0 +1,36 @@ +macro_rules! id_gen { + ($mod_name:ident) => { + mod $mod_name { + use std::{ + collections::HashSet, + sync::{LazyLock, Mutex}, + }; + + static ID_DATA: LazyLock, usize)>> = + LazyLock::new(|| Mutex::new((HashSet::new(), 0))); + + pub(crate) fn next() -> usize { + let (id_set, counter) = &mut *ID_DATA.lock().unwrap(); + + if id_set.len() == usize::MAX { + panic!("Out of ids"); + } + + while !id_set.insert(*counter) { + *counter = counter.wrapping_add(1); + } + + let new_id = *counter; + *counter = counter.wrapping_add(1); + + new_id + } + + pub(crate) fn remove(id: usize) -> bool { + ID_DATA.lock().unwrap().0.remove(&id) + } + } + }; +} + +pub(crate) use id_gen; diff --git a/vendor/smithay/src/utils/iter.rs b/vendor/smithay/src/utils/iter.rs new file mode 100644 index 0000000..eb9185d --- /dev/null +++ b/vendor/smithay/src/utils/iter.rs @@ -0,0 +1,71 @@ +/// Common iterator types +use wayland_server::{Resource, Weak, backend::ClientId}; + +use std::{fmt, sync::MutexGuard}; + +/// Iterator helper over a mutex of client objects +pub struct LockedClientObjsIter<'a, T: 'static, G, F> { + pub iterator: std::iter::FilterMap>, F>, + pub guard: MutexGuard<'a, G>, +} + +pub(crate) fn new_locked_obj_iter_from_vec( + guard: MutexGuard<'_, Vec>>, + client: ClientId, +) -> impl Iterator + '_ { + new_locked_obj_iter(guard, client, |guard| guard.iter()) +} + +pub(crate) fn new_locked_obj_iter< + 'a, + T: Resource + 'static, + G, + F: for<'b> FnOnce(&'b G) -> std::slice::Iter<'b, Weak>, +>( + guard: MutexGuard<'a, G>, + client: ClientId, + iterator_fn: F, +) -> impl Iterator + 'a { + let iterator = unsafe { + std::mem::transmute::>, std::slice::Iter<'static, Weak>>(iterator_fn( + &*guard, + )) + }; + + let iterator = iterator.filter_map(move |p| { + let client = &client; + p.upgrade() + .ok() + .filter(|p| p.client().is_some_and(|c| c.id() == *client)) + }); + + LockedClientObjsIter::<'a, T, G, _>::new_internal(iterator, guard) +} + +impl<'a, T, G, F> LockedClientObjsIter<'a, T, G, F> { + fn new_internal( + iterator: std::iter::FilterMap>, F>, + guard: MutexGuard<'a, G>, + ) -> Self { + LockedClientObjsIter { iterator, guard } + } +} + +impl Iterator for LockedClientObjsIter<'_, T, G, F> +where + F: FnMut(&'static Weak) -> Option, +{ + type Item = T; + + fn next(&mut self) -> Option { + self.iterator.next() + } +} + +impl fmt::Debug for LockedClientObjsIter<'_, T, G, F> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LockedClientObjsIter") + .field("inner", &self.guard) + .finish() + } +} diff --git a/vendor/smithay/src/utils/mod.rs b/vendor/smithay/src/utils/mod.rs new file mode 100644 index 0000000..2827315 --- /dev/null +++ b/vendor/smithay/src/utils/mod.rs @@ -0,0 +1,63 @@ +//! Various utilities functions and types + +mod geometry; +pub mod signaling; + +#[cfg(feature = "x11rb_event_source")] +pub mod x11rb; + +pub(crate) mod ids; +pub mod user_data; + +pub(crate) mod alive_tracker; +pub use self::alive_tracker::IsAlive; + +#[cfg(feature = "wayland_frontend")] +pub(crate) mod iter; + +mod fd; +pub use fd::*; + +mod sealed_file; +pub use sealed_file::SealedFile; + +#[cfg(feature = "wayland_frontend")] +pub(crate) use self::geometry::Client; +pub use self::geometry::{ + Buffer, Coordinate, FrameExtents, Logical, Physical, Point, Raw, Rectangle, Scale, Size, Transform, +}; + +mod serial; +pub use serial::*; + +mod clock; +pub use clock::*; + +#[cfg(feature = "wayland_frontend")] +pub(crate) mod hook; +#[cfg(feature = "wayland_frontend")] +pub use hook::HookId; + +/// This resource is not managed by Smithay +#[derive(Debug)] +pub struct UnmanagedResource; + +impl std::fmt::Display for UnmanagedResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("This resource is not managed by Smithay.") + } +} + +impl std::error::Error for UnmanagedResource {} + +/// This resource has been destroyed and can no longer be used. +#[derive(Debug)] +pub struct DeadResource; + +impl std::fmt::Display for DeadResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("This resource has been destroyed and can no longer be used.") + } +} + +impl std::error::Error for DeadResource {} diff --git a/vendor/smithay/src/utils/sealed_file.rs b/vendor/smithay/src/utils/sealed_file.rs new file mode 100644 index 0000000..2fc478f --- /dev/null +++ b/vendor/smithay/src/utils/sealed_file.rs @@ -0,0 +1,114 @@ +use std::{ + ffi::CStr, + fs::File, + io::Write, + os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}, +}; + +/// A file whose fd cannot be written by other processes +/// +/// This mechanism is useful for giving clients access to large amounts of +/// information such as keymaps without them being able to write to the handle. +/// +/// On Linux, Android, and FreeBSD, this uses a sealed memfd. On other platforms +/// it creates a POSIX shared memory object with `shm_open`, opens a read-only +/// copy, and unlinks it. +#[derive(Debug)] +pub struct SealedFile { + file: File, + size: usize, +} + +impl SealedFile { + /// Create a `[SealedFile]` with the given nul-terminated C string. + pub fn with_content(name: &CStr, contents: &CStr) -> Result { + Self::with_data(name, contents.to_bytes_with_nul()) + } + + /// Create a `[SealedFile]` with the given binary data. + #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))] + pub fn with_data(name: &CStr, data: &[u8]) -> Result { + use rustix::fs::{MemfdFlags, SealFlags}; + use std::io::Seek; + + let fd = rustix::fs::memfd_create(name, MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING)?; + + let mut file: File = fd.into(); + file.write_all(data)?; + file.flush()?; + + file.seek(std::io::SeekFrom::Start(0))?; + + rustix::fs::fcntl_add_seals( + &file, + SealFlags::SEAL | SealFlags::SHRINK | SealFlags::GROW | SealFlags::WRITE, + )?; + + Ok(Self { + file, + size: data.len(), + }) + } + + /// Create a `[SealedFile]` with the given binary data. + #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "android")))] + pub fn with_data(name: &CStr, data: &[u8]) -> Result { + use rand::{Rng, distr::Alphanumeric}; + use rustix::{ + io::Errno, + shm::{self, Mode}, + }; + + let mut rng = rand::rng(); + + // `memfd_create` isn't available. Instead, try `shm_open` with a randomized name, and + // loop a couple times if it exists. + let mut n = 0; + let (shm_name, mut file) = loop { + let mut shm_name = name.to_bytes().to_owned(); + shm_name.push(b'-'); + shm_name.extend((0..7).map(|_| rng.sample(Alphanumeric))); + let fd = shm::open( + shm_name.as_slice(), + shm::OFlags::RDWR | shm::OFlags::CREATE | shm::OFlags::EXCL, + Mode::RWXU, + ); + if !matches!(fd, Err(Errno::EXIST)) || n > 3 { + break (shm_name, File::from(fd?)); + } + n += 1; + }; + + // Sealing isn't available, so re-open read-only. + let fd_rdonly = shm::open(shm_name.as_slice(), shm::OFlags::RDONLY, Mode::empty())?; + let file_rdonly = File::from(fd_rdonly); + + // Unlink so another process can't open shm file. + let _ = shm::unlink(shm_name.as_slice()); + + file.write_all(data)?; + file.flush()?; + + Ok(Self { + file: file_rdonly, + size: data.len(), + }) + } + + /// Size of the data contained in the sealed file. + pub fn size(&self) -> usize { + self.size + } +} + +impl AsRawFd for SealedFile { + fn as_raw_fd(&self) -> RawFd { + self.file.as_raw_fd() + } +} + +impl AsFd for SealedFile { + fn as_fd(&self) -> BorrowedFd<'_> { + self.file.as_fd() + } +} diff --git a/vendor/smithay/src/utils/serial.rs b/vendor/smithay/src/utils/serial.rs new file mode 100644 index 0000000..0370283 --- /dev/null +++ b/vendor/smithay/src/utils/serial.rs @@ -0,0 +1,135 @@ +use std::sync::atomic::{AtomicU32, Ordering}; + +/// A global [`SerialCounter`] for use in your compositor. +/// +/// Is is also used internally by some parts of Smithay. +pub static SERIAL_COUNTER: SerialCounter = SerialCounter::new(); + +/// A serial type, whose comparison takes into account the wrapping-around behavior of the +/// underlying counter. +#[derive(Hash, PartialEq, Eq, Debug, Copy, Clone)] +pub struct Serial(pub(crate) u32); + +impl PartialOrd for Serial { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + let distance = self.0.abs_diff(other.0); + if distance < u32::MAX / 2 { + self.0.partial_cmp(&other.0) + } else { + // wrap-around occurred, invert comparison + other.0.partial_cmp(&self.0) + } + } +} + +impl From for Serial { + #[inline] + fn from(n: u32) -> Self { + Serial(n) + } +} + +impl From for u32 { + #[inline] + fn from(serial: Serial) -> u32 { + serial.0 + } +} + +impl Serial { + /// Checks if a serial was generated after or is equal to another given serial + #[inline] + pub fn is_no_older_than(&self, other: &Serial) -> bool { + other <= self + } +} + +/// A counter for generating serials, for use in the client protocol +/// +/// A global instance of this counter is available as the `SERIAL_COUNTER` +/// static. It is recommended to only use this global counter to ensure the +/// uniqueness of serials. +/// +/// The counter will wrap around on overflow, ensuring it can run for as long +/// as needed. +#[derive(Debug)] +pub struct SerialCounter { + serial: AtomicU32, +} + +impl Default for SerialCounter { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl SerialCounter { + /// Create a new counter starting at `1` + pub const fn new() -> Self { + Self { + serial: AtomicU32::new(1), + } + } + + /// Retrieve the next serial from the counter + pub fn next_serial(&self) -> Serial { + let _ = self + .serial + .compare_exchange(0, 1, Ordering::AcqRel, Ordering::SeqCst); + Serial(self.serial.fetch_add(1, Ordering::AcqRel)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_serial_counter(initial_value: u32) -> SerialCounter { + SerialCounter { + serial: AtomicU32::new(initial_value), + } + } + + #[test] + #[allow(clippy::eq_op)] + fn serial_equals_self() { + let counter = create_serial_counter(0); + let serial = counter.next_serial(); + assert!(serial == serial); + } + + #[test] + fn consecutive_serials() { + let counter = create_serial_counter(0); + let serial1 = counter.next_serial(); + let serial2 = counter.next_serial(); + assert!(serial1 < serial2); + } + + #[test] + fn non_consecutive_serials() { + let skip_serials = 147; + + let counter = create_serial_counter(0); + let serial1 = counter.next_serial(); + for _ in 0..skip_serials { + let _ = counter.next_serial(); + } + let serial2 = counter.next_serial(); + assert!(serial1 < serial2); + } + + #[test] + fn serial_wrap_around() { + let counter = create_serial_counter(u32::MAX); + let serial1 = counter.next_serial(); + let serial2 = counter.next_serial(); + + assert!(serial1 == u32::MAX.into()); + assert!(serial2 == 1.into()); + + assert!(serial1 < serial2); + } +} diff --git a/vendor/smithay/src/utils/signaling.rs b/vendor/smithay/src/utils/signaling.rs new file mode 100644 index 0000000..175cb63 --- /dev/null +++ b/vendor/smithay/src/utils/signaling.rs @@ -0,0 +1,272 @@ +//! A general purpose signaling mechanism +//! +//! This mechanism allows inter-module communication, by letting your modules +//! register callbacks to listen for events generated by other modules. This +//! signaling mechanism is synchronous and non-thread safe. If you need +//! asynchronous thread safe communication, instead consider relying on channels. +//! +//! The whole mechanism is built on the [`Signaler`] type. +//! It serves both as a message sending facility and a way to register new callbacks +//! for these messages. It can be cloned and passed around between your modules with +//! `Rc`-like semantics. +//! +//! When sending a new signal with [`Signaler::signal`], the provided value `E` will +//! be made accessible as a reference `&E` to all registered callback. +//! +//! Sending a signal or registering a new callback from within a callback is supported. +//! These will however take effect after the current signal is completely delivered. +//! Ordering of sent signals and callback registration is preserved. + +use std::{ + any::Any, + cell::RefCell, + collections::VecDeque, + fmt, + rc::{Rc, Weak}, +}; + +/// A signaler, main type for signaling +#[derive(Debug)] +pub struct Signaler { + inner: Rc>, +} + +// Manual clone impl because of type parameters +impl Clone for Signaler { + fn clone(&self) -> Signaler { + Signaler { + inner: self.inner.clone(), + } + } +} + +impl Signaler { + /// Create a new signaler for given signal type + pub fn new() -> Signaler { + Signaler { + inner: Rc::new(SignalInner::new()), + } + } + + /// Register a new callback to this signaler + /// + /// This method returns a `SignalToken`, which you must keep as long + /// as you need your callback to remain in place. Dropping it will + /// disable and free your callback. If you don't plan to ever disable + /// your callback, see [`SignalToken::leak()`](./struct.SignalToken.html). + /// + /// If you register a callback from within a callback of the same Signaler, + /// the new callback will only be inserted *after* the current signal is + /// completely delivered, and thus will not receive it. + #[must_use] + pub fn register(&self, f: F) -> SignalToken { + let rc = Rc::new(RefCell::new(f)); + let weak = Rc::downgrade(&rc) as Weak>; + self.inner.insert(weak); + SignalToken { signal: rc } + } + + /// Signal the callbacks + /// + /// All registered callbacks will be invoked with a reference to the value + /// you provide here, after which that value will be dropped. + /// + /// If this method is invoked from within a callback of the same Signaler, + /// its signalling will be delayed until the current signal is completely + /// delivered and this method will return immediately. + pub fn signal(&self, signal: S) { + self.inner.send(signal); + } +} + +impl Default for Signaler { + fn default() -> Self { + Self::new() + } +} + +/// A token associated with a callback registered to a Signaler +/// +/// Dropping it will disable and drop the callback it is associated to. +/// If you don't plan to ever disable the callback, you can use the `leak` +/// method to safely get rid of this value. +#[derive(Debug)] +pub struct SignalToken { + signal: Rc, +} + +impl SignalToken { + /// Destroy the token without disabling the associated callback + pub fn leak(self) { + // leak the Rc, so that it is never deallocated + let _ = Rc::into_raw(self.signal); + } +} + +type WeakCallback = Weak>; + +struct SignalInner { + callbacks: RefCell>>, + pending_callbacks: RefCell>>, + pending_events: RefCell>, +} + +// WeakCallback does not implement debug, so we have to impl Debug manually +impl fmt::Debug for SignalInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SignalInner") + .field("callbacks::len()", &self.callbacks.borrow().len()) + .field("pending_callbacks::len()", &self.pending_callbacks.borrow().len()) + .field("pending_events", &self.pending_events) + .finish() + } +} + +impl SignalInner { + fn new() -> SignalInner { + SignalInner { + callbacks: RefCell::new(Vec::new()), + pending_callbacks: RefCell::new(Vec::new()), + pending_events: RefCell::new(VecDeque::new()), + } + } + + fn insert(&self, weak: WeakCallback) { + // attempt to insert the new callback + if let Ok(mut guard) = self.callbacks.try_borrow_mut() { + // success, insert it + guard.push(weak); + } else { + // The callback list is already borrowed, this means that this insertion is + // done from within a callback. + // In that case, insert the callback into the pending list, `send` + // will insert it in the callback list when it is finished dispatching + // the current event. + self.pending_callbacks.borrow_mut().push(weak); + } + } + + fn send(&self, event: S) { + // insert the new event into the pending list + self.pending_events.borrow_mut().push_back(event); + // now try to dispatch the events from the pending list + // new events might be added by other callbacks in the process + // so we try to completely drain it before returning + // + // If we cannot get the guard, that means an other dispatching is + // already in progress. It'll empty the pending list, so there is + // nothing more we need to do. + if let Ok(mut guard) = self.callbacks.try_borrow_mut() { + // We cannot just use `while let` because this would keep the + // borrow of self.pending_events alive during the whole loop, rather + // than just the evaluation of the condition. :/ + loop { + let next_event = self.pending_events.borrow_mut().pop_front(); + if let Some(event) = next_event { + // Send the message, cleaning up defunct callbacks in the process + guard.retain(|weak| { + if let Some(cb) = Weak::upgrade(weak) { + (*cb.borrow_mut())(&event); + true + } else { + false + } + }); + // integrate any pending callbacks resulting from the dispatching + // of this event + guard.extend(self.pending_callbacks.borrow_mut().drain(..)); + } else { + break; + } + } + } + } +} + +/// Trait representing the capability of an object to listen for some signals +/// +/// It is provided so that the signaling system can play nicely into generic +/// constructs. +pub trait Linkable { + /// Make this object listen for signals from given signaler + fn link(&mut self, signaler: Signaler); +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::{cell::Cell, rc::Rc}; + + #[test] + fn basic_signal() { + let signaler = Signaler::::new(); + + let signaled = Rc::new(Cell::new(false)); + let signaled2 = signaled.clone(); + + let _token = signaler.register(move |_| signaled2.set(true)); + + signaler.signal(0); + + assert!(signaled.get()); + } + + #[test] + fn remove_callback() { + let signaler = Signaler::::new(); + + let token = signaler.register(|&i| assert_eq!(i, 42)); + + signaler.signal(42); + + ::std::mem::drop(token); + + signaler.signal(41); + + let _token = signaler.register(|&i| assert_eq!(i, 39)); + + signaler.signal(39); + } + + #[test] + fn delayed_signal() { + let signaler = Signaler::::new(); + + let mut signaled = false; + let sign2 = signaler.clone(); + let _token = signaler.register(move |&i| { + if !signaled { + sign2.signal(42); + signaled = true; + } else { + assert_eq!(i, 42); + } + }); + + signaler.signal(0); + } + + #[test] + fn delayed_register() { + let signaler = Signaler::::new(); + + let signaled = Rc::new(Cell::new(0u32)); + let signaled2 = signaled.clone(); + let sign2 = signaler.clone(); + + let _token1 = signaler.register(move |&original| { + signaled2.set(signaled2.get() + 1); + if original { + let signaled3 = signaled2.clone(); + sign2.register(move |_| signaled3.set(signaled3.get() + 1)).leak(); + sign2.signal(false); + } + }); + + signaler.signal(true); + + // Two rounds of signals, the first triggers 1 callback, the second triggers 2 + assert_eq!(signaled.get(), 3); + } +} diff --git a/vendor/smithay/src/utils/user_data.rs b/vendor/smithay/src/utils/user_data.rs new file mode 100644 index 0000000..6dbab82 --- /dev/null +++ b/vendor/smithay/src/utils/user_data.rs @@ -0,0 +1,387 @@ +//! Various utilities used for user data implementations + +use std::any::Any; +use std::mem::ManuallyDrop; +use std::sync::OnceLock; +use std::thread::{self, ThreadId}; + +use self::list::AppendList; + +// `UserData.get()` is called frequently, and unfortunately +// `thread::current().id()` is not very efficient to be calling every time. +#[inline] +fn current_thread_id() -> ThreadId { + thread_local! { + static ID: ThreadId = thread::current().id(); + } + ID.with(|id| *id) +} + +/// A wrapper for user data, able to store any type, and correctly +/// handling access from a wrong thread +#[derive(Debug)] +pub struct UserData { + inner: OnceLock, +} + +#[derive(Debug)] +enum UserDataInner { + ThreadSafe(Box), + NonThreadSafe(Box>, ThreadId), +} + +// UserData itself is always threadsafe, as it only gives access to its +// content if it is Send + Sync, or we are on the right thread +unsafe impl Send for UserData {} +unsafe impl Sync for UserData {} + +impl Default for UserData { + fn default() -> Self { + Self::new() + } +} + +impl UserData { + /// Create a new UserData instance + pub const fn new() -> UserData { + UserData { + inner: OnceLock::new(), + } + } + + /// Sets the UserData to a given value + /// + /// The provided closure is called to init the UserData, + /// does nothing is the UserData had already been set. + pub fn set T>(&self, f: F) { + self.inner.get_or_init(|| { + UserDataInner::NonThreadSafe(Box::new(ManuallyDrop::new(f())), current_thread_id()) + }); + } + + /// Sets the UserData to a given threadsafe value + /// + /// The provided closure is called to init the UserData, + /// does nothing is the UserData had already been set. + pub fn set_threadsafe T>(&self, f: F) { + self.inner + .get_or_init(|| UserDataInner::ThreadSafe(Box::new(f()))); + } + + /// Attempt to access the wrapped user data + /// + /// Will return `None` if either: + /// + /// - The requested type `T` does not match the type used for construction + /// - This `UserData` has been created using the non-threadsafe variant and access + /// is attempted from another thread than the one it was created on + pub fn get(&self) -> Option<&T> { + match self.inner.get() { + Some(UserDataInner::ThreadSafe(val)) => ::downcast_ref::(&**val), + Some(&UserDataInner::NonThreadSafe(ref val, threadid)) => { + // only give access if we are on the right thread + if threadid == current_thread_id() { + ::downcast_ref::(&***val) + } else { + None + } + } + None => None, + } + } +} + +impl Drop for UserData { + fn drop(&mut self) { + // only drop non-Send user data if we are on the right thread, leak it otherwise + if let Some(&mut UserDataInner::NonThreadSafe(ref mut val, threadid)) = self.inner.get_mut() { + if threadid == current_thread_id() { + unsafe { + ManuallyDrop::drop(&mut **val); + } + } + } + } +} + +/// A storage able to store several values of `UserData` +/// of different types. It behaves similarly to a `TypeMap`. +#[derive(Debug)] +pub struct UserDataMap { + list: AppendList, +} + +impl UserDataMap { + /// Create a new map + pub fn new() -> UserDataMap { + UserDataMap { + list: AppendList::new(), + } + } + + /// Attempt to access the wrapped user data of a given type + /// + /// Will return `None` if no value of type `T` is stored in this `UserDataMap` + /// and accessible from this thread + pub fn get(&self) -> Option<&T> { + for user_data in &self.list { + if let Some(val) = user_data.get::() { + return Some(val); + } + } + None + } + + /// Access the user data of a given type, initializing it if required. + pub fn get_or_insert T>(&self, init: F) -> &T { + match self.get() { + Some(data) => data, + None => { + // Insert the new node. + self.insert(init); + + // Return it again immediately. + self.list.iter().last().and_then(|data| data.get::()).unwrap() + } + } + } + + /// Access the user data of a given type, initializing it if required. + pub fn get_or_insert_threadsafe T>(&self, init: F) -> &T { + match self.get() { + Some(data) => data, + None => { + // Insert the new node. + self.insert_threadsafe(init); + + // Return it again immediately. + self.list.iter().last().and_then(|data| data.get::()).unwrap() + } + } + } + + /// Insert a value in the map if it is not already there + /// + /// This is the non-threadsafe variant, the type you insert don't have to be + /// threadsafe, but they will not be visible from other threads (even if they are + /// actually threadsafe). + /// + /// If the value does not already exist, the closure is called to create it and + /// this function returns `true`. If the value already exists, the closure is not + /// called, and this function returns `false`. + pub fn insert_if_missing T>(&self, init: F) -> bool { + if self.get::().is_some() { + return false; + } + + self.insert(init); + + true + } + + /// Insert a value in the map if it is not already there + /// + /// This is the threadsafe variant, the type you insert must be threadsafe and will + /// be visible from all threads. + /// + /// If the value does not already exist, the closure is called to create it and + /// this function returns `true`. If the value already exists, the closure is not + /// called, and this function returns `false`. + pub fn insert_if_missing_threadsafe T>(&self, init: F) -> bool { + if self.get::().is_some() { + return false; + } + + self.insert_threadsafe(init); + + true + } + + /// Insert a value into the user data. + fn insert T>(&self, init: F) { + let data = UserData::new(); + data.set(init); + self.list.append(data); + } + + /// Insert a value into the user data. + fn insert_threadsafe T>(&self, init: F) { + let data = UserData::new(); + data.set_threadsafe(init); + self.list.append(data); + } +} + +impl Default for UserDataMap { + fn default() -> UserDataMap { + UserDataMap::new() + } +} + +mod list { + /* + * This is a lock-free append-only list, it is used as an implementation + * detail of the UserDataMap. + * + * It was extracted from https://github.com/Diggsey/lockless under MIT license + * Copyright © Diggory Blake + */ + + use std::sync::atomic::{AtomicPtr, Ordering}; + use std::{mem, ptr}; + + type NodePtr = Option>>; + + #[derive(Debug)] + struct Node { + value: T, + next: AppendList, + } + + #[derive(Debug)] + pub struct AppendList(AtomicPtr>); + + impl AppendList { + fn node_into_raw(ptr: NodePtr) -> *mut Node { + match ptr { + Some(b) => Box::into_raw(b), + None => ptr::null_mut(), + } + } + unsafe fn node_from_raw(ptr: *mut Node) -> NodePtr { + if ptr.is_null() { + None + } else { + Some(unsafe { Box::from_raw(ptr) }) + } + } + + fn new_internal(ptr: NodePtr) -> Self { + AppendList(AtomicPtr::new(Self::node_into_raw(ptr))) + } + + pub fn new() -> Self { + Self::new_internal(None) + } + + pub fn append(&self, value: T) { + self.append_list(AppendList::new_internal(Some(Box::new(Node { + value, + next: AppendList::new(), + })))); + } + + unsafe fn append_ptr(&self, p: *mut Node) { + loop { + match self + .0 + .compare_exchange_weak(ptr::null_mut(), p, Ordering::AcqRel, Ordering::Acquire) + { + Ok(_) => return, + Err(head) => { + if !head.is_null() { + return unsafe { (*head).next.append_ptr(p) }; + } + } + } + } + } + + pub fn append_list(&self, other: AppendList) { + let p = other.0.load(Ordering::Acquire); + mem::forget(other); + unsafe { self.append_ptr(p) }; + } + + pub fn iter(&self) -> AppendListIterator<'_, T> { + AppendListIterator(&self.0) + } + + pub fn iter_mut(&mut self) -> AppendListMutIterator<'_, T> { + AppendListMutIterator(&mut self.0) + } + } + + impl<'a, T> IntoIterator for &'a AppendList { + type Item = &'a T; + type IntoIter = AppendListIterator<'a, T>; + + fn into_iter(self) -> AppendListIterator<'a, T> { + self.iter() + } + } + + impl<'a, T> IntoIterator for &'a mut AppendList { + type Item = &'a mut T; + type IntoIter = AppendListMutIterator<'a, T>; + + fn into_iter(self) -> AppendListMutIterator<'a, T> { + self.iter_mut() + } + } + + impl Drop for AppendList { + fn drop(&mut self) { + unsafe { Self::node_from_raw(mem::replace(self.0.get_mut(), ptr::null_mut())) }; + } + } + + impl Default for AppendList { + fn default() -> Self { + Self::new() + } + } + + #[derive(Debug)] + pub struct AppendListIterator<'a, T>(&'a AtomicPtr>); + + impl<'a, T: 'a> Iterator for AppendListIterator<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option<&'a T> { + let p = self.0.load(Ordering::Acquire); + if p.is_null() { + None + } else { + unsafe { + self.0 = &(*p).next.0; + Some(&(*p).value) + } + } + } + } + + #[derive(Debug)] + pub struct AppendListMutIterator<'a, T>(&'a mut AtomicPtr>); + + impl<'a, T: 'a> Iterator for AppendListMutIterator<'a, T> { + type Item = &'a mut T; + + fn next(&mut self) -> Option<&'a mut T> { + let p = self.0.load(Ordering::Acquire); + if p.is_null() { + None + } else { + unsafe { + self.0 = &mut (*p).next.0; + Some(&mut (*p).value) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::UserDataMap; + + #[test] + fn insert_twice() { + let map = UserDataMap::new(); + + assert_eq!(map.get::(), None); + assert!(map.insert_if_missing(|| 42usize)); + assert!(!map.insert_if_missing(|| 43usize)); + assert_eq!(map.get::(), Some(&42)); + } +} diff --git a/vendor/smithay/src/utils/x11rb.rs b/vendor/smithay/src/utils/x11rb.rs new file mode 100644 index 0000000..a203ed5 --- /dev/null +++ b/vendor/smithay/src/utils/x11rb.rs @@ -0,0 +1,176 @@ +//! Helper utilities for using x11rb as an event source in calloop. +//! +//! The primary use for this module is XWayland integration but is also widely useful for an X11 +//! backend in a compositor. + +use std::{ + sync::Arc, + thread::{JoinHandle, spawn}, +}; + +use tracing::{error, warn}; +use x11rb::{ + connection::Connection as _, + protocol::{ + Event, + xproto::{Atom, CLIENT_MESSAGE_EVENT, ClientMessageEvent, ConnectionExt as _, EventMask, Window}, + }, + rust_connection::RustConnection, +}; + +use calloop::{ + EventSource, Poll, PostAction, Readiness, Token, TokenFactory, + channel::{Channel, ChannelError, Event as ChannelEvent, Sender, channel}, +}; + +/// Integration of an x11rb X11 connection with calloop. +/// +/// This is a thin wrapper around `Channel`. It works by spawning an extra thread reads events from +/// the X11 connection and then sends them across the channel. +/// +/// See [1] for why this extra thread is necessary. The single-thread solution proposed on that +/// page does not work with calloop, since it requires checking something on every main loop +/// iteration. Calloop only allows "when an FD becomes readable". +/// +/// [1]: https://docs.rs/x11rb/0.8.1/x11rb/event_loop_integration/index.html#threads-and-races +#[derive(Debug)] +pub struct X11Source { + connection: Arc, + channel: Option>, + event_thread: Option>, + close_window: Window, + close_type: Atom, +} + +impl X11Source { + /// Create a new X11 source. + /// + /// The returned instance will use `SendRequest` to cause a `ClientMessageEvent` to be sent to + /// the given window with the given type. The expectation is that this is a window that was + /// created by us. Thus, the event reading thread will wake up and check an internal exit flag, + /// then exit. + pub fn new(connection: Arc, close_window: Window, close_type: Atom) -> Self { + let (sender, channel) = channel(); + let conn = Arc::clone(&connection); + let event_thread = Some(spawn(move || { + run_event_thread(conn, sender); + })); + + Self { + connection, + channel: Some(channel), + event_thread, + close_window, + close_type, + } + } +} + +impl Drop for X11Source { + fn drop(&mut self) { + // Signal the worker thread to exit by dropping the read end of the channel. + self.channel.take(); + + // Send an event to wake up the worker so that it actually exits + let event = ClientMessageEvent { + response_type: CLIENT_MESSAGE_EVENT, + format: 8, + sequence: 0, + window: self.close_window, + type_: self.close_type, + data: [0; 20].into(), + }; + + let _ = self + .connection + .send_event(false, self.close_window, EventMask::NO_EVENT, event); + let _ = self.connection.flush(); + + // Wait for the worker thread to exit + self.event_thread.take().map(|handle| handle.join()); + } +} + +impl EventSource for X11Source { + type Event = ChannelEvent; + type Metadata = (); + type Ret = (); + type Error = ChannelError; + + #[profiling::function] + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: C, + ) -> Result + where + C: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + if let Some(channel) = &mut self.channel { + channel.process_events(readiness, token, move |event, meta| { + if matches!(event, ChannelEvent::Closed) { + warn!("Event thread exited"); + } + callback(event, meta) + }) + } else { + Ok(PostAction::Remove) + } + } + + fn register(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + if let Some(channel) = &mut self.channel { + channel.register(poll, factory)?; + } + + Ok(()) + } + + fn reregister(&mut self, poll: &mut Poll, factory: &mut TokenFactory) -> calloop::Result<()> { + if let Some(channel) = &mut self.channel { + channel.reregister(poll, factory)?; + } + + Ok(()) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + if let Some(channel) = &mut self.channel { + channel.unregister(poll)?; + } + + Ok(()) + } +} + +/// This thread reads X11 events from the connection and sends them on the channel. +/// +/// This is run in an extra thread since sending an X11 request or waiting for the reply to an X11 +/// request can both read X11 events from the underlying socket which are then saved in the +/// RustConnection. Thus, readability of the underlying socket is not enough to guarantee we do not +/// miss wakeups. +/// +/// This thread will call wait_for_event(). RustConnection then ensures internally to wake us up +/// when an event arrives. So far, this seems to be the only safe way to integrate x11rb with +/// calloop. +fn run_event_thread(connection: Arc, sender: Sender) { + loop { + let event = match connection.wait_for_event() { + Ok(event) => event, + Err(err) => { + // Connection errors are most likely permanent. Thus, exit the thread. + error!("Event thread exiting due to connection error {}", err); + break; + } + }; + match sender.send(event) { + Ok(()) => {} + Err(_) => { + // The only possible error is that the other end of the channel was dropped. + // This happens in X11Source's Drop impl. + break; + } + } + } +} diff --git a/vendor/smithay/src/wayland/alpha_modifier/dispatch.rs b/vendor/smithay/src/wayland/alpha_modifier/dispatch.rs new file mode 100644 index 0000000..f0ac34b --- /dev/null +++ b/vendor/smithay/src/wayland/alpha_modifier/dispatch.rs @@ -0,0 +1,136 @@ +use wayland_protocols::wp::alpha_modifier::v1::server::{ + wp_alpha_modifier_surface_v1::{self, WpAlphaModifierSurfaceV1}, + wp_alpha_modifier_v1::{self, WpAlphaModifierV1}, +}; + +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, New, Resource, backend::ClientId}; + +use super::{AlphaModifierSurfaceCachedState, AlphaModifierSurfaceData, AlphaModifierSurfaceUserData}; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2, compositor}; + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _: &Client, + manager: &WpAlphaModifierV1, + request: wp_alpha_modifier_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_alpha_modifier_v1::Request::GetSurface { id, surface } => { + let already_taken = compositor::with_states(&surface, |states| { + states + .data_map + .insert_if_missing_threadsafe(AlphaModifierSurfaceData::new); + let data = states.data_map.get::().unwrap(); + + let already_taken = data.is_resource_attached(); + + if !already_taken { + data.set_is_resource_attached(true); + } + + already_taken + }); + + if already_taken { + manager.post_error( + wp_alpha_modifier_v1::Error::AlreadyConstructed, + "wl_surface already has a alpha modifier object attached", + ) + } else { + data_init.init(id, AlphaModifierSurfaceUserData::new(surface)); + } + } + + wp_alpha_modifier_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for AlphaModifierSurfaceUserData { + fn request( + &self, + _state: &mut D, + _: &Client, + obj: &WpAlphaModifierSurfaceV1, + request: wp_alpha_modifier_surface_v1::Request, + _dh: &DisplayHandle, + _: &mut DataInit<'_, D>, + ) { + match request { + wp_alpha_modifier_surface_v1::Request::SetMultiplier { factor } => { + let Some(surface) = self.wl_surface() else { + obj.post_error( + wp_alpha_modifier_surface_v1::Error::NoSurface, + "wl_surface was destroyed", + ); + return; + }; + + compositor::with_states(&surface, |states| { + states + .cached_state + .get::() + .pending() + .multiplier = Some(factor); + }) + } + // Switch back to not specifying the alpha multiplier of this surface. + wp_alpha_modifier_surface_v1::Request::Destroy => { + let Some(surface) = self.wl_surface() else { + obj.post_error( + wp_alpha_modifier_surface_v1::Error::NoSurface, + "wl_surface was destroyed", + ); + return; + }; + + compositor::with_states(&surface, |states| { + states + .data_map + .get::() + .unwrap() + .set_is_resource_attached(false); + + states + .cached_state + .get::() + .pending() + .multiplier = None; + }); + } + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: ClientId, _object: &WpAlphaModifierSurfaceV1) { + // Nothing to do here, graceful Destroy is already handled with double buffering + // and in case of client close WlSurface destroyed handler will clean up the data anyway, + // so there is no point in queuing new update + } +} diff --git a/vendor/smithay/src/wayland/alpha_modifier/mod.rs b/vendor/smithay/src/wayland/alpha_modifier/mod.rs new file mode 100644 index 0000000..d6bc168 --- /dev/null +++ b/vendor/smithay/src/wayland/alpha_modifier/mod.rs @@ -0,0 +1,178 @@ +//! Implementation of `wp_alpha_modifier` protocol +//! +//! [`WaylandSurfaceRenderElement`][`crate::backend::renderer::element::surface::WaylandSurfaceRenderElement`] +//! takes the alpha multiplier into account automatically. +//! +//! ### Example +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use wayland_server::{protocol::wl_surface::WlSurface, DisplayHandle}; +//! use smithay::{ +//! wayland::compositor::{self, CompositorState, CompositorClientState, CompositorHandler}, +//! wayland::alpha_modifier::{AlphaModifierSurfaceCachedState, AlphaModifierState}, +//! }; +//! +//! pub struct State { +//! compositor_state: CompositorState, +//! }; +//! struct ClientState { compositor_state: CompositorClientState } +//! impl wayland_server::backend::ClientData for ClientState {} +//! +//! smithay::delegate_dispatch2!(State); +//! +//! impl CompositorHandler for State { +//! fn compositor_state(&mut self) -> &mut CompositorState { +//! &mut self.compositor_state +//! } +//! +//! fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { +//! &client.get_data::().unwrap().compositor_state +//! } +//! +//! fn commit(&mut self, surface: &WlSurface) { +//! compositor::with_states(&surface, |states| { +//! let mut modifier_state = states.cached_state.get::(); +//! dbg!(modifier_state.current().multiplier()); +//! }); +//! } +//! } +//! +//! let mut display = wayland_server::Display::::new().unwrap(); +//! +//! let compositor_state = CompositorState::new::(&display.handle()); +//! AlphaModifierState::new::(&display.handle()); +//! +//! let state = State { +//! compositor_state, +//! }; +//! ``` + +use std::sync::{ + Mutex, + atomic::{self, AtomicBool}, +}; + +use wayland_protocols::wp::alpha_modifier::v1::server::{ + wp_alpha_modifier_surface_v1::WpAlphaModifierSurfaceV1, wp_alpha_modifier_v1::WpAlphaModifierV1, +}; +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, Resource, Weak, backend::GlobalId, + protocol::wl_surface::WlSurface, +}; + +use super::compositor::Cacheable; + +use crate::wayland::GlobalData; + +mod dispatch; + +/// Data associated with WlSurface +/// Represents the client pending state +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::alpha_modifier::AlphaModifierSurfaceCachedState; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// let mut modifier_state = states.cached_state.get::(); +/// dbg!(modifier_state.current().multiplier()); +/// }); +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct AlphaModifierSurfaceCachedState { + multiplier: Option, +} + +impl AlphaModifierSurfaceCachedState { + /// Alpha multiplier for the surface + pub fn multiplier(&self) -> Option { + self.multiplier + } + + /// Alpha multiplier for the surface + pub fn multiplier_f32(&self) -> Option { + self.multiplier + .map(|multiplier| multiplier as f32 / u32::MAX as f32) + } + + /// Alpha multiplier for the surface + pub fn multiplier_f64(&self) -> Option { + self.multiplier + .map(|multiplier| multiplier as f64 / u32::MAX as f64) + } +} + +impl Cacheable for AlphaModifierSurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +#[derive(Debug)] +struct AlphaModifierSurfaceData { + is_resource_attached: AtomicBool, +} + +impl AlphaModifierSurfaceData { + fn new() -> Self { + Self { + is_resource_attached: AtomicBool::new(false), + } + } + + fn set_is_resource_attached(&self, is_attached: bool) { + self.is_resource_attached + .store(is_attached, atomic::Ordering::Release) + } + + fn is_resource_attached(&self) -> bool { + self.is_resource_attached.load(atomic::Ordering::Acquire) + } +} + +/// User data of [WpAlphaModifierSurfaceV1] object +#[derive(Debug)] +pub struct AlphaModifierSurfaceUserData(Mutex>); + +impl AlphaModifierSurfaceUserData { + fn new(surface: WlSurface) -> Self { + Self(Mutex::new(surface.downgrade())) + } + + fn wl_surface(&self) -> Option { + self.0.lock().unwrap().upgrade().ok() + } +} + +/// Delegate type for [WpAlphaModifierV1] global. +#[derive(Debug)] +pub struct AlphaModifierState { + global: GlobalId, +} + +impl AlphaModifierState { + /// Regiseter new [WpAlphaModifierV1] global + pub fn new(display: &DisplayHandle) -> AlphaModifierState + where + D: GlobalDispatch + + Dispatch + + Dispatch + + 'static, + { + let global = display.create_global::(1, GlobalData); + + AlphaModifierState { global } + } + + /// Returns the [WpAlphaModifierV1] global id + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} diff --git a/vendor/smithay/src/wayland/background_effect/dispatch.rs b/vendor/smithay/src/wayland/background_effect/dispatch.rs new file mode 100644 index 0000000..860c988 --- /dev/null +++ b/vendor/smithay/src/wayland/background_effect/dispatch.rs @@ -0,0 +1,139 @@ +use crate::wayland::background_effect::{BackgroundEffectSurfaceData, ExtBackgroundEffectHandler}; +use crate::wayland::compositor; +use crate::wayland::{ + Dispatch2, GlobalData, GlobalDispatch2, + background_effect::{BackgroundEffectSurfaceCachedState, BackgroundEffectSurfaceUserData}, + compositor::with_states, +}; +use wayland_protocols::ext::background_effect::v1::server::{ + ext_background_effect_manager_v1::{ + Error as ManagerError, ExtBackgroundEffectManagerV1, Request as ManagerRequest, + }, + ext_background_effect_surface_v1::{ + Error as SurfaceError, ExtBackgroundEffectSurfaceV1, Request as SurfaceRequest, + }, +}; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, New, Resource}; + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, +{ + fn bind( + &self, + state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let manager = data_init.init(resource, GlobalData); + manager.capabilities(state.capabilities()); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + manager: &ExtBackgroundEffectManagerV1, + request: ManagerRequest, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ManagerRequest::GetBackgroundEffect { id, surface } => { + let already_taken = with_states(&surface, |states| { + let data = states + .data_map + .get_or_insert_threadsafe(BackgroundEffectSurfaceData::new); + let already = data.is_resource_attached(); + if !already { + data.set_is_resource_attached(true); + } + already + }); + + if already_taken { + manager.post_error( + ManagerError::BackgroundEffectExists, + "wl_surface already has a background effect object attached", + ); + } else { + data_init.init(id, BackgroundEffectSurfaceUserData::new(surface)); + } + } + ManagerRequest::Destroy => {} + _ => {} + } + } +} + +impl Dispatch2 + for BackgroundEffectSurfaceUserData +{ + fn request( + &self, + state: &mut D, + _client: &Client, + obj: &ExtBackgroundEffectSurfaceV1, + request: SurfaceRequest, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + SurfaceRequest::SetBlurRegion { region } => { + let Some(surface) = self.wl_surface() else { + obj.post_error(SurfaceError::SurfaceDestroyed, "wl_surface was destroyed"); + return; + }; + + let region = region.as_ref().map(compositor::get_region_attributes); + + with_states(&surface, |states| { + let mut cached = states.cached_state.get::(); + let pending = cached.pending(); + pending.blur_region = region.clone(); + }); + + if let Some(region) = region { + state.set_blur_region(surface, region); + } else { + state.unset_blur_region(surface); + } + } + SurfaceRequest::Destroy => { + let Some(surface) = self.wl_surface() else { + // object is inert, but destroy is still allowed. We are done here. + return; + }; + + with_states(&surface, |states| { + states + .data_map + .get::() + .unwrap() + .set_is_resource_attached(false); + let mut cached = states.cached_state.get::(); + cached.pending().blur_region = None; + }); + + state.unset_blur_region(surface); + } + _ => {} + } + } + + fn destroyed( + &self, + _state: &mut D, + _client_id: wayland_server::backend::ClientId, + _object: &ExtBackgroundEffectSurfaceV1, + ) { + // No-op: cleanup is handled by double-buffering and surface destruction + } +} diff --git a/vendor/smithay/src/wayland/background_effect/mod.rs b/vendor/smithay/src/wayland/background_effect/mod.rs new file mode 100644 index 0000000..763be21 --- /dev/null +++ b/vendor/smithay/src/wayland/background_effect/mod.rs @@ -0,0 +1,157 @@ +//! Implementation of `ext_background_effect_manager_v1` protocol +//! +//! In order to advertise background effect global call [BackgroundEffectState::new]. +//! Currently attached effects are available in double-buffered [BackgroundEffectSurfaceCachedState] +//! +//! ``` +//! use smithay::wayland::background_effect::{BackgroundEffectState, ExtBackgroundEffectHandler, Capability}; +//! use wayland_server::protocol::wl_surface::WlSurface; +//! use smithay::wayland::compositor; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! BackgroundEffectState::new::( +//! &display.handle(), +//! ); +//! +//! impl ExtBackgroundEffectHandler for State { +//! fn capabilities(&self) -> Capability { +//! Capability::Blur +//! } +//! +//! fn set_blur_region(&mut self, wl_surface: WlSurface, region: compositor::RegionAttributes) { +//! // Called when blur becomes pending, and awaits surface commit. +//! // Blur region is stored in wl_surface [BackgroundEffectSurfaceCachedState] +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::sync::{ + Mutex, + atomic::{AtomicBool, Ordering}, +}; + +use crate::wayland::{ + GlobalData, + compositor::{self, Cacheable}, +}; +use wayland_protocols::ext::background_effect::v1::server::ext_background_effect_manager_v1::{ + self, ExtBackgroundEffectManagerV1, +}; +use wayland_server::{ + DisplayHandle, GlobalDispatch, Resource, Weak, backend::GlobalId, protocol::wl_surface::WlSurface, +}; + +pub use ext_background_effect_manager_v1::Capability; + +mod dispatch; + +/// Handler trait for ext background effect events. +pub trait ExtBackgroundEffectHandler: 'static { + /// Getter for background-effect capabilities of the compositor + fn capabilities(&self) -> Capability { + // For now only blur is supported, so it's safe to assume it's supported by default + Capability::Blur + } + + /// Called when blur becomes pending, and awaits surface commit. + /// Blur region is stored in wl_surface [BackgroundEffectSurfaceCachedState] + fn set_blur_region(&mut self, wl_surface: WlSurface, region: compositor::RegionAttributes) { + let _ = wl_surface; + let _ = region; + } + + /// Called when blur unset becomes pending, and awaits surface commit. + fn unset_blur_region(&mut self, wl_surface: WlSurface) { + let _ = wl_surface; + } +} + +/// Cached state for background effect per surface (double-buffered) +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::background_effect::BackgroundEffectSurfaceCachedState; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// let mut modifier_state = states.cached_state.get::(); +/// dbg!(&modifier_state.current().blur_region); +/// }); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct BackgroundEffectSurfaceCachedState { + /// Region of the surface that will have its background blurred. + pub blur_region: Option, +} + +impl Cacheable for BackgroundEffectSurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + self.clone() + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +/// User data for ext_background_effect_surface_v1 +#[derive(Debug)] +pub struct BackgroundEffectSurfaceUserData(Mutex>); + +impl BackgroundEffectSurfaceUserData { + fn new(surface: WlSurface) -> Self { + Self(Mutex::new(surface.downgrade())) + } + + fn wl_surface(&self) -> Option { + self.0.lock().unwrap().upgrade().ok() + } +} + +/// Tracks if a surface already has a background effect object +#[derive(Debug)] +struct BackgroundEffectSurfaceData { + is_resource_attached: AtomicBool, +} + +impl BackgroundEffectSurfaceData { + fn new() -> Self { + Self { + is_resource_attached: AtomicBool::new(false), + } + } + + fn set_is_resource_attached(&self, is_attached: bool) { + self.is_resource_attached.store(is_attached, Ordering::Release) + } + + fn is_resource_attached(&self) -> bool { + self.is_resource_attached.load(Ordering::Acquire) + } +} + +/// Global state for background effect protocol +#[derive(Debug)] +pub struct BackgroundEffectState { + global: GlobalId, +} + +impl BackgroundEffectState { + /// Regiseter new [ExtBackgroundEffectManagerV1] global + pub fn new(display: &DisplayHandle) -> BackgroundEffectState + where + D: ExtBackgroundEffectHandler + GlobalDispatch, + { + let global = display.create_global::(1, GlobalData); + BackgroundEffectState { global } + } + + /// Returns the [ExtBackgroundEffectManagerV1] global id + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} diff --git a/vendor/smithay/src/wayland/buffer/mod.rs b/vendor/smithay/src/wayland/buffer/mod.rs new file mode 100644 index 0000000..57164b3 --- /dev/null +++ b/vendor/smithay/src/wayland/buffer/mod.rs @@ -0,0 +1,23 @@ +//! Buffer management traits. +//! +//! This module provides the [`BufferHandler`] trait to notify compositors that a +//! [`WlBuffer`](wayland_server::protocol::wl_buffer::WlBuffer) managed by +//! Smithay has been destroyed. + +use wayland_server::protocol::wl_buffer; + +/// Handler trait for associating data with a [`WlBuffer`](wayland_server::protocol::wl_buffer::WlBuffer). +/// +/// This trait primarily allows compositors to be told when a buffer is destroyed. +/// +/// # For buffer abstractions +/// +/// Buffer abstractions (such as [`shm`](crate::wayland::shm)) should require this trait in their +/// [`delegate_dispatch`](wayland_server::delegate_dispatch) implementations to notify the compositor when a +/// buffer is destroyed. +pub trait BufferHandler { + /// Called when the client has destroyed the buffer. + /// + /// At this point the buffer is no longer usable by Smithay. + fn buffer_destroyed(&mut self, buffer: &wl_buffer::WlBuffer); +} diff --git a/vendor/smithay/src/wayland/commit_timing/mod.rs b/vendor/smithay/src/wayland/commit_timing/mod.rs new file mode 100644 index 0000000..6cd6f2c --- /dev/null +++ b/vendor/smithay/src/wayland/commit_timing/mod.rs @@ -0,0 +1,441 @@ +//! Utilities for handling the `wp-commit-timing` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation create the [`CommitTimingManagerState`] and store it inside your `State` struct. +//! +//! ``` +//! use smithay::wayland::compositor; +//! use smithay::wayland::commit_timing::CommitTimingManagerState; +//! +//! # struct State { commit_timing_manager_state: CommitTimingManagerState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the compositor state +//! let commit_timing_manager_state = CommitTimingManagerState::new::( +//! &display.handle(), +//! ); +//! +//! // insert the CommitTimingManagerState into your state +//! // .. +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Use the commit timer state +//! +//! Whenever the client commits a surface content update containing a commit timer timestamp set +//! through [`wp_commit_timer_v1::Request::SetTimestamp`] the implementation will place a [`Blocker`](crate::wayland::compositor::Blocker) +//! on the surface and register it in the [`CommitTimerBarrierState`]. It is your responsibility to query for pending commit timers +//! and signal them to allow client to make forward progress. +//! +//! You can query the pending commit timers and signal them as shown in the following example: +//! +//! ```no_run +//! # use wayland_server::{backend::ObjectId, protocol::wl_surface, Resource}; +//! use smithay::wayland::compositor; +//! use smithay::wayland::commit_timing::CommitTimerBarrierStateUserData; +//! # use smithay::wayland::commit_timing::Timestamp; +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let dh = display.handle(); +//! # let surface = wl_surface::WlSurface::from_id(&dh, ObjectId::null()).unwrap(); +//! # let frame_target: Timestamp = todo!(); +//! compositor::with_states(&surface, |states| { +//! if let Some(mut commit_timer_state) = states +//! .data_map +//! .get::() +//! .map(|commit_timer| commit_timer.lock().unwrap()) +//! { +//! if commit_timer_state.signal_until(frame_target) { +//! // ..signal blocker cleared +//! } +//! } +//! }); +//! ``` +//! +//! ### Unmanaged mode +//! +//! If for some reason the integrated solution for commit timers does not suit your needs +//! you can create an unmanaged version with [`CommitTimingManagerState::unmanaged`]. +//! In this case it is your responsibility to listen for surface commits and place blockers +//! accordingly to the spec. +//! +//! The commit timer timestamp can be retrieved like shown in the following example: +//! +//! ```no_run +//! # let surface: wayland_server::protocol::wl_surface::WlSurface = todo!(); +//! use smithay::wayland::compositor; +//! use smithay::wayland::commit_timing::CommitTimerStateUserData; +//! +//! let timestamp = compositor::with_states(&surface, |states| { +//! states +//! .data_map +//! .get::() +//! .and_then(|state| state.borrow_mut().timestamp.take()) +//! }); +//! ``` +use std::{cell::RefCell, collections::BinaryHeap, sync::Mutex}; + +use rustix::fs::Timespec; +use wayland_protocols::wp::commit_timing::v1::server::{ + wp_commit_timer_v1::{self, WpCommitTimerV1}, + wp_commit_timing_manager_v1::{self, WpCommitTimingManagerV1}, +}; +use wayland_server::{ + DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, backend::GlobalId, + protocol::wl_surface::WlSurface, +}; + +use crate::{ + utils::Time, + wayland::{ + Dispatch2, GlobalDispatch2, + compositor::{add_blocker, add_pre_commit_hook}, + }, +}; + +use super::compositor::{Barrier, with_states}; + +/// State for the [`WpCommitTimingManagerV1`] global +#[derive(Debug)] +pub struct CommitTimingManagerState { + global: GlobalId, + is_managed: bool, +} + +impl CommitTimingManagerState { + /// Create a new [`WpCommitTimingManagerV1`] global + // + /// The id provided by [`CommitTimingManagerState::global`] may be used to + /// remove or disable this global in the future. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: 'static, + { + Self::new_internal::(display, true) + } + + /// Create a new unmanaged [`WpCommitTimingManagerV1`] global + // + /// The id provided by [`CommitTimingManagerState::global`] may be used to + /// remove or disable this global in the future. + pub fn unmanaged(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: 'static, + { + Self::new_internal::(display, false) + } + + fn new_internal(display: &DisplayHandle, is_managed: bool) -> Self + where + D: GlobalDispatch, + D: 'static, + { + let global = + display.create_global::(1, CommitTimingManagerData { is_managed }); + + Self { global, is_managed } + } + + /// Returns the id of the [`WpCommitTimingManagerV1`] global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } + + /// Returns if this [`CommitTimingManagerState`] operates in managed mode. + pub fn is_managed(&self) -> bool { + self.is_managed + } +} + +#[doc(hidden)] +#[derive(Debug, Clone, Copy)] +pub struct CommitTimingManagerData { + is_managed: bool, +} + +impl GlobalDispatch2 for CommitTimingManagerData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, *self); + } +} + +impl Dispatch2 for CommitTimingManagerData +where + D: Dispatch, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &WpCommitTimingManagerV1, + request: wp_commit_timing_manager_v1::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_commit_timing_manager_v1::Request::GetTimer { id, surface } => { + let (is_initial, has_active_commit_timer) = with_states(&surface, |states| { + let marker = states.data_map.get::>(); + ( + marker.is_none(), + marker.map(|m| m.borrow().0.is_some()).unwrap_or(false), + ) + }); + + // The protocol mandates that only a single commit timer object is associated with a surface at all times + if has_active_commit_timer { + surface.post_error( + wp_commit_timing_manager_v1::Error::CommitTimerExists, + "the surface has already a commit timer object associated", + ); + return; + } + + // Make sure we do not install the hook more then once in case the surface is being reused + if self.is_managed && is_initial { + add_pre_commit_hook::(&surface, |_, _, surface| { + let timestamp = with_states(surface, |states| { + states + .data_map + .get::() + .and_then(|state| state.borrow_mut().timestamp.take()) + }); + + if let Some(timestamp) = timestamp { + let barrier = with_states(surface, |states| { + let barrier_state = states + .data_map + .get_or_insert(CommitTimerBarrierStateUserData::default); + barrier_state.lock().unwrap().register(timestamp) + }); + + add_blocker(surface, barrier); + } + }); + } + + let commit_timer: WpCommitTimerV1 = data_init.init(id, CommitTimerData(surface.downgrade())); + + with_states(&surface, |states| { + states + .data_map + .get_or_insert(|| RefCell::new(CommitTimerMarker(None))) + .borrow_mut() + .0 = Some(commit_timer); + }); + } + wp_commit_timing_manager_v1::Request::Destroy => (), + _ => unreachable!(), + } + } +} + +// Internal marker to track if a commit timer object is currently associated with a surface. +// Used to realize the `AlreadyExists` protocol check. +struct CommitTimerMarker(Option); + +#[doc(hidden)] +#[derive(Debug)] +pub struct CommitTimerData(Weak); + +impl Dispatch2 for CommitTimerData +where + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + resource: &WpCommitTimerV1, + request: wp_commit_timer_v1::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_commit_timer_v1::Request::SetTimestamp { + tv_sec_hi, + tv_sec_lo, + tv_nsec, + } => { + let Ok(surface) = self.0.upgrade() else { + resource.post_error( + wp_commit_timer_v1::Error::SurfaceDestroyed, + "the surface associated with this commit timer object has been destroyed".to_string(), + ); + return; + }; + + let tv_sec = ((tv_sec_hi as u64) << 32) | tv_sec_lo as u64; + + let timestamp = Timestamp(Timespec { + tv_sec: tv_sec as rustix::time::Secs, + tv_nsec: tv_nsec as rustix::time::Nsecs, + }); + + let already_has_timestamp = with_states(&surface, move |states| { + let mut commit_timer_state = states + .data_map + .get_or_insert(CommitTimerStateUserData::default) + .borrow_mut(); + + if commit_timer_state.timestamp.is_some() { + return true; + } + + commit_timer_state.timestamp = Some(timestamp); + false + }); + + if already_has_timestamp { + resource.post_error( + wp_commit_timer_v1::Error::TimestampExists, + "the surface already has a timestamp associated for this commit".to_string(), + ); + } + } + wp_commit_timer_v1::Request::Destroy => { + if let Ok(surface) = self.0.upgrade() { + with_states(&surface, |states| { + states + .data_map + .get::>() + .unwrap() + .borrow_mut() + .0 = None; + }); + } + } + _ => unreachable!(), + } + } +} + +/// Timestamp set through [`wp_commit_timer_v1::Request::SetTimestamp`] for a surface +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct Timestamp(Timespec); + +impl Ord for Timestamp { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.0.tv_sec.cmp(&other.0.tv_sec) { + std::cmp::Ordering::Equal => self.0.tv_nsec.cmp(&other.0.tv_nsec), + other => other, + } + } +} + +impl PartialOrd for Timestamp { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Per surface [`WpCommitTimerV1`] state stored in the surface user data +pub type CommitTimerStateUserData = RefCell; + +/// Per surface state for [`WpCommitTimerV1`] +#[derive(Debug, Default)] +pub struct CommitTimerState { + /// The timestamp set through [`wp_commit_timer_v1::Request::SetTimestamp`] for this surface + pub timestamp: Option, +} + +/// Per surface barrier state stored in the surface user data +pub type CommitTimerBarrierStateUserData = Mutex; + +/// Per surface barrier state using the managed mode +#[derive(Debug, Default)] +pub struct CommitTimerBarrierState { + barriers: BinaryHeap, +} + +impl From> for Timestamp { + fn from(value: Time) -> Self { + Self(value.into()) + } +} + +impl From for Time { + fn from(value: Timestamp) -> Self { + Time::from(value.0) + } +} + +impl CommitTimerBarrierState { + /// Signal all tracked barriers matching the specified deadline + /// + /// A barrier is considered to lie within the deadline if the + /// timestamp of the barrier is past the specified deadline + /// + /// Returns `true` if a barrier has been signaled, false otherwise + pub fn signal_until(&mut self, deadline: impl Into) -> bool { + let deadline = deadline.into(); + + let num_barriers = self.barriers.len(); + + while let Some(barrier) = self.barriers.peek() { + if barrier.timestamp > deadline { + break; + } + + barrier.barrier.signal(); + let _ = self.barriers.pop(); + } + + num_barriers != self.barriers.len() + } + + /// Register a new barrier with the provided timestamp + pub fn register(&mut self, timestamp: Timestamp) -> Barrier { + let barrier = Barrier::new(false); + self.barriers.push(CommitTimerBarrier { + timestamp, + barrier: barrier.clone(), + }); + barrier + } + + /// Retrieve the next deadline when available + pub fn next_deadline(&self) -> Option { + self.barriers.peek().map(|barrier| barrier.timestamp) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct CommitTimerBarrier { + timestamp: Timestamp, + barrier: Barrier, +} + +impl std::cmp::Ord for CommitTimerBarrier { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // earlier values have priority + self.timestamp.cmp(&other.timestamp).reverse() + } +} + +impl std::cmp::PartialOrd for CommitTimerBarrier { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/vendor/smithay/src/wayland/compositor/cache.rs b/vendor/smithay/src/wayland/compositor/cache.rs new file mode 100644 index 0000000..1921ada --- /dev/null +++ b/vendor/smithay/src/wayland/compositor/cache.rs @@ -0,0 +1,224 @@ +// The caching logic is used to process surface synchronization. It creates +// an effective decoupling between the moment the client sends wl_surface.commit +// and the moment where the state that was committed is actually applied by the +// compositor. +// +// The way this is modelled in Smithay is through the `Cache` type, which is a container +// representing a cached state for a particular type. The full cached state of a surface +// is thus composed of a a set of `Cache` for all relevant `T`, as modelled by the +// `MultiCache`. +// +// The logic of the `Cache` is as follows: +// +// - The protocol handlers mutably access the `pending` state to modify it accord to +// the client requests +// - On commit, a snapshot of this pending state is created by invoking `Cacheable::commit` +// and stored in the cache alongside an externally provided id +// - When the compositor decides that a given state (represented by its commit id) should +// become active, `Cache::apply_state` is invoked with that commit id. The associated state +// is then applied to the `current` state, that the compositor can then use as a reference +// for the current window state. Note that, to preserve the commit ordering, all states +// with a commit id older than the one requested are applied as well, in order. +// +// The logic for generating these commit ids and deciding when to apply them is implemented +// and described in `transaction.rs`. + +use std::{ + collections::VecDeque, + sync::{Mutex, MutexGuard}, +}; + +use downcast_rs::{Downcast, impl_downcast}; +use wayland_server::DisplayHandle; + +use crate::utils::Serial; + +/// Trait representing a value that can be used in double-buffered storage +/// +/// The type needs to implement the [`Default`] trait, which will be used +/// to initialize. You further need to provide two methods: +/// [`Cacheable::commit`] and [`Cacheable::merge_into`]. +/// +/// Double-buffered state works by having a "pending" instance of your type, +/// into which new values provided by the client are inserted. When the client +/// sends `wl_surface.commit`, the [`Cacheable::commit`] method will be +/// invoked on your value. This method is expected to produce a new instance of +/// your type, that will be stored in the cache, and eventually merged into the +/// current state. +/// +/// In most cases, this method will simply produce a copy of the pending state, +/// but you might need additional logic in some cases, such as for handling +/// non-cloneable resources (which thus need to be moved into the produce value). +/// +/// Then at some point the [`Cacheable::merge_into`] method of your type will be +/// invoked. In this method, `self` acts as the update that should be merged into +/// the current state provided as argument. In simple cases, the action would just +/// be to copy `self` into the current state, but more complex cases require +/// additional logic. +pub trait Cacheable: Default { + /// Produce a new state to be cached from the pending state + fn commit(&mut self, dh: &DisplayHandle) -> Self; + /// Merge a state update into the current state + fn merge_into(self, into: &mut Self, dh: &DisplayHandle); +} + +/// Double buffered cached state of type `T` +#[derive(Debug)] +pub struct CachedState { + pending: T, + cache: VecDeque<(Serial, T)>, + current: T, +} + +impl Default for CachedState { + fn default() -> Self { + CachedState { + pending: T::default(), + cache: VecDeque::new(), + current: T::default(), + } + } +} + +impl CachedState { + /// Access the current state for `T` + pub fn current(&mut self) -> &mut T { + &mut self.current + } + + /// Access the pending state for `T` + pub fn pending(&mut self) -> &mut T { + &mut self.pending + } +} + +trait Cache: Downcast { + fn commit(&self, commit_id: Option, dh: &DisplayHandle); + fn apply_state(&self, commit_id: Serial, dh: &DisplayHandle); +} + +impl_downcast!(Cache); + +impl Cache for Mutex> { + fn commit(&self, commit_id: Option, dh: &DisplayHandle) { + let mut guard = self.lock().unwrap(); + let me = &mut *guard; + let new_state = me.pending.commit(dh); + if let Some(id) = commit_id { + match me.cache.back_mut() { + Some(&mut (cid, ref mut state)) if cid == id => new_state.merge_into(state, dh), + _ => me.cache.push_back((id, new_state)), + } + } else { + for (_, state) in me.cache.drain(..) { + state.merge_into(&mut me.current, dh); + } + new_state.merge_into(&mut me.current, dh); + } + } + + fn apply_state(&self, commit_id: Serial, dh: &DisplayHandle) { + let mut me = self.lock().unwrap(); + loop { + if me.cache.front().map(|&(s, _)| s > commit_id).unwrap_or(true) { + // if the cache is empty or the next state has a commit_id greater than the requested one + break; + } + me.cache.pop_front().unwrap().1.merge_into(&mut me.current, dh); + } + } +} + +/// A typemap-like container for double-buffered values +/// +/// All values inserted into this container must implement the [`Cacheable`] trait, +/// which defines their buffering semantics. They furthermore must be [`Send`] as the surface state +/// can be accessed from multiple threads (but [`Sync`] is not required, the surface internally synchronizes +/// access to its state). +/// +/// [`MultiCache::get`] provides access to the [`CachedState`] associated with a particular type. +/// +/// Consumers of surface state (like compositor applications using Smithay) will mostly be concerned +/// with the [`CachedState::current`] method, which gives access to the current state of the surface for +/// a particular type. +/// +/// Writers of protocol extensions logic will mostly be concerned with the [`CachedState::pending`] method, +/// which provides access to the pending state of the surface, in which new state from clients will be +/// stored. +/// +/// This contained has [`Mutex`]-like semantics: values of multiple stored types can be accessed at the +/// same time, but accessing the same value multiple times will cause a deadlock. +/// The stored values are initialized lazily the first time [`get`][Self::get] is invoked with this type as argument. +pub struct MultiCache { + caches: appendlist::AppendList>, +} + +impl std::fmt::Debug for MultiCache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MultiCache").finish_non_exhaustive() + } +} + +impl MultiCache { + pub(crate) fn new() -> Self { + Self { + caches: appendlist::AppendList::new(), + } + } + + fn find_or_insert(&self) -> &Mutex> { + for cache in &self.caches { + if let Some(v) = (**cache).as_any().downcast_ref() { + return v; + } + } + // if we reach here, then the value is not yet in the list, insert it + self.caches + .push(Box::new(Mutex::new(CachedState::::default())) as Box<_>); + (*self.caches[self.caches.len() - 1]) + .as_any() + .downcast_ref() + .unwrap() + } + + /// Access the [`CachedState`] associated with type `T` + pub fn get(&self) -> MutexGuard<'_, CachedState> { + self.find_or_insert::().lock().unwrap() + } + + /// Check if the container currently contains values for type `T` + pub fn has(&self) -> bool { + self.caches + .iter() + .any(|c| (**c).as_any().is::>>()) + } + + /// Commits the pending state, invoking Cacheable::commit() + /// + /// If commit_id is None, then the pending state is directly merged + /// into the current state. Otherwise, this id is used to store the + /// cached state. An id ca no longer be re-used as soon as not new id + /// has been used in between. Provided IDs are expected to be provided + /// in increasing order according to `Serial` semantics. + /// + /// If a None commit is given but there are some cached states, they'll + /// all be merged into the current state before merging the pending one. + pub(crate) fn commit(&mut self, commit_id: Option, dh: &DisplayHandle) { + // none of the underlying borrow_mut() can panic, as we hold + // a &mut reference to the container, non are borrowed. + for cache in &self.caches { + cache.commit(commit_id, dh); + } + } + + /// Apply given identified cached state to the current one + /// + /// All other preceding states are applied as well, to preserve commit ordering + pub(crate) fn apply_state(&self, commit_id: Serial, dh: &DisplayHandle) { + // none of the underlying borrow_mut() can panic, as we hold + // a &mut reference to the container, non are borrowed. + for cache in &self.caches { + cache.apply_state(commit_id, dh); + } + } +} diff --git a/vendor/smithay/src/wayland/compositor/handlers.rs b/vendor/smithay/src/wayland/compositor/handlers.rs new file mode 100644 index 0000000..84c326c --- /dev/null +++ b/vendor/smithay/src/wayland/compositor/handlers.rs @@ -0,0 +1,667 @@ +use std::sync::{ + Mutex, + atomic::{AtomicBool, Ordering}, +}; + +use wayland_server::{ + DataInit, Dispatch, DisplayHandle, New, Resource, WEnum, + protocol::{ + wl_callback::{self, WlCallback}, + wl_compositor::{self, WlCompositor}, + wl_region::{self, WlRegion}, + wl_subcompositor::{self, WlSubcompositor}, + wl_subsurface::{self, WlSubsurface}, + wl_surface::{self, WlSurface}, + }, +}; + +use crate::utils::{ + Client, Logical, Point, + alive_tracker::{AliveTracker, IsAlive}, +}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +use super::{ + AlreadyHasRole, BufferAssignment, CompositorHandler, Damage, Rectangle, RectangleKind, RegionAttributes, + SurfaceAttributes, + cache::Cacheable, + tree::{Location, PrivateSurfaceData}, +}; + +use tracing::trace; + +/* + * wl_compositor + */ + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: CompositorHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: CompositorHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + _resource: &WlCompositor, + request: wl_compositor::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_compositor::Request::CreateSurface { id } => { + trace!(id = ?id, "Creating a new wl_surface"); + + let surface = data_init.init( + id, + SurfaceUserData { + inner: PrivateSurfaceData::new(), + alive_tracker: Default::default(), + user_state_type: (std::any::TypeId::of::(), std::any::type_name::()), + }, + ); + + state.compositor_state().surfaces.push(surface.clone()); + + PrivateSurfaceData::init(&surface); + state.new_surface(&surface); + } + wl_compositor::Request::CreateRegion { id } => { + trace!(id = ?id, "Creating a new wl_region"); + + data_init.init( + id, + RegionUserData { + inner: Default::default(), + }, + ); + } + _ => unreachable!(), + } + } +} + +/* + * wl_surface + */ + +impl Cacheable for SurfaceAttributes { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + SurfaceAttributes { + buffer: self.buffer.take(), + buffer_delta: self.buffer_delta.take(), + buffer_scale: self.buffer_scale, + buffer_transform: self.buffer_transform, + damage: std::mem::take(&mut self.damage), + opaque_region: self.opaque_region.clone(), + input_region: self.input_region.clone(), + frame_callbacks: std::mem::take(&mut self.frame_callbacks), + client_scale: self.client_scale, + } + } + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + if self.buffer.is_some() { + if let Some(BufferAssignment::NewBuffer(buffer)) = + std::mem::replace(&mut into.buffer, self.buffer) + { + let new_buffer = into.buffer.as_ref().and_then(|b| match b { + BufferAssignment::Removed => None, + BufferAssignment::NewBuffer(buffer) => Some(buffer), + }); + + if Some(&buffer) != new_buffer { + buffer.release(); + } + } + } + into.buffer_delta = self.buffer_delta; + into.buffer_scale = self.buffer_scale; + into.buffer_transform = self.buffer_transform; + into.damage.extend(self.damage); + into.opaque_region = self.opaque_region; + into.input_region = self.input_region; + into.frame_callbacks.extend(self.frame_callbacks); + into.client_scale = self.client_scale; + } +} + +/// User data for WlSurface +#[derive(Debug)] +pub struct SurfaceUserData { + pub(crate) inner: Mutex, + alive_tracker: AliveTracker, + pub(super) user_state_type: (std::any::TypeId, &'static str), +} + +impl Dispatch2 for SurfaceUserData +where + D: Dispatch, + D: CompositorHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + client: &wayland_server::Client, + surface: &WlSurface, + request: wl_surface::Request, + handle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wl_surface::Request::Attach { buffer, x, y } => { + let offset: Point = (x, y).into(); + let offset = (x != 0 || y != 0).then_some(offset); + + // If version predates 5 just use the offset + // Otherwise error out and use None + let offset = if surface.version() < 5 { + offset + } else { + if offset.is_some() { + surface.post_error( + wl_surface::Error::InvalidOffset, + "Passing non-zero x,y is protocol violation since versions 5", + ); + } + + None + }; + + let client_scale = state.client_compositor_state(client).client_scale(); + let offset = offset.map(|p| p.to_f64().to_logical(client_scale).to_i32_round()); + + PrivateSurfaceData::with_states(surface, |states| { + let mut guard = states.cached_state.get::(); + let pending = guard.pending(); + + // Let's set the offset here in case it is supported and non-zero + if offset.is_some() { + pending.buffer_delta = offset; + } + + pending.buffer = Some(match buffer { + Some(buffer) => BufferAssignment::NewBuffer(buffer), + None => BufferAssignment::Removed, + }) + }); + } + wl_surface::Request::Damage { x, y, width, height } => { + let client_scale = state.client_compositor_state(client).client_scale(); + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .damage + .push(Damage::Surface( + Rectangle::::new((x, y).into(), (width, height).into()) + .to_f64() + .to_logical(client_scale) + .to_i32_round(), + )); + }); + } + wl_surface::Request::Frame { callback } => { + let callback = data_init.init(callback, GlobalData); + + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .frame_callbacks + .push(callback.clone()); + }); + } + wl_surface::Request::SetOpaqueRegion { region } => { + let attributes = region.map(|r| { + let attributes_mutex = &r.data::().unwrap().inner; + attributes_mutex.lock().unwrap().clone() + }); + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .opaque_region = attributes; + }); + } + wl_surface::Request::SetInputRegion { region } => { + let attributes = region.map(|r| { + let attributes_mutex = &r.data::().unwrap().inner; + attributes_mutex.lock().unwrap().clone() + }); + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .input_region = attributes; + }); + } + wl_surface::Request::Commit => { + let client_scale = state.client_compositor_state(client).client_scale(); + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .client_scale = client_scale; + }); + + PrivateSurfaceData::invoke_pre_commit_hooks(state, handle, surface); + + PrivateSurfaceData::commit(surface, handle, state); + } + wl_surface::Request::SetBufferTransform { transform } => { + if let WEnum::Value(transform) = transform { + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .buffer_transform = transform; + }); + } + } + wl_surface::Request::SetBufferScale { scale } => { + if scale >= 1 { + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .buffer_scale = scale; + }); + } else { + surface.post_error(wl_surface::Error::InvalidScale, "Scale must be positive"); + } + } + wl_surface::Request::DamageBuffer { x, y, width, height } => { + if width < 0 || height < 0 { + return; + } + + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .damage + .push(Damage::Buffer(Rectangle::new( + (x, y).into(), + (width, height).into(), + ))) + }); + } + wl_surface::Request::Offset { x, y } => { + let client_scale = state.client_compositor_state(client).client_scale(); + PrivateSurfaceData::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .buffer_delta = Some( + Point::::from((x, y)) + .to_f64() + .to_logical(client_scale) + .to_i32_round(), + ); + }); + } + wl_surface::Request::Destroy => { + // All is already handled by our destructor + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client_id: wayland_server::backend::ClientId, surface: &WlSurface) { + // We let the destruction hooks run first and then tell the compositor handler the surface was + // destroyed. + self.alive_tracker.destroy_notify(); + state.destroyed(surface); + + // Remove the surface after the callback is invoked. + state + .compositor_state() + .surfaces + .retain(|s| s.id() != surface.id()); + PrivateSurfaceData::cleanup(state, self, surface); + } +} + +impl IsAlive for WlSurface { + #[inline] + fn alive(&self) -> bool { + let data: &SurfaceUserData = self.data().unwrap(); + data.alive_tracker.alive() + } +} + +/* + * wl_region + */ + +/// User data of WlRegion +#[derive(Debug)] +pub struct RegionUserData { + pub(crate) inner: Mutex, +} + +impl Dispatch2 for RegionUserData +where + D: CompositorHandler, +{ + fn request( + &self, + state: &mut D, + client: &wayland_server::Client, + _resource: &WlRegion, + request: wl_region::Request, + _dhandle: &DisplayHandle, + _init: &mut wayland_server::DataInit<'_, D>, + ) { + let mut guard = self.inner.lock().unwrap(); + let client_scale = state.client_compositor_state(client).client_scale(); + + match request { + wl_region::Request::Add { x, y, width, height } => { + if width > 0 && height > 0 { + guard.rects.push(( + RectangleKind::Add, + Rectangle::::new((x, y).into(), (width, height).into()) + .to_f64() + .to_logical(client_scale) + .to_i32_round(), + )); + } + } + wl_region::Request::Subtract { x, y, width, height } => { + if width > 0 && height > 0 { + guard.rects.push(( + RectangleKind::Subtract, + Rectangle::::new((x, y).into(), (width, height).into()) + .to_f64() + .to_logical(client_scale) + .to_i32_round(), + )); + } + } + wl_region::Request::Destroy => { + // all is handled by our destructor + } + _ => unreachable!(), + } + } +} + +/* + * wl_subcompositor + */ + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: CompositorHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: CompositorHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + subcompositor: &WlSubcompositor, + request: wl_subcompositor::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_subcompositor::Request::GetSubsurface { id, surface, parent } => { + if let Err(AlreadyHasRole) = PrivateSurfaceData::set_parent(&surface, &parent) { + subcompositor + .post_error(wl_subcompositor::Error::BadSurface, "Surface already has a role."); + return; + } + + data_init.init( + id, + SubsurfaceUserData { + surface: surface.clone(), + }, + ); + + super::with_states(&surface, |states| { + states.data_map.insert_if_missing_threadsafe(SubsurfaceState::new) + }); + + state.new_subsurface(&surface, &parent); + } + wl_subcompositor::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +/* + * wl_subsurface + */ + +/// User data of WlSubsurface +#[derive(Debug)] +pub struct SubsurfaceUserData { + surface: WlSurface, +} + +impl SubsurfaceUserData { + /// Returns the surface for this subsurface (not to be confused with the parent surface). + pub fn surface(&self) -> &WlSurface { + &self.surface + } +} +/// The cached state associated with a subsurface +#[derive(Debug)] +pub struct SubsurfaceCachedState { + /// Location of the top-left corner of this subsurface + /// relative to its parent coordinate space + pub location: Point, +} + +impl Default for SubsurfaceCachedState { + fn default() -> Self { + SubsurfaceCachedState { + location: (0, 0).into(), + } + } +} + +impl Cacheable for SubsurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + Self { + location: self.location, + } + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + into.location = self.location; + } +} + +pub(crate) struct SubsurfaceState { + pub(crate) sync: AtomicBool, +} + +impl SubsurfaceState { + fn new() -> SubsurfaceState { + SubsurfaceState { + sync: AtomicBool::new(true), + } + } +} + +/// Check if a (sub)surface is effectively sync +pub fn is_effectively_sync(surface: &wl_surface::WlSurface) -> bool { + let is_direct_sync = PrivateSurfaceData::with_states(surface, |state| { + state + .data_map + .get::() + .map(|s| s.sync.load(Ordering::Acquire)) + .unwrap_or(false) + }); + if is_direct_sync { + return true; + } + if let Some(parent) = PrivateSurfaceData::get_parent(surface) { + is_effectively_sync(&parent) + } else { + false + } +} + +impl Dispatch2 for SubsurfaceUserData +where + D: CompositorHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + client: &wayland_server::Client, + subsurface: &WlSubsurface, + request: wl_subsurface::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_subsurface::Request::SetPosition { x, y } => { + let client_scale = state.client_compositor_state(client).client_scale(); + PrivateSurfaceData::with_states(&self.surface, |state| { + state + .cached_state + .get::() + .pending() + .location = Point::::from((x, y)) + .to_f64() + .to_logical(client_scale) + .to_i32_round(); + }) + } + wl_subsurface::Request::PlaceAbove { sibling } => { + if let Err(()) = PrivateSurfaceData::reorder(&self.surface, Location::After, &sibling) { + subsurface.post_error( + wl_subsurface::Error::BadSurface, + "Provided surface is not a sibling or parent.", + ) + } + } + wl_subsurface::Request::PlaceBelow { sibling } => { + if let Err(()) = PrivateSurfaceData::reorder(&self.surface, Location::Before, &sibling) { + subsurface.post_error( + wl_subsurface::Error::BadSurface, + "Provided surface is not a sibling or parent.", + ) + } + } + wl_subsurface::Request::SetSync => PrivateSurfaceData::with_states(&self.surface, |state| { + state + .data_map + .get::() + .unwrap() + .sync + .store(true, Ordering::Release); + }), + wl_subsurface::Request::SetDesync => PrivateSurfaceData::with_states(&self.surface, |state| { + state + .data_map + .get::() + .unwrap() + .sync + .store(false, Ordering::Release); + }), + wl_subsurface::Request::Destroy => { + // Our destructor already handles it + } + _ => unreachable!(), + } + } + + fn destroyed( + &self, + _state: &mut D, + _client_id: wayland_server::backend::ClientId, + _object: &WlSubsurface, + ) { + PrivateSurfaceData::unset_parent(&self.surface); + PrivateSurfaceData::with_states(&self.surface, |state| { + state + .data_map + .get::() + .unwrap() + .sync + .store(true, Ordering::Release); + + let mut guard = state.cached_state.get::(); + *guard.pending() = Default::default(); + *guard.current() = Default::default(); + }); + } +} + +impl Dispatch2 for GlobalData +where + D: CompositorHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _subsurface: &WlCallback, + _request: wl_callback::Request, + _handle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } +} diff --git a/vendor/smithay/src/wayland/compositor/mod.rs b/vendor/smithay/src/wayland/compositor/mod.rs new file mode 100644 index 0000000..81bac6d --- /dev/null +++ b/vendor/smithay/src/wayland/compositor/mod.rs @@ -0,0 +1,788 @@ +//! Utilities for handling surfaces, subsurfaces and regions +//! +//! This module provides automatic handling of surfaces, subsurfaces +//! and region Wayland objects, by registering an implementation for +//! for the [`wl_compositor`](wayland_server::protocol::wl_compositor) +//! and [`wl_subcompositor`](wayland_server::protocol::wl_subcompositor) globals. +//! +//! ## Why use this implementation +//! +//! This implementation does a simple job: it stores in a coherent way the state of +//! surface trees with subsurfaces, to provide you direct access to the tree +//! structure and all surface attributes, and handles the application of double-buffered +//! state. +//! +//! As such, you can, given a root surface with a role requiring it to be displayed, +//! you can iterate over the whole tree of subsurfaces to recover all the metadata you +//! need to display the subsurface tree. +//! +//! This implementation will not do anything more than present you the metadata specified by the +//! client in a coherent and practical way. All the logic regarding drawing itself, and +//! the positioning of windows (surface trees) one relative to another is out of its scope. +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation create the [`CompositorState`], store it inside your `State` struct +//! and implement the [`CompositorHandler`], as shown in this example: +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! use smithay::wayland::compositor::{CompositorState, CompositorClientState, CompositorHandler}; +//! +//! # struct State { compositor_state: CompositorState } +//! # struct ClientState { compositor_state: CompositorClientState } +//! # impl wayland_server::backend::ClientData for ClientState {} +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the compositor state +//! let compositor_state = CompositorState::new::( +//! &display.handle(), +//! ); +//! +//! // insert the CompositorState into your state +//! // .. +//! +//! // implement the necessary traits +//! impl CompositorHandler for State { +//! fn compositor_state(&mut self) -> &mut CompositorState { +//! &mut self.compositor_state +//! } +//! +//! fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { +//! &client.get_data::().unwrap().compositor_state +//! } +//! +//! fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) { +//! // called on every buffer commit. +//! // .. your implementation .. +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Use the surface states +//! +//! The main access to surface states is done through the [`with_states`] function, which +//! gives you access to the [`SurfaceData`] instance associated with this surface. It acts +//! as a general purpose container for associating state to a surface, double-buffered or +//! not. See its documentation for more details. +//! +//! ### State application and hooks +//! +//! On commit of a surface several steps are taken to update the state of the surface. Actions +//! are taken by smithay in the following order: +//! +//! 1. Pre Commit hooks registered to this surface are invoked. Such hooks can be registered using +//! the [`add_pre_commit_hook`] function. They are typically used by protocol extensions that +//! add state to a surface and need to check on commit that client did not request an +//! illegal state before it is applied on commit. +//! 2. The pending state is either applied and made current, or cached for later application +//! is the surface is a synchronize subsurface. If the current state is applied, state +//! of the synchronized children subsurface are applied as well at this point. +//! 3. Post Commit hooks registered to this surface are invoked. Such hooks can be registered using +//! the [`add_post_commit_hook`] function. They are typically used by abstractions that further process +//! the state. +//! 4. Your implementation of [`CompositorHandler::commit`] is invoked, so that you can access +//! the new current state of the surface. The state of sync children subsurfaces of your +//! surface may have changed as well, so this is the place to check it, using functions +//! like [`with_surface_tree_upward`] or [`with_surface_tree_downward`]. On the other hand, +//! if the surface is a sync subsurface, its current state will note have changed as +//! the result of that commit. You can check if it is using [`is_sync_subsurface`]. +//! 5. If the surface is destroyed, destruction hooks are invoked. Such hooks can be registered +//! using the [`add_destruction_hook`] function. They are typically used to cleanup associated +//! state. +//! +//! ### Surface roles +//! +//! The wayland protocol specifies that a surface needs to be assigned a role before it can +//! be displayed. Furthermore, a surface can only have a single role during its whole lifetime. +//! Smithay represents this role as a `&'static str` identifier, that can only be set once +//! on a surface. See [`give_role`] and [`get_role`] for details. This module manages the +//! subsurface role, which is identified by the string `"subsurface"`. + +mod cache; +mod handlers; +mod transaction; +mod tree; + +use std::cell::RefCell; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::{any::Any, sync::Mutex}; + +pub use self::cache::{Cacheable, CachedState, MultiCache}; +pub use self::handlers::{RegionUserData, SubsurfaceCachedState, SubsurfaceUserData, SurfaceUserData}; +use self::transaction::TransactionQueue; +pub use self::transaction::{Barrier, Blocker, BlockerState}; +pub use self::tree::{AlreadyHasRole, TraversalAction}; +use self::tree::{PrivateSurfaceData, SuggestedSurfaceState}; +use crate::input::touch::FrameMarker; +use crate::utils::Transform; +pub use crate::utils::hook::HookId; +use crate::utils::{Buffer, Logical, Point, Rectangle, user_data::UserDataMap}; +use crate::wayland::GlobalData; +use portable_atomic::AtomicF64; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_compositor::WlCompositor; +use wayland_server::protocol::wl_subcompositor::WlSubcompositor; +use wayland_server::protocol::{wl_buffer, wl_callback, wl_output, wl_region, wl_surface::WlSurface}; +use wayland_server::{Client, DisplayHandle, GlobalDispatch, Resource}; + +/// The role of a subsurface surface. +pub const SUBSURFACE_ROLE: &str = "subsurface"; + +/// Description of a part of a surface that +/// should be considered damaged and needs to be redrawn +#[derive(Debug, PartialEq, Eq)] +pub enum Damage { + /// A rectangle containing the damaged zone, in surface coordinates + Surface(Rectangle), + /// A rectangle containing the damaged zone, in buffer coordinates + /// + /// Note: Buffer scaling must be taken into consideration + Buffer(Rectangle), +} + +/// The state container associated with a surface +/// +/// This general-purpose container provides 2 main storages: +/// +/// - the `data_map` storage has typemap semantics and allows you +/// to associate and access non-buffered data to the surface +/// - the `cached_state` storages allows you to associate state to +/// the surface that follows the double-buffering semantics associated +/// with the `commit` procedure of surfaces, also with typemap-like +/// semantics +/// +/// See the respective documentation of each container for its usage. +/// +/// By default, all surfaces have a [`SurfaceAttributes`] cached state, +/// and subsurface also have a [`SubsurfaceCachedState`] state as well. +#[derive(Debug)] +pub struct SurfaceData { + /// The current role of the surface. + /// + /// If `None` if the surface has not yet been assigned a role + pub role: Option<&'static str>, + /// The non-buffered typemap storage of this surface + pub data_map: UserDataMap, + /// The double-buffered typemap storage of this surface + pub cached_state: MultiCache, +} + +/// New buffer assignation for a surface +#[derive(Debug)] +pub enum BufferAssignment { + /// The surface no longer has a buffer attached to it + Removed, + /// A new buffer has been attached + NewBuffer(wl_buffer::WlBuffer), +} + +/// General state associated with a surface +/// +/// The fields `buffer`, `damage` and `frame_callbacks` should be +/// reset (by clearing their contents) once you have adequately +/// processed them, as their contents are aggregated from commit to commit. +#[derive(Debug)] +pub struct SurfaceAttributes { + /// Buffer defining the contents of the surface + /// + /// You are free to set this field to `None` to avoid processing it several + /// times. It'll be set to `Some(...)` if the user attaches a buffer (or `NULL`) to + /// the surface, and be left to `None` if the user does not attach anything. + pub buffer: Option, + + /// Location of the new buffer relative to the previous one + /// + /// The x and y arguments specify the location of the new pending buffer's upper left corner, + /// relative to the current buffer's upper left corner, in surface-local coordinates. + /// + /// In other words, the x and y, combined with the new surface size define in which directions + /// the surface's size changes. + /// + /// You are free to set this field to `None` to avoid processing it several times. + pub buffer_delta: Option>, + + /// Scale of the contents of the buffer, for higher-resolution contents. + /// + /// If it matches the one of the output displaying this surface, no change + /// is necessary. + pub buffer_scale: i32, + /// Transform under which interpret the contents of the buffer + /// + /// If it matches the one of the output displaying this surface, no change + /// is necessary. + pub buffer_transform: wl_output::Transform, + /// Region of the surface that is guaranteed to be opaque + /// + /// By default the whole surface is potentially transparent + pub opaque_region: Option, + /// Region of the surface that is sensitive to user input + /// + /// By default the whole surface should be sensitive + pub input_region: Option, + /// Damage rectangle + /// + /// Hint provided by the client to suggest that only this part + /// of the surface was changed and needs to be redrawn + pub damage: Vec, + /// The frame callbacks associated with this surface for the commit + /// + /// The server must send the notifications so that a client + /// will not send excessive updates, while still allowing + /// the highest possible update rate for clients that wait for the reply + /// before drawing again. The server should give some time for the client + /// to draw and commit after sending the frame callback events to let it + /// hit the next output refresh. + /// + /// A server should avoid signaling the frame callbacks if the + /// surface is not visible in any way, e.g. the surface is off-screen, + /// or completely obscured by other opaque surfaces. + /// + /// An example possibility would be to trigger it once the frame + /// associated with this commit has been displayed on the screen. + pub frame_callbacks: Vec, + + pub(crate) client_scale: f64, +} + +impl Default for SurfaceAttributes { + fn default() -> SurfaceAttributes { + SurfaceAttributes { + buffer: None, + buffer_delta: None, + buffer_scale: 1, + buffer_transform: wl_output::Transform::Normal, + opaque_region: None, + input_region: None, + damage: Vec::new(), + frame_callbacks: Vec::new(), + client_scale: 1., + } + } +} + +/// Kind of a rectangle part of a region +#[derive(Copy, Clone, Debug)] +pub enum RectangleKind { + /// This rectangle should be added to the region + Add, + /// The intersection of this rectangle with the region should + /// be removed from the region + Subtract, +} + +/// Description of the contents of a region +/// +/// A region is defined as an union and difference of rectangle. +/// +/// This struct contains an ordered `Vec` containing the rectangles defining +/// a region. They should be added or subtracted in this order to compute the +/// actual contents of the region. +#[derive(Clone, Debug, Default)] +pub struct RegionAttributes { + /// List of rectangle part of this region + pub rects: Vec<(RectangleKind, Rectangle)>, +} + +impl RegionAttributes { + /// Checks whether given point is inside the region. + pub fn contains>>(&self, point: P) -> bool { + let point: Point = point.into(); + let mut contains = false; + for (kind, rect) in &self.rects { + if rect.contains(point) { + match kind { + RectangleKind::Add => contains = true, + RectangleKind::Subtract => contains = false, + } + } + } + contains + } +} + +/// Access the data of a surface tree from bottom to top +/// +/// You provide three closures, a "filter", a "processor" and a "post filter". +/// +/// The first closure is initially called on a surface to determine if its children +/// should be processed as well. It returns a `TraversalAction` reflecting that. +/// +/// The second closure is supposed to do the actual processing. The processing closure for +/// a surface may be called after the processing closure of some of its children, depending +/// on the stack ordering the client requested. Here the surfaces are processed in the same +/// order as they are supposed to be drawn: from the farthest of the screen to the nearest. +/// +/// The third closure is called once all the subtree of a node has been processed, and gives +/// an opportunity for early-stopping. If it returns `true` the processing will continue, +/// while if it returns `false` it'll stop. +/// +/// The arguments provided to the closures are, in this order: +/// +/// - The surface object itself +/// - a mutable reference to its surface attribute data +/// - a mutable reference to its role data, +/// - a custom value that is passed in a fold-like manner, but only from the output of a parent +/// to its children. See [`TraversalAction`] for details. +/// +/// If the surface not managed by the `CompositorGlobal` that provided this token, this +/// will panic (having more than one compositor is not supported). +pub fn with_surface_tree_upward( + surface: &WlSurface, + initial: T, + filter: F1, + processor: F2, + post_filter: F3, +) where + F1: FnMut(&WlSurface, &SurfaceData, &T) -> TraversalAction, + F2: FnMut(&WlSurface, &SurfaceData, &T), + F3: FnMut(&WlSurface, &SurfaceData, &T) -> bool, +{ + PrivateSurfaceData::map_tree(surface, &initial, filter, processor, post_filter, false); +} + +/// Access the data of a surface tree from top to bottom +/// +/// Behavior is the same as [`with_surface_tree_upward`], but the processing is done in the reverse order, +/// from the nearest of the screen to the deepest. +/// +/// This would typically be used to find out which surface of a subsurface tree has been clicked for example. +pub fn with_surface_tree_downward( + surface: &WlSurface, + initial: T, + filter: F1, + processor: F2, + post_filter: F3, +) where + F1: FnMut(&WlSurface, &SurfaceData, &T) -> TraversalAction, + F2: FnMut(&WlSurface, &SurfaceData, &T), + F3: FnMut(&WlSurface, &SurfaceData, &T) -> bool, +{ + PrivateSurfaceData::map_tree(surface, &initial, filter, processor, post_filter, true); +} + +/// Retrieve the parent of this surface +/// +/// Returns `None` is this surface is a root surface +pub fn get_parent(surface: &WlSurface) -> Option { + PrivateSurfaceData::get_parent(surface) +} + +/// Retrieve the children of this surface +pub fn get_children(surface: &WlSurface) -> Vec { + PrivateSurfaceData::get_children(surface) +} + +/// Check if this subsurface is a synchronized subsurface +pub fn is_sync_subsurface(surface: &WlSurface) -> bool { + self::handlers::is_effectively_sync(surface) +} + +/// Get the current role of this surface +pub fn get_role(surface: &WlSurface) -> Option<&'static str> { + PrivateSurfaceData::get_role(surface) +} + +/// Register that this surface has given role +/// +/// Fails if the surface already has a role. +pub fn give_role(surface: &WlSurface, role: &'static str) -> Result<(), AlreadyHasRole> { + PrivateSurfaceData::set_role(surface, role) +} + +/// Access the states associated to this surface +pub fn with_states(surface: &WlSurface, f: F) -> T +where + F: FnOnce(&SurfaceData) -> T, +{ + PrivateSurfaceData::with_states(surface, f) +} + +/// Send the `scale` and `transform` preferences for the given surface when it supports them. +/// +/// The new state is only send when it differs from the already cached one on the calling thread. +pub fn send_surface_state(surface: &WlSurface, data: &SurfaceData, scale: i32, transform: Transform) { + if surface.version() < 6 { + return; + } + + // NOTE we insert default for checks below to work properly. + let mut storage = data + .data_map + .get_or_insert(|| RefCell::new(SuggestedSurfaceState::default())) + .borrow_mut(); + + if storage.scale != scale { + surface.preferred_buffer_scale(scale); + storage.scale = scale; + } + + let transform = transform.into(); + if storage.transform != transform { + surface.preferred_buffer_transform(transform); + storage.transform = transform; + } +} + +/// Retrieve the metadata associated with a `wl_region` +/// +/// If the region is not managed by the `CompositorGlobal` that provided this token, this +/// will panic (having more than one compositor is not supported). +pub fn get_region_attributes(region: &wl_region::WlRegion) -> RegionAttributes { + match region.data::() { + Some(data) => data.inner.lock().unwrap().clone(), + None => panic!("Accessing the data of foreign regions is not supported."), + } +} + +/// Register a pre-commit hook to be invoked on surface commit +/// +/// It'll be invoked on surface commit, *before* the new state is merged into the current state. +/// +/// Protocol implementations should use this for error checking, but they should **not** apply +/// state changes here, since the commit may be further arbitrarily delayed by blockers. Use a +/// post-commit hook to apply state changes (i.e. copy last acked state to current). +/// +/// Compositors should use this for adding blockers if needed, e.g. the DMA-BUF readiness blocker. +pub fn add_pre_commit_hook(surface: &WlSurface, hook: F) -> HookId +where + F: Fn(&mut D, &DisplayHandle, &WlSurface) + Send + Sync + 'static, + D: 'static, +{ + let (user_state_type_id, user_state_type) = surface.data::().unwrap().user_state_type; + assert_eq!( + std::any::TypeId::of::(), + user_state_type_id, + "D has to equal D used in CompositorState::new(), {} != {}", + std::any::type_name::(), + user_state_type, + ); + + let hook = move |state: &mut dyn Any, dh: &DisplayHandle, surface: &WlSurface| { + let state = state.downcast_mut::().unwrap(); + hook(state, dh, surface); + }; + PrivateSurfaceData::add_pre_commit_hook(surface, hook) +} + +/// Register a post-commit hook to be invoked on surface commit +/// +/// It'll be invoked on surface commit, *after* the new state is merged into the current state, +/// after all commit blockers complete. +/// +/// Protocol implementations should apply state changes here, i.e. copy last acked state into +/// current. +pub fn add_post_commit_hook(surface: &WlSurface, hook: F) -> HookId +where + F: Fn(&mut D, &DisplayHandle, &WlSurface) + Send + Sync + 'static, + D: 'static, +{ + let (user_state_type_id, user_state_type) = surface.data::().unwrap().user_state_type; + assert_eq!( + std::any::TypeId::of::(), + user_state_type_id, + "D has to equal D used in CompositorState::new(), {} != {}", + std::any::type_name::(), + user_state_type, + ); + + let hook = move |state: &mut dyn Any, dh: &DisplayHandle, surface: &WlSurface| { + let state = state.downcast_mut::().unwrap(); + hook(state, dh, surface); + }; + PrivateSurfaceData::add_post_commit_hook(surface, hook) +} + +/// Register a destruction hook to be invoked on surface destruction +/// +/// It'll be invoked when the surface is destroyed (either explicitly by the client or on +/// client disconnect). +/// +/// D generic is the compositor state, same as used in `CompositorState::new()` +pub fn add_destruction_hook(surface: &WlSurface, hook: F) -> HookId +where + F: Fn(&mut D, &WlSurface) + Send + Sync + 'static, + D: 'static, +{ + let (user_state_type_id, user_state_type) = surface.data::().unwrap().user_state_type; + assert_eq!( + std::any::TypeId::of::(), + user_state_type_id, + "D has to equal D used in CompositorState::new(), {} != {}", + std::any::type_name::(), + user_state_type, + ); + + let hook = move |state: &mut dyn Any, surface: &WlSurface| { + let state = state.downcast_mut::().unwrap(); + hook(state, surface); + }; + PrivateSurfaceData::add_destruction_hook(surface, hook) +} + +/// Unregister a pre-commit hook +pub fn remove_pre_commit_hook(surface: &WlSurface, hook_id: &HookId) { + PrivateSurfaceData::remove_pre_commit_hook(surface, hook_id) +} + +/// Unregister a post-commit hook +pub fn remove_post_commit_hook(surface: &WlSurface, hook_id: &HookId) { + PrivateSurfaceData::remove_post_commit_hook(surface, hook_id) +} + +/// Unregister a destruction hook +pub fn remove_destruction_hook(surface: &WlSurface, hook_id: &HookId) { + PrivateSurfaceData::remove_destruction_hook(surface, hook_id) +} + +/// Adds a blocker for the currently queued up state changes of the given surface. +/// +/// Blockers will delay the pending state to be applied on the next commit until +/// all of them return the state `Released`. Any blocker returning `Cancelled` will +/// discard all changes. +/// +/// The module will only evaluate blocker states on commit. If a blocker +/// becomes ready later, a call to [`CompositorClientState::blocker_cleared`] is necessary +/// to trigger a re-evaluation. +pub fn add_blocker(surface: &WlSurface, blocker: impl Blocker + Send + 'static) { + PrivateSurfaceData::add_blocker(surface, blocker) +} + +/// Handler trait for compositor +pub trait CompositorHandler { + /// [CompositorState] getter + fn compositor_state(&mut self) -> &mut CompositorState; + /// [CompositorClientState] getter + /// + /// The compositor implementation needs some state to be client specific. + /// Downstream is expected to store this inside its `ClientData` implementation(s) + /// to ensure automatic cleanup of the state, when the client disconnects. + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState; + + /// New surface handler. + /// + /// This handler can be used to setup hooks (see [`add_pre_commit_hook`]/[`add_post_commit_hook`]/[`add_destruction_hook`]), + /// but not much else. The surface has no role or attached data at this point and cannot be rendered. + fn new_surface(&mut self, surface: &WlSurface) { + let _ = surface; + } + + /// New subsurface handler. + /// + /// This handler can be used to run extra logic when subsurface is getting created. This + /// is an addition to [`new_surface`], which will be run for the subsurface surface anyway. + /// + /// When your compositor knows beforehand where it'll position subsurfaces it can send + /// [`send_surface_state`] to them. + /// + /// [`new_surface`]: Self::new_surface + fn new_subsurface(&mut self, surface: &WlSurface, parent: &WlSurface) { + let _ = surface; + let _ = parent; + } + + /// Surface commit handler + /// + /// This is called when any changed state from a commit actually becomes visible. + /// That might be some time after the actual commit has taken place, if the + /// state changes are delayed by an added blocker (see [`add_blocker`]). + /// + /// If you need to handle a commit as soon as it occurs, you might want to consider + /// using a pre-commit hook (see [`add_pre_commit_hook`]). + fn commit(&mut self, surface: &WlSurface); + + /// The surface was destroyed. + /// + /// This allows the compositor to clean up any uses of the surface. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn destroyed(&mut self, _surface: &WlSurface) {} +} + +/// State of a compositor +#[derive(Debug)] +pub struct CompositorState { + compositor: GlobalId, + subcompositor: GlobalId, + surfaces: Vec, +} + +/// Per-client state of a compositor +#[derive(Debug)] +pub struct CompositorClientState { + queue: Mutex>, + scale_override: Arc, + last_touch_frame: Mutex>, +} + +impl Default for CompositorClientState { + fn default() -> Self { + CompositorClientState { + queue: Mutex::new(None), + scale_override: Arc::new(AtomicF64::new(1.)), + last_touch_frame: Mutex::new(None), + } + } +} + +impl CompositorClientState { + /// To be called, when a previously added blocker (via [`add_blocker`]) + /// got `Released` or `Cancelled` from being `Pending` previously for any + /// surface belonging to this client. + pub fn blocker_cleared(&self, state: &mut D, dh: &DisplayHandle) { + let transactions = if let Some(queue) = self.queue.lock().unwrap().as_mut() { + queue.take_ready() + } else { + Vec::new() + }; + + for transaction in transactions { + transaction.apply(dh, state) + } + } + + /// Set an additionally mapping between smithay's `Logical` coordinate space + /// and this clients logical coordinate space. + /// + /// This is used in the same way as if the client was setting the + /// surface.buffer_scale on every surface i.e a value of 2.0 will make + /// the windows appear smaller on a regular DPI monitor. + /// + /// Only the minimal set of protocols used by xwayland are guaranteed to be supported. + /// + /// Buffer sizes are unaffected. + pub fn set_client_scale(&self, new_scale: f64) { + self.scale_override.store(new_scale, Ordering::Release); + } + + /// Get the scale factor of the additional mapping between smithay's `Logical` + /// coordinate space and this clients logical coordinate space. + /// + /// This is mainly intended to support out-of-tree protocol implementations. + pub fn client_scale(&self) -> f64 { + self.scale_override.load(Ordering::Acquire) + } + + pub(crate) fn clone_client_scale(&self) -> Arc { + self.scale_override.clone() + } + + pub(crate) fn set_last_touch_frame(&self, frame_marker: FrameMarker) { + *self.last_touch_frame.lock().unwrap() = Some(frame_marker); + } + + pub(crate) fn last_touch_frame(&self) -> Option { + *self.last_touch_frame.lock().unwrap() + } +} + +impl CompositorState { + /// Create new [`wl_compositor`] version 5 and [`wl_subcompositor`] globals. + /// + /// It returns the two global handles, in case you wish to remove these globals from + /// the event loop in the future. + /// + /// [`wl_compositor`]: wayland_server::protocol::wl_compositor + /// [`wl_subcompositor`]: wayland_server::protocol::wl_subcompositor + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch + GlobalDispatch + 'static, + { + Self::new_with_version::(display, 5) + } + + /// The same as [`new`], but binds at least version 6 of [`wl_compositor`]. + /// + /// This means that for clients to scale and apply transformation with + /// non-default values [`send_surface_state`] must be used. + /// + /// [`new`]: Self::new + /// [`wl_compositor`]: wayland_server::protocol::wl_compositor + pub fn new_v6(display: &DisplayHandle) -> Self + where + D: GlobalDispatch + GlobalDispatch + 'static, + { + Self::new_with_version::(display, 6) + } + + fn new_with_version(display: &DisplayHandle, version: u32) -> Self + where + D: GlobalDispatch + GlobalDispatch + 'static, + { + let compositor = display.create_global::(version, GlobalData); + let subcompositor = display.create_global::(1, GlobalData); + + CompositorState { + compositor, + subcompositor, + surfaces: Vec::new(), + } + } + + /// Get id of compositor global + pub fn compositor_global(&self) -> GlobalId { + self.compositor.clone() + } + + /// Get id of subcompositor global + pub fn subcompositor_global(&self) -> GlobalId { + self.subcompositor.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn region_attributes_empty() { + let region = RegionAttributes { rects: vec![] }; + assert!(!region.contains((0, 0))); + } + + #[test] + fn region_attributes_add() { + let region = RegionAttributes { + rects: vec![(RectangleKind::Add, Rectangle::from_size((10, 10).into()))], + }; + + assert!(region.contains((0, 0))); + } + + #[test] + fn region_attributes_add_subtract() { + let region = RegionAttributes { + rects: vec![ + (RectangleKind::Add, Rectangle::from_size((10, 10).into())), + (RectangleKind::Subtract, Rectangle::from_size((5, 5).into())), + ], + }; + + assert!(!region.contains((0, 0))); + assert!(region.contains((5, 5))); + } + + #[test] + fn region_attributes_add_subtract_add() { + let region = RegionAttributes { + rects: vec![ + (RectangleKind::Add, Rectangle::from_size((10, 10).into())), + (RectangleKind::Subtract, Rectangle::from_size((5, 5).into())), + (RectangleKind::Add, Rectangle::new((2, 2).into(), (2, 2).into())), + ], + }; + + assert!(!region.contains((0, 0))); + assert!(region.contains((5, 5))); + assert!(region.contains((2, 2))); + } +} diff --git a/vendor/smithay/src/wayland/compositor/transaction.rs b/vendor/smithay/src/wayland/compositor/transaction.rs new file mode 100644 index 0000000..41083d6 --- /dev/null +++ b/vendor/smithay/src/wayland/compositor/transaction.rs @@ -0,0 +1,351 @@ +// The transaction model for handling surface states in Smithay +// +// The caching logic in `cache.rs` provides surfaces with a queue of +// pending states identified with numeric commit ids, allowing the compositor +// to precisely control *when* a state become active. This file is the second +// half: these identified states are grouped into transactions, which allow the +// synchronization of updates across surfaces. +// +// There are 2 main cases when the state of multiple surfaces must be updated +// atomically: +// - synchronized subsurface must have their state updated at the same time as their parents +// - The upcoming `wp_transaction` protocol +// +// In these situations, the individual states in a surface queue are grouped into a transaction +// and are all applied atomically when the transaction itself is applied. The logic for creating +// new transactions is currently the following: +// +// - Each surface has an implicit "pending" transaction, into which its newly committed state is +// recorded +// - Furthermore, on commit, the pending transaction of all synchronized child subsurfaces is merged +// into the current surface's pending transaction, and a new implicit transaction is started for those +// children (logic is implemented in `handlers.rs`, in `PrivateSurfaceData::commit`). +// - Then, still on commit, if the surface is not a synchronized subsurface, its pending transaction is +// directly applied +// +// This last step will change once we have support for explicit synchronization (and further in the future, +// of the wp_transaction protocol). Explicit synchronization introduces a notion of blockers: the transaction +// cannot be applied before all blockers are released, and thus must wait for it to be the case. +// +// For those situations, the (currently unused) `TransactionQueue` will come into play. It is a per-client +// queue of transactions, that stores and applies them by both respecting their topological order +// (ensuring that for each surface, states are applied in the correct order) and that all transactions +// wait before all their blockers are resolved to be merged. If a blocker is cancelled, the whole transaction +// it blocks is cancelled as well, and simply dropped. Thanks to the logic of `Cache::apply_state`, the +// associated state will be applied automatically when the next valid transaction is applied, ensuring +// global coherence. + +// A significant part of the logic of this module is not yet used, +// but will be once proper transaction & blockers support is +// added to smithay +use std::{ + collections::HashSet, + fmt, + sync::{Arc, Mutex, atomic::AtomicBool}, +}; + +use wayland_server::{DisplayHandle, Resource, Weak, protocol::wl_surface::WlSurface}; + +use crate::utils::Serial; + +use super::{CompositorHandler, tree::PrivateSurfaceData}; + +/// Types potentially blocking state changes +pub trait Blocker { + /// Retrieve the current state of the blocker + fn state(&self) -> BlockerState; +} + +/// States of a [`Blocker`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BlockerState { + /// The block is pending and not resolved yet + Pending, + /// The block got released and changes can be applied + Released, + /// The block got cancelled and changes should be discarded + Cancelled, +} + +/// A simple [`Blocker`] barrier +#[derive(Debug, Clone)] +pub struct Barrier(Arc); + +impl PartialEq for Barrier { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} +impl Eq for Barrier {} + +impl Barrier { + /// Initialize a new [`Barrier`] with the provided state + pub fn new(signaled: bool) -> Self { + Self(Arc::new(AtomicBool::new(signaled))) + } + + /// Query if this barrier has been signaled + #[inline] + pub fn is_signaled(&self) -> bool { + self.0.load(std::sync::atomic::Ordering::Acquire) + } + + /// Signal this barrier + #[inline] + pub fn signal(&self) { + self.0.store(true, std::sync::atomic::Ordering::Release) + } +} + +impl Blocker for Barrier { + fn state(&self) -> BlockerState { + if self.is_signaled() { + BlockerState::Released + } else { + BlockerState::Pending + } + } +} + +#[derive(Default)] +struct TransactionState { + surfaces: Vec<(Weak, Serial)>, + blockers: Vec>, +} + +impl TransactionState { + fn insert(&mut self, surface: WlSurface, id: Serial) { + if let Some(place) = self.surfaces.iter_mut().find(|place| place.0 == surface) { + // the surface is already in the list, update the serial + if place.1 < id { + place.1 = id; + } + } else { + // the surface is not in the list, insert it + self.surfaces.push((surface.downgrade(), id)); + } + } +} + +enum TransactionInner { + Data(TransactionState), + Fused(Arc>), +} + +pub(crate) struct PendingTransaction { + inner: Arc>, +} + +impl Default for PendingTransaction { + fn default() -> Self { + PendingTransaction { + inner: Arc::new(Mutex::new(TransactionInner::Data(Default::default()))), + } + } +} + +impl PendingTransaction { + fn with_inner_state T>(&self, f: F) -> T { + let mut next = self.inner.clone(); + loop { + let tmp = match *next.lock().unwrap() { + TransactionInner::Data(ref mut state) => return f(state), + TransactionInner::Fused(ref into) => into.clone(), + }; + next = tmp; + } + } + + pub(crate) fn insert_state(&self, surface: WlSurface, id: Serial) { + self.with_inner_state(|state| state.insert(surface, id)) + } + + pub(crate) fn add_blocker(&self, blocker: B) { + self.with_inner_state(|state| state.blockers.push(Box::new(blocker) as Box<_>)) + } + + pub(crate) fn is_same_as(&self, other: &PendingTransaction) -> bool { + let ptr1 = self.with_inner_state(|state| state as *const _); + let ptr2 = other.with_inner_state(|state| state as *const _); + ptr1 == ptr2 + } + + pub(crate) fn merge_into(&self, into: &PendingTransaction) { + if self.is_same_as(into) { + // nothing to do + return; + } + // extract our pending surfaces and change our link + let mut next = self.inner.clone(); + let my_state; + loop { + let tmp = { + let mut guard = next.lock().unwrap(); + match *guard { + TransactionInner::Data(ref mut state) => { + my_state = std::mem::take(state); + *guard = TransactionInner::Fused(into.inner.clone()); + break; + } + TransactionInner::Fused(ref into) => into.clone(), + } + }; + next = tmp; + } + // fuse our surfaces into our new transaction state + self.with_inner_state(|state| { + for (surface, id) in my_state.surfaces { + if let Ok(surface) = surface.upgrade() { + state.insert(surface, id); + } + } + state.blockers.extend(my_state.blockers); + }); + } + + pub(crate) fn finalize(mut self) -> Transaction { + // When finalizing a transaction, this *must* be the last handle to this transaction + loop { + let inner = match Arc::try_unwrap(self.inner) { + Ok(mutex) => mutex.into_inner().unwrap(), + Err(_) => panic!("Attempting to finalize a transaction but handle is not the last."), + }; + match inner { + TransactionInner::Data(TransactionState { + surfaces, blockers, .. + }) => return Transaction { surfaces, blockers }, + TransactionInner::Fused(into) => self.inner = into, + } + } + } +} + +#[derive(Debug)] +pub(crate) struct Transaction { + surfaces: Vec<(Weak, Serial)>, + blockers: Vec>, +} + +impl fmt::Debug for Box { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Blocker").field("state", &self.state()).finish() + } +} + +impl Transaction { + /// Computes the global state of the transaction with regard to its blockers + /// + /// The logic is: + /// + /// - if at least one blocker is cancelled, the transaction is cancelled + /// - otherwise, if at least one blocker is pending, the transaction is pending + /// - otherwise, all blockers are released, and the transaction is also released + pub(crate) fn state(&self) -> BlockerState { + // In case all of our surfaces have been destroyed we can cancel this transaction + // as we won't apply its state anyway + if !self.surfaces.iter().any(|surface| surface.0.is_alive()) { + return BlockerState::Cancelled; + } + + use BlockerState::*; + self.blockers + .iter() + .fold(Released, |acc, blocker| match (acc, blocker.state()) { + (Cancelled, _) | (_, Cancelled) => Cancelled, + (Pending, _) | (_, Pending) => Pending, + (Released, Released) => Released, + }) + } + + pub(crate) fn apply(self, dh: &DisplayHandle, state: &mut C) { + for (surface, id) in self.surfaces { + let Ok(surface) = surface.upgrade() else { + continue; + }; + + PrivateSurfaceData::with_states(&surface, |states| { + states.cached_state.apply_state(id, dh); + }); + + PrivateSurfaceData::invoke_post_commit_hooks::(state, dh, &surface); + + tracing::trace!("Calling user implementation for wl_surface.commit"); + + state.commit(&surface); + } + } +} + +// This queue should be per-client +#[derive(Debug, Default)] +pub(crate) struct TransactionQueue { + transactions: Vec, + // we keep the hashset around to reuse allocations + seen_surfaces: HashSet, +} + +impl TransactionQueue { + pub(crate) fn append(&mut self, t: Transaction) { + self.transactions.push(t); + } + + pub(crate) fn take_ready(&mut self) -> Vec { + // FIXME: Get rid of this allocation here + let mut ready_transactions = Vec::new(); + // this is a very non-optimized implementation + // we just iterate over the queue of transactions, keeping track of which + // surface we have seen as they encode transaction dependencies + self.seen_surfaces.clear(); + // manually iterate as we're going to modify the Vec while iterating on it + let mut i = 0; + // the loop will terminate, as at every iteration either i is incremented by 1 + // or the length of self.transactions is reduced by 1. + while i < self.transactions.len() { + let mut skip = false; + // does the transaction have any active blocker? + match self.transactions[i].state() { + BlockerState::Cancelled => { + // this transaction is cancelled, remove it without further processing + self.transactions.remove(i); + continue; + } + BlockerState::Pending => { + skip = true; + } + BlockerState::Released => {} + } + // if not, does this transaction depend on any previous transaction? + if !skip { + for (s, _) in &self.transactions[i].surfaces { + // TODO: is this alive check still needed? + if !s.is_alive() { + continue; + } + if self.seen_surfaces.contains(&s.id().protocol_id()) { + skip = true; + break; + } + } + } + + if skip { + // this transaction is not yet ready and should be skipped, add its surfaces to our + // seen list + for (s, _) in &self.transactions[i].surfaces { + // TODO: is this alive check still needed? + if !s.is_alive() { + continue; + } + self.seen_surfaces.insert(s.id().protocol_id()); + } + i += 1; + } else { + // this transaction is to be applied, yay! + ready_transactions.push(self.transactions.remove(i)); + } + } + + ready_transactions + } +} diff --git a/vendor/smithay/src/wayland/compositor/tree.rs b/vendor/smithay/src/wayland/compositor/tree.rs new file mode 100644 index 0000000..a563576 --- /dev/null +++ b/vendor/smithay/src/wayland/compositor/tree.rs @@ -0,0 +1,551 @@ +use crate::{ + utils::{ + Serial, + hook::{Hook, HookId}, + }, + wayland::compositor::SUBSURFACE_ROLE, +}; + +use super::{ + BufferAssignment, CompositorHandler, SurfaceAttributes, SurfaceData, + cache::MultiCache, + handlers::{SurfaceUserData, is_effectively_sync}, + transaction::{Blocker, PendingTransaction, TransactionQueue}, +}; +use std::{ + any::Any, + fmt, + sync::{Arc, Mutex, MutexGuard, atomic::Ordering}, +}; +use wayland_server::{ + DisplayHandle, Resource, + protocol::{wl_output::Transform, wl_surface::WlSurface}, +}; + +type CommitHook = dyn Fn(&mut dyn Any, &DisplayHandle, &WlSurface) + Send + Sync; +type DestructionHook = dyn Fn(&mut dyn Any, &WlSurface) + Send + Sync; + +/// Node of a subsurface tree, holding some user specified data type U +/// at each node +/// +/// This type is internal to Smithay, and should not appear in the +/// public API +/// +/// It is a bidirectional tree, meaning we can move along it in both +/// direction (top-bottom or bottom-up). We are taking advantage of the +/// fact that lifetime of objects are decided by Wayland-server to ensure +/// the cleanup will be done properly, and we won't leak anything. +/// +/// Each node also appears within its children list, to allow relative placement +/// between them. +pub(crate) struct PrivateSurfaceData { + parent: Option, + children: Vec, + public_data: SurfaceData, + pending_transaction: PendingTransaction, + current_txid: Serial, + pre_commit_hooks: Vec>, + post_commit_hooks: Vec>, + destruction_hooks: Vec>, +} + +impl fmt::Debug for PrivateSurfaceData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PrivateSurfaceData") + .field("parent", &self.parent) + .field("children", &self.children) + .field("public_data", &self.public_data) + .field("pending_transaction", &"...") + .field("current_txid", &self.current_txid) + .field("commit_hooks", &"...") + .field("pre_commit_hooks.len", &self.pre_commit_hooks.len()) + .field("post_commit_hooks.len", &self.post_commit_hooks.len()) + .field("destruction_hooks.len", &self.destruction_hooks.len()) + .finish() + } +} + +/// An error type signifying that the surface already has a role and +/// cannot be assigned an other +/// +/// Generated if you attempt a role operation on a surface that does +/// not have the role you asked for. +#[derive(Debug)] +pub struct AlreadyHasRole; + +impl std::fmt::Display for AlreadyHasRole { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Surface already has a role.") + } +} + +impl std::error::Error for AlreadyHasRole {} + +pub enum Location { + Before, + After, +} + +/// Possible actions to do after handling a node during tree traversal +#[derive(Debug)] +pub enum TraversalAction { + /// Traverse its children as well, providing them the data T + DoChildren(T), + /// Skip its children + SkipChildren, + /// Stop traversal completely + Break, +} + +impl PrivateSurfaceData { + pub fn new() -> Mutex { + Mutex::new(PrivateSurfaceData { + parent: None, + children: vec![], + public_data: SurfaceData { + role: Default::default(), + data_map: Default::default(), + cached_state: MultiCache::new(), + }, + pending_transaction: Default::default(), + current_txid: Serial(0), + pre_commit_hooks: Vec::new(), + post_commit_hooks: Vec::new(), + destruction_hooks: Vec::new(), + }) + } + + /// Initializes the surface, must be called at creation for state coherence + pub fn init(surface: &WlSurface) { + let mut my_data = Self::lock_user_data(surface); + debug_assert!(my_data.children.is_empty()); + my_data.children.push(surface.clone()); + } + + /// Cleans the `as_ref().user_data` of that surface, must be called when it is destroyed + pub fn cleanup(state: &mut D, surface_data: &SurfaceUserData, surface: &WlSurface) { + // Don't hold the lock on `surface_data.inner`, while also locking the parent + let old_parent = surface_data.inner.lock().unwrap().parent.take(); + if let Some(old_parent) = old_parent { + // We had a parent, lets unregister ourselves from it + let old_parent_mutex = &old_parent.data::().unwrap().inner; + let mut old_parent_guard = old_parent_mutex.lock().unwrap(); + old_parent_guard.children.retain(|c| c.id() != surface.id()); + } + + let my_data_mutex = &surface_data.inner; + let mut my_data = my_data_mutex.lock().unwrap(); + // orphan all our children + for child in my_data.children.drain(..) { + let child_mutex = &child.data::().unwrap().inner; + if std::ptr::eq(child_mutex, my_data_mutex) { + // This child is ourselves, don't do anything. + continue; + } + + let mut child_guard = child_mutex.lock().unwrap(); + child_guard.parent = None; + } + let mut guard = my_data.public_data.cached_state.get::(); + if let Some(BufferAssignment::NewBuffer(buffer)) = guard.current().buffer.take() { + buffer.release(); + }; + if let Some(BufferAssignment::NewBuffer(buffer)) = guard.pending().buffer.take() { + buffer.release(); + }; + + let hooks = my_data.destruction_hooks.clone(); + // don't hold the mutex while the hooks are invoked + drop(guard); + drop(my_data); + for hook in hooks { + (hook.cb)(state, surface) + } + } + + pub fn lock_user_data(surface: &WlSurface) -> MutexGuard<'_, PrivateSurfaceData> { + surface.data::().unwrap().inner.lock().unwrap() + } + + pub fn set_role(surface: &WlSurface, role: &'static str) -> Result<(), AlreadyHasRole> { + let mut my_data = Self::lock_user_data(surface); + if my_data.public_data.role.is_some() && my_data.public_data.role != Some(role) { + return Err(AlreadyHasRole); + } + my_data.public_data.role = Some(role); + Ok(()) + } + + pub fn get_role(surface: &WlSurface) -> Option<&'static str> { + Self::lock_user_data(surface).public_data.role + } + + pub fn with_states T>(surface: &WlSurface, f: F) -> T { + let guard = Self::lock_user_data(surface); + f(&guard.public_data) + } + + pub fn add_blocker(surface: &WlSurface, blocker: impl Blocker + Send + 'static) { + Self::lock_user_data(surface) + .pending_transaction + .add_blocker(blocker) + } + + pub fn remove_pre_commit_hook(surface: &WlSurface, hook_id: &HookId) { + Self::lock_user_data(surface) + .pre_commit_hooks + .retain(|hook| hook.id != *hook_id); + } + + pub fn remove_post_commit_hook(surface: &WlSurface, hook_id: &HookId) { + Self::lock_user_data(surface) + .post_commit_hooks + .retain(|hook| hook.id != *hook_id); + } + + pub fn remove_destruction_hook(surface: &WlSurface, hook_id: &HookId) { + Self::lock_user_data(surface) + .destruction_hooks + .retain(|hook| hook.id != *hook_id); + } + + pub fn add_pre_commit_hook( + surface: &WlSurface, + hook: impl Fn(&mut dyn Any, &DisplayHandle, &WlSurface) + Send + Sync + 'static, + ) -> HookId { + let hook: Hook = Hook::new(Arc::new(hook)); + let id = hook.id.clone(); + Self::lock_user_data(surface).pre_commit_hooks.push(hook); + id + } + + pub fn add_post_commit_hook( + surface: &WlSurface, + hook: impl Fn(&mut dyn Any, &DisplayHandle, &WlSurface) + Send + Sync + 'static, + ) -> HookId { + let hook: Hook = Hook::new(Arc::new(hook)); + let id = hook.id.clone(); + Self::lock_user_data(surface).post_commit_hooks.push(hook); + id + } + + pub fn add_destruction_hook( + surface: &WlSurface, + hook: impl Fn(&mut dyn Any, &WlSurface) + Send + Sync + 'static, + ) -> HookId { + let hook: Hook = Hook::new(Arc::new(hook)); + let id = hook.id.clone(); + Self::lock_user_data(surface).destruction_hooks.push(hook); + id + } + + pub fn invoke_pre_commit_hooks(state: &mut D, dh: &DisplayHandle, surface: &WlSurface) { + // don't hold the mutex while the hooks are invoked + let hooks = Self::lock_user_data(surface).pre_commit_hooks.clone(); + for hook in hooks { + (hook.cb)(state, dh, surface); + } + } + + pub fn invoke_post_commit_hooks(state: &mut D, dh: &DisplayHandle, surface: &WlSurface) { + // don't hold the mutex while the hooks are invoked + let hooks = Self::lock_user_data(surface).post_commit_hooks.clone(); + for hook in hooks { + (hook.cb)(state, dh, surface); + } + } + + fn commit_sync_surface_tree( + surface: &WlSurface, + parent_transaction: &PendingTransaction, + dh: &DisplayHandle, + ) { + let children = PrivateSurfaceData::get_children(surface); + let mut my_data = Self::lock_user_data(surface); + + for child in children { + Self::commit_sync_surface_tree(&child, &my_data.pending_transaction, dh); + } + + let current_txid = my_data.current_txid; + my_data.public_data.cached_state.commit(Some(current_txid), dh); + my_data + .pending_transaction + .insert_state(surface.clone(), current_txid); + + let child_tx = std::mem::take(&mut my_data.pending_transaction); + child_tx.merge_into(parent_transaction); + my_data.current_txid.0 = my_data.current_txid.0.wrapping_add(1); + } + + pub fn commit(surface: &WlSurface, dh: &DisplayHandle, state: &mut C) { + let is_sync = is_effectively_sync(surface); + let children = PrivateSurfaceData::get_children(surface); + let mut my_data = Self::lock_user_data(surface); + // commit our state + let current_txid = my_data.current_txid; + my_data.public_data.cached_state.commit(Some(current_txid), dh); + // take all our children state into our pending transaction + for child in children { + // if the child is effectively sync, take its state + // this is the case if either we are effectively sync, or the child is explicitly sync + let mut child_data = Self::lock_user_data(&child); + let is_child_sync = child_data + .public_data + .data_map + .get::() + .map(|s| s.sync.load(Ordering::Acquire)) + .unwrap_or(false); + + // if we are not sync, but the child is we also have to commit the complete child surface tree + if !is_sync && is_child_sync { + std::mem::drop(child_data); + Self::commit_sync_surface_tree(&child, &my_data.pending_transaction, dh); + } else if is_sync || is_child_sync { + let child_tx = std::mem::take(&mut child_data.pending_transaction); + child_tx.merge_into(&my_data.pending_transaction); + child_data.current_txid.0 = child_data.current_txid.0.wrapping_add(1); + } + } + my_data + .pending_transaction + .insert_state(surface.clone(), current_txid); + my_data.current_txid.0 = my_data.current_txid.0.wrapping_add(1); + if !is_sync { + let client = match surface.client() { + Some(client) => client, + None => return, + }; + // if we are not sync, add the transaction to the queue + let tx = std::mem::take(&mut my_data.pending_transaction); + let mut queue_guard = state.client_compositor_state(&client).queue.lock().unwrap(); + let queue = queue_guard.get_or_insert_with(TransactionQueue::default); + queue.append(tx.finalize()); + // release the mutex, as applying the transaction will try to lock it + std::mem::drop(my_data); + // trigger the queue + let transactions = queue.take_ready(); + // release the queue lock + std::mem::drop(queue_guard); + // apply might call commit, which might call blocker_cleared, so we need to free the queue before applying + for transaction in transactions { + transaction.apply(dh, state) + } + } + } + + /// Checks if the first surface is an ancestor of the second + pub fn is_ancestor(a: &WlSurface, b: &WlSurface) -> bool { + let b_guard = Self::lock_user_data(b); + if let Some(ref parent) = b_guard.parent { + if parent.id() == a.id() { + true + } else { + let parent = parent.clone(); + std::mem::drop(b_guard); + Self::is_ancestor(a, &parent) + } + } else { + false + } + } + + /// Sets the parent of a surface + /// + /// if this surface already has a role, does nothing and fails, otherwise + /// its role is now to be a subsurface + pub fn set_parent(child: &WlSurface, parent: &WlSurface) -> Result<(), AlreadyHasRole> { + // debug_assert!(child.as_ref().is_alive()); + // debug_assert!(parent.as_ref().is_alive()); + + // ensure the child is not the parent itself or its ancestor + if child == parent || Self::is_ancestor(child, parent) { + return Err(AlreadyHasRole); + } + + // change child's parent + { + let mut child_guard = Self::lock_user_data(child); + // if surface already has a role, it cannot become a subsurface + if child_guard.public_data.role.is_some() && child_guard.public_data.role != Some(SUBSURFACE_ROLE) + { + return Err(AlreadyHasRole); + } + // ensure the child doesn't have a parent already set by a previous + // wl_subcompositor.get_subsurface request + if child_guard.parent.is_some() { + return Err(AlreadyHasRole); + } + child_guard.public_data.role = Some(SUBSURFACE_ROLE); + child_guard.parent = Some(parent.clone()); + } + // register child to new parent + Self::lock_user_data(parent).children.push(child.clone()); + + Ok(()) + } + + /// Remove a pre-existing parent of this child + /// + /// Does nothing if it has no parent + pub fn unset_parent(child: &WlSurface) { + let old_parent = Self::lock_user_data(child).parent.take(); + // unregister from our parent + if let Some(old_parent) = old_parent { + Self::lock_user_data(&old_parent) + .children + .retain(|c| c.id() != child.id()); + } + } + + /// Retrieve the parent surface (if any) of this surface + pub fn get_parent(child: &WlSurface) -> Option { + Self::lock_user_data(child).parent.clone() + } + + /// Retrieve the children surface (if any) of this surface + pub fn get_children(parent: &WlSurface) -> Vec { + Self::lock_user_data(parent) + .children + .iter() + .filter(|s| s.id() != parent.id()) + .cloned() + .collect() + } + + /// Reorders a surface relative to one of its sibling + /// + /// Fails if `relative_to` is not a sibling or parent of `surface`. + pub fn reorder(surface: &WlSurface, to: Location, relative_to: &WlSurface) -> Result<(), ()> { + let parent = Self::get_parent(surface).ok_or(())?; + + fn index_of(surface: &WlSurface, slice: &[WlSurface]) -> Option { + for (i, s) in slice.iter().enumerate() { + if s.id() == surface.id() { + return Some(i); + } + } + None + } + + let mut parent_guard = Self::lock_user_data(&parent); + let my_index = index_of(surface, &parent_guard.children).unwrap(); + let mut other_index = match index_of(relative_to, &parent_guard.children) { + Some(idx) => idx, + None => return Err(()), + }; + let me = parent_guard.children.remove(my_index); + if my_index < other_index { + other_index -= 1; + } + let new_index = match to { + Location::Before => other_index, + Location::After => other_index + 1, + }; + parent_guard.children.insert(new_index, me); + + Ok(()) + } +} + +impl PrivateSurfaceData { + /// Access sequentially the attributes associated with a surface tree, + /// in a depth-first order. + /// + /// Note that an internal lock is taken during access of this data, + /// so the tree cannot be manipulated at the same time. + /// + /// The first callback determines if this node children should be processed or not. + /// + /// The second actually does the processing, being called on children in display depth + /// order. + /// + /// The third is called once all the children of a node has been processed (including itself), only if the first + /// returned `DoChildren`, and gives an opportunity to early stop + pub fn map_tree( + surface: &WlSurface, + initial: &T, + mut filter: F1, + mut processor: F2, + mut post_filter: F3, + reverse: bool, + ) where + F1: FnMut(&WlSurface, &SurfaceData, &T) -> TraversalAction, + F2: FnMut(&WlSurface, &SurfaceData, &T), + F3: FnMut(&WlSurface, &SurfaceData, &T) -> bool, + { + Self::map( + surface, + initial, + &mut filter, + &mut processor, + &mut post_filter, + reverse, + ); + } + + // helper function for map_tree + fn map( + surface: &WlSurface, + initial: &T, + filter: &mut F1, + processor: &mut F2, + post_filter: &mut F3, + reverse: bool, + ) -> bool + where + F1: FnMut(&WlSurface, &SurfaceData, &T) -> TraversalAction, + F2: FnMut(&WlSurface, &SurfaceData, &T), + F3: FnMut(&WlSurface, &SurfaceData, &T) -> bool, + { + let data_guard = &mut *Self::lock_user_data(surface); + // call the filter on ourselves + match filter(surface, &data_guard.public_data, initial) { + TraversalAction::DoChildren(t) => { + // loop over children + if reverse { + for c in data_guard.children.iter().rev() { + if c.id() == surface.id() { + processor(surface, &data_guard.public_data, initial); + } else if !Self::map(c, &t, filter, processor, post_filter, true) { + return false; + } + } + } else { + for c in &data_guard.children { + if c.id() == surface.id() { + processor(surface, &data_guard.public_data, initial); + } else if !Self::map(c, &t, filter, processor, post_filter, false) { + return false; + } + } + } + post_filter(surface, &data_guard.public_data, initial) + } + TraversalAction::SkipChildren => { + // still process ourselves + processor(surface, &data_guard.public_data, initial); + true + } + TraversalAction::Break => false, + } + } +} + +/// The latest surface state suggest by wl_compositor `v6` events. +#[derive(Debug)] +pub struct SuggestedSurfaceState { + /// Latest scale sent via `wl_surface::preferred_buffer_scale`. + pub scale: i32, + /// Latest transform sent via `wl_surface::preferred_buffer_transform`. + pub transform: Transform, +} + +impl Default for SuggestedSurfaceState { + fn default() -> Self { + Self { + scale: 1, + transform: Transform::Normal, + } + } +} diff --git a/vendor/smithay/src/wayland/content_type/dispatch.rs b/vendor/smithay/src/wayland/content_type/dispatch.rs new file mode 100644 index 0000000..7ffacef --- /dev/null +++ b/vendor/smithay/src/wayland/content_type/dispatch.rs @@ -0,0 +1,132 @@ +use wayland_protocols::wp::content_type::v1::server::{ + wp_content_type_manager_v1::{self, WpContentTypeManagerV1}, + wp_content_type_v1::{self, WpContentTypeV1}, +}; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, New, Resource, backend::ClientId}; + +use super::{ContentTypeSurfaceCachedState, ContentTypeSurfaceData, ContentTypeUserData}; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2, compositor}; + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _: &Client, + manager: &wp_content_type_manager_v1::WpContentTypeManagerV1, + request: wp_content_type_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_content_type_manager_v1::Request::GetSurfaceContentType { id, surface } => { + let already_taken = compositor::with_states(&surface, |states| { + states + .data_map + .insert_if_missing_threadsafe(ContentTypeSurfaceData::new); + let data = states.data_map.get::().unwrap(); + + let already_taken = data.is_resource_attached(); + + if !already_taken { + data.set_is_resource_attached(true); + } + + already_taken + }); + + if already_taken { + manager.post_error( + wp_content_type_manager_v1::Error::AlreadyConstructed, + "WlSurface already has WpSurfaceContentType attached", + ) + } else { + data_init.init(id, ContentTypeUserData::new(surface)); + } + } + + wp_content_type_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for ContentTypeUserData { + fn request( + &self, + _state: &mut D, + _: &Client, + _: &WpContentTypeV1, + request: wp_content_type_v1::Request, + _dh: &DisplayHandle, + _: &mut DataInit<'_, D>, + ) { + match request { + wp_content_type_v1::Request::SetContentType { content_type } => { + let wayland_server::WEnum::Value(content_type) = content_type else { + return; + }; + let Some(surface) = self.wl_surface() else { + return; + }; + + compositor::with_states(&surface, |states| { + states + .cached_state + .get::() + .pending() + .content_type = content_type; + }) + } + // Switch back to not specifying the content type of this surface. + // This is equivalent to setting the content type to none, + // including double buffering semantics. + wp_content_type_v1::Request::Destroy => { + let Some(surface) = self.wl_surface() else { + return; + }; + + compositor::with_states(&surface, |states| { + states + .data_map + .get::() + .unwrap() + .set_is_resource_attached(false); + + states + .cached_state + .get::() + .pending() + .content_type = wp_content_type_v1::Type::None; + }); + } + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: ClientId, _object: &WpContentTypeV1) { + // Nothing to do here, graceful Destroy is already handled with double buffering + // and in case of client close WlSurface destroyed handler will clean up the data anyway, + // so there is no point in queuing new update + } +} diff --git a/vendor/smithay/src/wayland/content_type/mod.rs b/vendor/smithay/src/wayland/content_type/mod.rs new file mode 100644 index 0000000..da99a7a --- /dev/null +++ b/vendor/smithay/src/wayland/content_type/mod.rs @@ -0,0 +1,170 @@ +//! Implementation of wp_content_type protocol +//! +//! ### Example +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use wayland_server::{protocol::wl_surface::WlSurface, DisplayHandle}; +//! use smithay::{ +//! wayland::compositor::{self, CompositorState, CompositorClientState, CompositorHandler}, +//! wayland::content_type::{ContentTypeSurfaceCachedState, ContentTypeState}, +//! }; +//! +//! pub struct State { +//! compositor_state: CompositorState, +//! }; +//! struct ClientState { compositor_state: CompositorClientState } +//! impl wayland_server::backend::ClientData for ClientState {} +//! +//! smithay::delegate_dispatch2!(State); +//! +//! impl CompositorHandler for State { +//! fn compositor_state(&mut self) -> &mut CompositorState { +//! &mut self.compositor_state +//! } +//! +//! fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { +//! &client.get_data::().unwrap().compositor_state +//! } +//! +//! fn commit(&mut self, surface: &WlSurface) { +//! compositor::with_states(&surface, |states| { +//! let mut guard = states.cached_state.get::(); +//! let current = guard.current(); +//! dbg!(current.content_type()); +//! }); +//! } +//! } +//! +//! let mut display = wayland_server::Display::::new().unwrap(); +//! +//! let compositor_state = CompositorState::new::(&display.handle()); +//! ContentTypeState::new::(&display.handle()); +//! +//! let state = State { +//! compositor_state, +//! }; +//! ``` + +use std::sync::{ + Mutex, + atomic::{self, AtomicBool}, +}; + +use wayland_protocols::wp::content_type::v1::server::{ + wp_content_type_manager_v1::WpContentTypeManagerV1, wp_content_type_v1, +}; +use wayland_server::{ + DisplayHandle, GlobalDispatch, Resource, Weak, backend::GlobalId, protocol::wl_surface::WlSurface, +}; + +use super::compositor::Cacheable; + +use crate::wayland::GlobalData; + +mod dispatch; + +/// Data associated with WlSurface +/// Represents the client pending state +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::content_type::ContentTypeSurfaceCachedState; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// let mut guard = states.cached_state.get::(); +/// let current = guard.current(); +/// dbg!(current.content_type()); +/// }); +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct ContentTypeSurfaceCachedState { + content_type: wp_content_type_v1::Type, +} + +impl ContentTypeSurfaceCachedState { + /// This informs the compositor that the client believes it is displaying buffers matching this content type. + pub fn content_type(&self) -> &wp_content_type_v1::Type { + &self.content_type + } +} + +impl Default for ContentTypeSurfaceCachedState { + fn default() -> Self { + Self { + content_type: wp_content_type_v1::Type::None, + } + } +} + +impl Cacheable for ContentTypeSurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +#[derive(Debug)] +struct ContentTypeSurfaceData { + is_resource_attached: AtomicBool, +} + +impl ContentTypeSurfaceData { + fn new() -> Self { + Self { + is_resource_attached: AtomicBool::new(false), + } + } + + fn set_is_resource_attached(&self, is_attached: bool) { + self.is_resource_attached + .store(is_attached, atomic::Ordering::Release) + } + + fn is_resource_attached(&self) -> bool { + self.is_resource_attached.load(atomic::Ordering::Acquire) + } +} + +/// User data of `WpContentTypeV1` object +#[derive(Debug)] +pub struct ContentTypeUserData(Mutex>); + +impl ContentTypeUserData { + fn new(surface: WlSurface) -> Self { + Self(Mutex::new(surface.downgrade())) + } + + #[inline] + fn wl_surface(&self) -> Option { + self.0.lock().unwrap().upgrade().ok() + } +} + +/// Delegate type for [WpContentTypeManagerV1] global. +#[derive(Debug)] +pub struct ContentTypeState { + global: GlobalId, +} + +impl ContentTypeState { + /// Regiseter new [WpContentTypeManagerV1] global + pub fn new(display: &DisplayHandle) -> ContentTypeState + where + D: GlobalDispatch + 'static, + { + let global = display.create_global::(1, GlobalData); + + ContentTypeState { global } + } + + /// Returns the WpContentTypeManagerV1 global id + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} diff --git a/vendor/smithay/src/wayland/cursor_shape.rs b/vendor/smithay/src/wayland/cursor_shape.rs new file mode 100644 index 0000000..3ee2961 --- /dev/null +++ b/vendor/smithay/src/wayland/cursor_shape.rs @@ -0,0 +1,362 @@ +//! Utilities for compositor side cursor theming support +//! +//! This protocol allows clients to request compositor to draw a cursor for them, thus resulting +//! in more consistent look and feel of the cursor across the applications. +//! +//! ## Initialization +//! +//! ``` +//! extern crate smithay; +//! extern crate wayland_server; +//! +//! use smithay::wayland::cursor_shape::CursorShapeManagerState; +//! +//! # use smithay::backend::input::{InputTime, KeyState, TabletToolDescriptor}; +//! # use smithay::input::{ +//! # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, +//! # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, +//! # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, +//! # GestureHoldBeginEvent, GestureHoldEndEvent}, +//! # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, +//! # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, +//! # tablet::{Tablet, TabletSeatHandler, tool::{self, TabletToolTarget}}, +//! # Seat, SeatHandler, SeatState, +//! # }; +//! # use smithay::utils::{IsAlive, Serial}; +//! # use smithay::wayland::seat::WaylandFocus; +//! # use wayland_server::protocol::wl_surface; +//! +//! # #[derive(Debug, Clone, PartialEq)] +//! # struct Target; +//! # impl IsAlive for Target { +//! # fn alive(&self) -> bool { true } +//! # } +//! # impl WaylandFocus for Target { +//! # fn wl_surface(&self) -> Option> { +//! # None +//! # } +//! # } +//! +//! # impl PointerTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} +//! # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} +//! # fn frame(&self, seat: &Seat, data: &mut State) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} +//! # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} +//! # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} +//! # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} +//! # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} +//! # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} +//! # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} +//! # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} +//! # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} +//! # } +//! # impl KeyboardTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} +//! # fn key( +//! # &self, +//! # seat: &Seat, +//! # data: &mut State, +//! # key: KeysymHandle<'_>, +//! # state: KeyState, +//! # serial: Serial, +//! # time: InputTime, +//! # ) {} +//! # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} +//! # } +//! # impl TouchTarget for Target { +//! # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} +//! # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} +//! # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } +//! # } +//! # impl TabletToolTarget for Target { +//! # fn proximity_in(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, tablet: &Tablet, serial: Serial) {} +//! # fn proximity_out(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor) {} +//! # fn down(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::MotionEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, frame: tool::AxisFrame) {} +//! # fn button(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, event: &tool::ButtonEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, tool_descriptor: &TabletToolDescriptor, time: InputTime) {} +//! # } +//! # struct State { +//! # seat_state: SeatState, +//! # }; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # impl SeatHandler for State { +//! # type KeyboardFocus = Target; +//! # type PointerFocus = Target; +//! # type TouchFocus = Target; +//! # +//! # fn seat_state(&mut self) -> &mut SeatState { +//! # &mut self.seat_state +//! # } +//! # } +//! # impl TabletSeatHandler for State { +//! # type ToolFocus = Target; +//! # } +//! +//! let state = CursorShapeManagerState::new::(&display.handle()); +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_device_v1::Request as ShapeRequest; +use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_device_v1::Shape; +use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1 as CursorShapeDevice; +use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_manager_v1::Request as ManagerRequest; +use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1 as CursorShapeManager; +use wayland_protocols::wp::tablet::zv2::server::zwp_tablet_tool_v2::ZwpTabletToolV2; +use wayland_server::GlobalDispatch; +use wayland_server::Resource; +use wayland_server::WEnum; +use wayland_server::Weak; +use wayland_server::{Dispatch, DisplayHandle, backend::GlobalId}; + +use crate::input::SeatHandler; +use crate::input::WeakSeat; +use crate::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle}; +use crate::input::tablet::TabletSeatHandler; +use crate::utils::Serial; +use crate::wayland::seat::{WaylandFocus, pointer::allow_setting_cursor}; +use crate::wayland::tablet_manager::TabletToolUserData; +use crate::wayland::tablet_manager::tablet_tool; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +/// State of the cursor shape manager. +#[derive(Debug)] +pub struct CursorShapeManagerState { + global: GlobalId, +} + +impl CursorShapeManagerState { + /// Register new [CursorShapeManager] global. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: SeatHandler, + D: 'static, + { + let global = display.create_global::(2, GlobalData); + Self { global } + } + + /// [CursorShapeManager] GlobalId getter. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: SeatHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch>, + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + _resource: &CursorShapeManager, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + ManagerRequest::GetPointer { + cursor_shape_device, + pointer, + } => { + let handle = match PointerHandle::from_resource(&pointer) { + Some(handle) => handle, + None => return, + }; + + let Some(seat) = state + .seat_state() + .seats + .iter() + .find(|seat| seat.get_pointer().map(|h| h == handle).unwrap_or(false)) + .cloned() + else { + return; + }; + + data_init.init( + cursor_shape_device, + CursorShapeDeviceUserData(CursorShapeDeviceUserDataInner::Pointer { + seat: seat.downgrade(), + }), + ); + } + ManagerRequest::GetTabletToolV2 { + cursor_shape_device, + tablet_tool, + } => { + data_init.init( + cursor_shape_device, + CursorShapeDeviceUserData(CursorShapeDeviceUserDataInner::Tablet( + tablet_tool.downgrade(), + )), + ); + } + ManagerRequest::Destroy => {} + _ => unreachable!(), + } + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct CursorShapeDeviceUserData(CursorShapeDeviceUserDataInner); + +#[derive(Debug, Clone)] +pub(crate) enum CursorShapeDeviceUserDataInner { + /// The device was created for the pointer. + Pointer { seat: WeakSeat }, + /// The device was created for the tablet tool. + Tablet(Weak), +} + +impl Dispatch2 for CursorShapeDeviceUserData +where + D: SeatHandler + TabletSeatHandler, + ::PointerFocus: WaylandFocus, + ::ToolFocus: WaylandFocus, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + resource: &CursorShapeDevice, + request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + ShapeRequest::SetShape { + serial, + shape: WEnum::Value(shape), + } => { + match &self.0 { + CursorShapeDeviceUserDataInner::Pointer { seat } => { + let Some(seat) = seat.upgrade() else { + return; + }; + + let Some(handle) = seat.get_pointer() else { + // When the pointer capability is removed from the wl_seat, + // the wp_cursor_shape_device_v1 object becomes inert. + return; + }; + + if !allow_setting_cursor(&handle, Serial(serial), &resource.id()) { + return; + } + + let cursor_icon = shape_to_cursor_icon(shape); + state.cursor_image(&seat, CursorImageStatus::Named(cursor_icon)); + } + CursorShapeDeviceUserDataInner::Tablet(tablet) => { + let Ok(tablet) = tablet.upgrade() else { + // When the zwp_tablet_tool_v2 is removed, the wp_cursor_shape_device_v1 object becomes inert. + return; + }; + let tablet_data = match tablet.data::>() { + Some(data) => data, + None => return, + }; + + let Some(handle) = tablet_data.handle.upgrade() else { + return; + }; + + if !tablet_tool::allow_setting_cursor(&handle, Serial(serial), &resource.id()) { + return; + } + + let cursor_icon = shape_to_cursor_icon(shape); + state + .tablet_tool_image(&handle.arc.descriptor, CursorImageStatus::Named(cursor_icon)); + } + } + } + ShapeRequest::SetShape { .. } => { + // Ignore unknown shapes. + } + ShapeRequest::Destroy => {} + _ => unreachable!(), + } + } +} + +fn shape_to_cursor_icon(shape: Shape) -> CursorIcon { + match shape { + Shape::Default => CursorIcon::Default, + Shape::ContextMenu => CursorIcon::ContextMenu, + Shape::Help => CursorIcon::Help, + Shape::Pointer => CursorIcon::Pointer, + Shape::Progress => CursorIcon::Progress, + Shape::Wait => CursorIcon::Wait, + Shape::Cell => CursorIcon::Cell, + Shape::Crosshair => CursorIcon::Crosshair, + Shape::Text => CursorIcon::Text, + Shape::VerticalText => CursorIcon::VerticalText, + Shape::Alias => CursorIcon::Alias, + Shape::Copy => CursorIcon::Copy, + Shape::Move => CursorIcon::Move, + Shape::NoDrop => CursorIcon::NoDrop, + Shape::NotAllowed => CursorIcon::NotAllowed, + Shape::Grab => CursorIcon::Grab, + Shape::Grabbing => CursorIcon::Grabbing, + Shape::EResize => CursorIcon::EResize, + Shape::NResize => CursorIcon::NResize, + Shape::NeResize => CursorIcon::NeResize, + Shape::NwResize => CursorIcon::NwResize, + Shape::SResize => CursorIcon::SResize, + Shape::SeResize => CursorIcon::SeResize, + Shape::SwResize => CursorIcon::SwResize, + Shape::WResize => CursorIcon::WResize, + Shape::EwResize => CursorIcon::EwResize, + Shape::NsResize => CursorIcon::NsResize, + Shape::NeswResize => CursorIcon::NeswResize, + Shape::NwseResize => CursorIcon::NwseResize, + Shape::ColResize => CursorIcon::ColResize, + Shape::RowResize => CursorIcon::RowResize, + Shape::AllScroll => CursorIcon::AllScroll, + Shape::ZoomIn => CursorIcon::ZoomIn, + Shape::ZoomOut => CursorIcon::ZoomOut, + Shape::DndAsk => CursorIcon::DndAsk, + Shape::AllResize => CursorIcon::AllResize, + _ => CursorIcon::Default, + } +} diff --git a/vendor/smithay/src/wayland/dispatch2.rs b/vendor/smithay/src/wayland/dispatch2.rs new file mode 100644 index 0000000..be4bc70 --- /dev/null +++ b/vendor/smithay/src/wayland/dispatch2.rs @@ -0,0 +1,91 @@ +use wayland_server::{Client, DataInit, DisplayHandle, New, Resource, backend::ClientId}; + +/// A simplified version of [`wayland_server::Dispatch`] +/// +/// A future version of `wayland-server` will replace `Dispatch` with this. +pub trait Dispatch2 { + /// Called when a request from a client is processed. + fn request( + &self, + state: &mut State, + client: &Client, + resource: &I, + request: I::Request, + dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, State>, + ); + + /// Called when the object this user data is associated with has been destroyed. + fn destroyed(&self, _state: &mut State, _client: ClientId, _resource: &I) {} +} + +/// A simplified version of [`wayland_server::GlobalDispatch`] +/// +/// A future version of `wayland-server` will replace `GlobalDispatch` with this. +pub trait GlobalDispatch2 { + /// Called when a client has bound this global. + fn bind( + &self, + state: &mut State, + handle: &DisplayHandle, + client: &Client, + resource: New, + data_init: &mut DataInit<'_, State>, + ); + + /// Checks if the global should be advertised to some client. + fn can_view(&self, _client: &Client) -> bool { + true + } +} + +/// Implement `Dispatch` and `GlobalDispatch` for every implementation of [`Dispatch2`] and +/// [`GlobalDispatch2`]. +#[macro_export] +macro_rules! delegate_dispatch2 { + ($(@< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? $ty:ty) => { + impl<$( $( $lt $( : $clt $(+ $dlt )* )? ),+, )? I, UserData> $crate::reexports::wayland_server::Dispatch for $ty + where + I: $crate::reexports::wayland_server::Resource, + UserData: $crate::wayland::Dispatch2 { + fn request( + state: &mut Self, + client: &$crate::reexports::wayland_server::Client, + resource: &I, + request: ::Request, + data: &UserData, + dhandle: &$crate::reexports::wayland_server::DisplayHandle, + data_init: &mut $crate::reexports::wayland_server::DataInit<'_, Self>, + ) { + data.request(state, client, resource, request, dhandle, data_init); + } + + fn destroyed(state: &mut Self, client: $crate::reexports::wayland_server::backend::ClientId, resource: &I, data: &UserData) { + data.destroyed(state, client, resource); + } + } + + impl<$( $( $lt $( : $clt $(+ $dlt )* )? ),+, )? I, UserData> $crate::reexports::wayland_server::GlobalDispatch for $ty + where + I: $crate::reexports::wayland_server::Resource, + UserData: $crate::wayland::GlobalDispatch2 { + fn bind( + state: &mut Self, + dhandle: &$crate::reexports::wayland_server::DisplayHandle, + client: &$crate::reexports::wayland_server::Client, + resource: $crate::reexports::wayland_server::New, + data: &UserData, + data_init: &mut $crate::reexports::wayland_server::DataInit<'_, Self>, + ) { + data.bind(state, dhandle, client, resource, data_init); + } + + fn can_view( + client: $crate::reexports::wayland_server::Client, + data: &UserData + ) -> bool { + data.can_view(&client) + } + } + }; +} diff --git a/vendor/smithay/src/wayland/dmabuf/dispatch.rs b/vendor/smithay/src/wayland/dmabuf/dispatch.rs new file mode 100644 index 0000000..400d659 --- /dev/null +++ b/vendor/smithay/src/wayland/dmabuf/dispatch.rs @@ -0,0 +1,365 @@ +use std::sync::{Arc, Mutex, atomic::AtomicBool}; + +use libc::dev_t; +use wayland_protocols::wp::linux_dmabuf::zv1::server::{ + zwp_linux_buffer_params_v1, zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, New, Resource, backend::ClientId, protocol::wl_buffer, +}; + +use crate::{ + backend::allocator::dmabuf::{Dmabuf, MAX_PLANES, Plane}, + wayland::{ + Dispatch2, GlobalDispatch2, buffer::BufferHandler, compositor, + dmabuf::SurfaceDmabufFeedbackStateInner, + }, +}; + +use super::{ + DmabufData, DmabufFeedbackData, DmabufGlobal, DmabufGlobalData, DmabufHandler, DmabufParamsData, Import, + ImportNotifier, Modifier, SurfaceDmabufFeedbackState, +}; + +impl Dispatch2 for Dmabuf +where + D: BufferHandler, +{ + fn request( + &self, + _data: &mut D, + _client: &Client, + _buffer: &wl_buffer::WlBuffer, + request: wl_buffer::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_buffer::Request::Destroy => { + // Handled in the destroyed callback. + } + + _ => unreachable!(), + } + } + + fn destroyed(&self, data: &mut D, _client: ClientId, buffer: &wl_buffer::WlBuffer) { + data.buffer_destroyed(buffer); + } +} + +impl Dispatch2 for DmabufData +where + D: Dispatch + + Dispatch + + DmabufHandler + + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + request: zwp_linux_dmabuf_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_linux_dmabuf_v1::Request::Destroy => {} + + zwp_linux_dmabuf_v1::Request::CreateParams { params_id } => { + data_init.init( + params_id, + DmabufParamsData { + id: self.id, + used: AtomicBool::new(false), + formats: self.formats.clone(), + modifier: Mutex::new(None), + planes: Mutex::new(Vec::with_capacity(MAX_PLANES)), + node: Mutex::new(None), + }, + ); + } + + zwp_linux_dmabuf_v1::Request::GetDefaultFeedback { id } => { + let feedback = data_init.init( + id, + DmabufFeedbackData { + known_default_feedbacks: self.known_default_feedbacks.clone(), + surface: None, + }, + ); + + self.known_default_feedbacks + .lock() + .unwrap() + .push(feedback.downgrade()); + + self.default_feedback + .as_ref() + .unwrap() + .lock() + .unwrap() + .send(&feedback); + } + + zwp_linux_dmabuf_v1::Request::GetSurfaceFeedback { id, surface } => { + let feedback = data_init.init( + id, + DmabufFeedbackData { + known_default_feedbacks: self.known_default_feedbacks.clone(), + surface: Some(surface.downgrade()), + }, + ); + + if compositor::with_states(&surface, |states| { + states.data_map.get::().is_none() + }) { + let new_feedback = state + .new_surface_feedback(&surface, &DmabufGlobal { id: self.id }) + .unwrap_or_else(|| self.default_feedback.as_ref().unwrap().lock().unwrap().clone()); + compositor::with_states(&surface, |states| { + states + .data_map + .insert_if_missing_threadsafe(|| SurfaceDmabufFeedbackState { + inner: Arc::new(Mutex::new(SurfaceDmabufFeedbackStateInner { + feedback: new_feedback, + known_instances: Vec::new(), + })), + }); + }); + } + + let surface_feedback = compositor::with_states(&surface, |states| { + let feedback_state = states.data_map.get::().unwrap(); + feedback_state.add_instance(&feedback) + }); + + surface_feedback.send(&feedback); + } + + _ => unreachable!(), + } + } +} + +impl Dispatch2 for DmabufFeedbackData { + fn request( + &self, + _state: &mut D, + _client: &Client, + resource: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_linux_dmabuf_feedback_v1::Request::Destroy => { + self.known_default_feedbacks + .lock() + .unwrap() + .retain(|feedback| feedback != resource); + + if let Some(surface) = self.surface.as_ref().and_then(|s| s.upgrade().ok()) { + compositor::with_states(&surface, |states| { + if let Some(surface_state) = states.data_map.get::() { + surface_state.remove_instance(resource); + } + }) + } + } + _ => unreachable!(), + } + } +} + +impl GlobalDispatch2 for DmabufGlobalData +where + D: Dispatch + 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let data = DmabufData { + formats: self.formats.clone(), + id: self.id, + default_feedback: self.default_feedback.clone(), + known_default_feedbacks: self.known_default_feedbacks.clone(), + }; + + let zwp_dmabuf = data_init.init(resource, data); + + // Immediately send format info to the client if we are the correct version. + // + // These events are deprecated in version 4 of the protocol. + if zwp_dmabuf.version() < zwp_linux_dmabuf_v1::REQ_GET_DEFAULT_FEEDBACK_SINCE { + for (fourcc, modifiers) in &*self.formats { + // Modifier support got added in version 3 + if zwp_dmabuf.version() < zwp_linux_dmabuf_v1::EVT_MODIFIER_SINCE { + if modifiers.contains(&Modifier::Invalid) || modifiers.contains(&Modifier::Linear) { + zwp_dmabuf.format(*fourcc as u32); + } + continue; + } + + for modifier in modifiers { + let modifier_hi = (Into::::into(*modifier) >> 32) as u32; + let modifier_lo = Into::::into(*modifier) as u32; + zwp_dmabuf.modifier(*fourcc as u32, modifier_hi, modifier_lo); + } + } + } + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for DmabufParamsData +where + D: Dispatch + BufferHandler + DmabufHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + params: &zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + request: zwp_linux_buffer_params_v1::Request, + dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_linux_buffer_params_v1::Request::Destroy => {} + + zwp_linux_buffer_params_v1::Request::Add { + fd, + plane_idx, + offset, + stride, + modifier_hi, + modifier_lo, + } => { + if !self.ensure_unused(params) { + return; + } + + // Plane index should not be too large + if plane_idx as usize >= MAX_PLANES { + params.post_error( + zwp_linux_buffer_params_v1::Error::PlaneIdx, + format!("Plane index {plane_idx} is out of bounds"), + ); + return; + } + + let mut planes = self.planes.lock().unwrap(); + + // Is the index already set? + if planes.iter().any(|plane| plane.plane_idx == plane_idx) { + params.post_error( + zwp_linux_buffer_params_v1::Error::PlaneSet, + format!("Plane index {plane_idx} is already set."), + ); + return; + } + + planes.push(Plane { + fd: Arc::new(fd), + plane_idx, + offset, + stride, + }); + + let modifier = Modifier::from(((modifier_hi as u64) << 32) + (modifier_lo as u64)); + let mut data_modifier = self.modifier.lock().unwrap(); + if let Some(data_modifier) = *data_modifier { + if params.version() >= 5 && modifier != data_modifier { + params.post_error( + zwp_linux_buffer_params_v1::Error::InvalidFormat, + format!("Planes have non-matching modifiers: {modifier:?} != {data_modifier:?}",), + ); + } + } else { + *data_modifier = Some(modifier); + } + } + + zwp_linux_buffer_params_v1::Request::SetSamplingDevice { device } => { + if let Ok(array) = device.try_into() { + *self.node.lock().unwrap() = Some(dev_t::from_ne_bytes(array)); + } else { + params.post_error( + zwp_linux_buffer_params_v1::Error::InvalidDevTSize, + "Could not parse the dev_t provided via SetSamplingDevice", + ); + } + } + + zwp_linux_buffer_params_v1::Request::Create { + width, + height, + format, + flags, + } => { + // create_dmabuf performs an implicit ensure_unused function call. + if let Some(dmabuf) = + self.create_dmabuf(params, width, height, format, flags, *self.node.lock().unwrap()) + { + if state.dmabuf_state().globals.contains_key(&self.id) { + let notifier = ImportNotifier::new( + params.clone(), + dh.clone(), + dmabuf.clone(), + Import::Falliable, + ); + state.dmabuf_imported(&DmabufGlobal { id: self.id }, dmabuf, notifier); + } else { + // If the dmabuf global was destroyed, we cannot import any buffers. + params.failed(); + } + } + } + + zwp_linux_buffer_params_v1::Request::CreateImmed { + buffer_id, + width, + height, + format, + flags, + } => { + // Client is killed if the if statement is not taken. + // create_dmabuf performs an implicit ensure_unused function call. + if let Some(dmabuf) = + self.create_dmabuf(params, width, height, format, flags, *self.node.lock().unwrap()) + { + if state.dmabuf_state().globals.contains_key(&self.id) { + // The buffer isn't technically valid during data_init, but the client is not allowed to use the buffer until ready. + let buffer = data_init.init(buffer_id, dmabuf.clone()); + let notifier = ImportNotifier::new( + params.clone(), + dh.clone(), + dmabuf.clone(), + Import::Infallible(buffer), + ); + state.dmabuf_imported(&DmabufGlobal { id: self.id }, dmabuf, notifier); + } else { + // Buffer import failed. The protocol documentation heavily implies killing the + // client is the right thing to do here. + params.post_error( + zwp_linux_buffer_params_v1::Error::InvalidWlBuffer, + "dmabuf global was destroyed on server", + ); + } + } + } + + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/dmabuf/mod.rs b/vendor/smithay/src/wayland/dmabuf/mod.rs new file mode 100644 index 0000000..edd3b83 --- /dev/null +++ b/vendor/smithay/src/wayland/dmabuf/mod.rs @@ -0,0 +1,1206 @@ +//! Linux DMABUF protocol +//! +//! This module provides helper to handle the linux-dmabuf protocol, which allows clients to submit their +//! contents as dmabuf file descriptors. These handlers automate the aggregation of the metadata associated +//! with a dma buffer, and do some basic checking of the sanity of what the client sends. +//! +//! ## How to use +//! +//! To setup the dmabuf global, you will need to provide 2 things: +//! +//! - the default [`DmabufFeedback`] containing the main device and the formats you wish to support when creating the `Global` through [`DmabufState::create_global_with_default_feedback`] +//! - an implementation of [`DmabufHandler`] to test if a dmabuf buffer can be imported by your renderer and optionally override the initial surface feedback +//! +//! The list of supported formats is a `Vec`, where you will enter all the (code, modifier) pairs you +//! support. You can typically receive a list of supported formats for one renderer by calling +//! [`ImportDma::dmabuf_formats`](crate::backend::renderer::ImportDma::dmabuf_formats). +//! +//! ```no_run +//! use smithay::{ +//! backend::allocator::dmabuf::{Dmabuf}, +//! reexports::{ +//! wayland_server::protocol::{ +//! wl_buffer::WlBuffer, +//! wl_surface::WlSurface, +//! } +//! }, +//! wayland::{ +//! buffer::BufferHandler, +//! dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier} +//! }, +//! }; +//! +//! pub struct State { +//! dmabuf_state: DmabufState, +//! dmabuf_global: DmabufGlobal, +//! } +//! +//! // Smithay's "DmabufHandler" also requires the buffer management utilities, you need to implement +//! // "BufferHandler". +//! impl BufferHandler for State { +//! fn buffer_destroyed(&mut self, buffer: &wayland_server::protocol::wl_buffer::WlBuffer) { +//! // All renderers can handle buffer destruction at this point. Some parts of window management may +//! // also use this function. +//! // +//! // If you need to mark a dmabuf elsewhere in your state as destroyed, you use the "get_dmabuf" +//! // function defined in this module to access the dmabuf associated the "Buffer". +//! } +//! } +//! +//! impl DmabufHandler for State { +//! fn dmabuf_state(&mut self) -> &mut DmabufState { +//! &mut self.dmabuf_state +//! } +//! +//! fn dmabuf_imported(&mut self, global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) { +//! // Here you should import the dmabuf into your renderer. +//! // +//! // The notifier is used to communicate whether import was successful. In this example we +//! // call successful to notify the client import was successful. +//! notifier.successful::(); +//! } +//! +//! fn new_surface_feedback( +//! &mut self, +//! surface: &WlSurface, +//! global: &DmabufGlobal, +//! ) -> Option { +//! // Here you can override the initial feedback sent to a client requesting feedback for a specific +//! // surface. Returning `None` instructs the global to return the default feedback to the client which +//! // is also the default implementation for this function when not overridden +//! None +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! // First a DmabufState must be created. This type is used to create some "DmabufGlobal"s +//! let mut dmabuf_state = DmabufState::new(); +//! +//! // ...identify primary render node and load dmabuf formats supported for rendering... +//! # let main_device = { todo!() }; +//! # let formats: Vec<_> = { todo!() }; +//! +//! // Build the default feedback from the device node of the primary render node and +//! // the supported dmabuf formats +//! let default_feedback = DmabufFeedbackBuilder::new(main_device, formats).build().unwrap(); +//! +//! // And create the dmabuf global. +//! let dmabuf_global = dmabuf_state.create_global_with_default_feedback::( +//! &display_handle, +//! &default_feedback, +//! ); +//! +//! let state = State { +//! dmabuf_state, +//! dmabuf_global, +//! }; +//! +//! // Rest of the compositor goes here... +//! ``` +//! +//! Accessing a [`Dmabuf`] associated with a [`WlBuffer`] +//! may be achieved using [`get_dmabuf`]. +//! +//! #### Notes on supporting per surface feedback +//! +//! If a client requests [`DmabufFeedback`] for a specific [`WlSurface`] it can be used to inform the client about +//! sub-optimal buffer allocations. This is especially important to support direct scan-out over drm planes +//! that typically only support a subset of supported formats from the rendering formats. +//! +//! The [`DmabufFeedback`] of a specific [`WlSurface`] can be updated by retrieving the [`SurfaceDmabufFeedbackState`] +//! with [`SurfaceDmabufFeedbackState::from_states`] and setting the feedback with [`SurfaceDmabufFeedbackState::set_feedback`]. +//! +//! [`DmabufFeedback`] uses preference tranches to inform the client about formats that could result on more optimal buffer placement. +//! Preference tranches can be added to the feedback during initialization with [`DmabufFeedbackBuilder::add_preference_tranche`]. +//! Note that the order of formats within a tranche (`target_device` + `flags`) is undefined, if you want to communicate preference +//! of a specific format you have to split the formats into multiple tranches. A tranche can additionally define [`TrancheFlags`] +//! which can give clients additional context what the tranche represents. As an example formats gathered from drm planes +//! should define [`TrancheFlags::Scanout`](`zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout) to communicate that buffers should be allocated so that +//! they support scan-out by the device specified as the `target device`. +//! +//! Note: Surface feedback only represents an optimization and the fallback path using compositing should always be supported, so +//! typically you do not want to announce formats in a preference tranche that are not supported by the main device for rendering. +//! +//! #### Notes on clients binding version 3 or lower +//! +//! During instantiation the global will automatically build a format list from the provided [`DmabufFeedback`] consisting of all formats that are part of a tranche +//! having the `target device` equal the `main device` and defining no special [`TrancheFlags`]. +//! +//! ### Without feedback (v3) +//! +//! It is also possible to initialize the `Global` without support for [`DmabufFeedback`] by using [`DmabufState::create_global`] which +//! will then only advertise version `3` to clients. This is mostly meant to guarantee an easy update path for compositors already +//! supporting dmabuf version `3` without breakage. +//! +//! ```no_run +//! # extern crate wayland_server; +//! # use smithay::{ +//! # backend::allocator::dmabuf::Dmabuf, +//! # reexports::{wayland_server::protocol::wl_buffer::WlBuffer}, +//! # wayland::{ +//! # buffer::BufferHandler, +//! # dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier} +//! # }, +//! # }; +//! # pub struct State { +//! # dmabuf_state: DmabufState, +//! # dmabuf_global: DmabufGlobal, +//! # } +//! # impl BufferHandler for State { +//! # fn buffer_destroyed(&mut self, buffer: &wayland_server::protocol::wl_buffer::WlBuffer) { } +//! # } +//! # impl DmabufHandler for State { +//! # fn dmabuf_state(&mut self) -> &mut DmabufState { +//! # &mut self.dmabuf_state +//! # } +//! # fn dmabuf_imported(&mut self, global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {} +//! # } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! # let mut dmabuf_state = DmabufState::new(); +//! # +//! // define your supported formats +//! let formats = vec![ +//! /* ... */ +//! ]; +//! +//! // And create the dmabuf global. +//! let dmabuf_global = dmabuf_state.create_global::( +//! &display_handle, +//! formats, +//! ); +//! +//! let state = State { +//! dmabuf_state, +//! dmabuf_global, +//! }; +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // Rest of the compositor goes here... +//! ``` + +mod dispatch; + +use std::{ + collections::HashMap, + ops::{RangeInclusive, Sub}, + os::unix::io::AsFd, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, +}; + +use indexmap::{IndexMap, IndexSet}; +use rustix::fs::{SeekFrom, seek}; +use wayland_protocols::wp::linux_dmabuf::zv1::server::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_feedback_v1::{self, TrancheFlags}, + zwp_linux_dmabuf_v1, +}; +use wayland_server::{ + Client, Dispatch, DisplayHandle, GlobalDispatch, Resource, WEnum, + backend::{GlobalId, InvalidId}, + protocol::{ + wl_buffer::{self, WlBuffer}, + wl_surface::WlSurface, + }, +}; + +#[cfg(feature = "backend_drm")] +use crate::backend::drm::DrmNode; +use crate::{ + backend::allocator::{ + Format, Fourcc, Modifier, + dmabuf::{Dmabuf, DmabufFlags, Plane}, + }, + utils::{SealedFile, UnmanagedResource, ids::id_gen}, +}; + +use super::{buffer::BufferHandler, compositor}; + +fn dmabuf_flags_from_wire(raw: u32) -> DmabufFlags { + DmabufFlags::from_bits_retain(raw) +} + +#[derive(Debug, Clone, PartialEq)] +struct DmabufFeedbackTranche { + target_device: libc::dev_t, + flags: zwp_linux_dmabuf_feedback_v1::TrancheFlags, + indices: IndexSet, + version_range: RangeInclusive, +} + +#[derive(Debug)] +struct DmabufFeedbackFormatTable { + formats: IndexSet, + file: SealedFile, +} + +#[derive(Debug)] +struct DmabufFeedbackInner { + main_device: libc::dev_t, + format_table: DmabufFeedbackFormatTable, + tranches: Vec, +} + +impl PartialEq for DmabufFeedbackInner { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.main_device == other.main_device + && self.format_table.formats == other.format_table.formats + && self.tranches == other.tranches + } +} + +/// Builder for [`DmabufFeedback`] +#[derive(Debug, Clone)] +pub struct DmabufFeedbackBuilder { + main_device: libc::dev_t, + main_tranche: DmabufFeedbackTranche, + formats: IndexSet, + preferred_tranches: Vec, +} + +#[derive(Copy, Clone)] +struct DmabufFeedbackFormat { + format: u32, + _reserved: u32, + modifier: u64, +} + +impl DmabufFeedbackFormat { + fn to_ne_bytes(self) -> [u8; 16] { + let format: [u8; 4] = self.format.to_ne_bytes(); + let reserved: [u8; 4] = self._reserved.to_ne_bytes(); + let modifier: [u8; 8] = self.modifier.to_ne_bytes(); + + [ + format[0], + format[1], + format[2], + format[3], + reserved[0], + reserved[1], + reserved[2], + reserved[3], + modifier[0], + modifier[1], + modifier[2], + modifier[3], + modifier[4], + modifier[5], + modifier[6], + modifier[7], + ] + } +} + +impl From for DmabufFeedbackFormat { + #[inline] + fn from(format: Format) -> Self { + DmabufFeedbackFormat { + format: format.code as u32, + _reserved: 0, + modifier: format.modifier.into(), + } + } +} + +impl DmabufFeedbackBuilder { + /// Create a new feedback builder with the specified device and formats as the main device + /// + /// Preference tranches can be added with [`DmabufFeedbackBuilder::add_preference_tranche`] + /// and the main tranche will be put after all preference tranches + pub fn new(main_device: libc::dev_t, formats: impl IntoIterator) -> Self { + let feedback_formats: IndexSet = formats.into_iter().collect(); + let format_indices: IndexSet = (0..feedback_formats.len()).collect(); + let main_tranche = DmabufFeedbackTranche { + flags: zwp_linux_dmabuf_feedback_v1::TrancheFlags::Sampling, + indices: format_indices, + target_device: main_device, + version_range: 3u32..=6, + }; + + Self { + main_device, + formats: feedback_formats, + main_tranche, + preferred_tranches: Vec::new(), + } + } + + /// Adds a preference tranche to the builder + /// + /// The tranches will be reported in the order they have been added with + /// this function. + /// + /// Note: Formats already present in a previously added preference tranche with the + /// same target device and flags will be skipped. + /// If all formats are already included in a previously added tranche + /// with the same target device and flags this tranche will be skipped. + pub fn add_preference_tranche( + mut self, + target_device: libc::dev_t, + flags: zwp_linux_dmabuf_feedback_v1::TrancheFlags, + formats: impl IntoIterator, + version: impl Into>, + ) -> Self { + let mut tranche = DmabufFeedbackTranche { + target_device, + flags, + indices: Default::default(), + version_range: version.into(), + }; + + for format in formats { + let (format_index, added) = self.formats.insert_full(format); + + // Compositors must not send duplicate format + modifier pairs within + // the same tranche or across two different tranches with the same target device and flags. + // + // if the format has just been added there is no need to test if a previous tranche + // with the same target device and flags already contains the format + let duplicate_format = !added + && self.preferred_tranches.iter().any(|tranche| { + tranche.target_device == target_device + && tranche.flags == flags + && tranche.indices.contains(&format_index) + }); + + if duplicate_format { + continue; + } + + // ...Compositors must not send duplicate format + modifier pairs within the same tranche... + // this is handled by using a IndexSet which won't hold duplicates + tranche.indices.insert(format_index); + } + + if !tranche.indices.is_empty() { + self.preferred_tranches.push(tranche); + } + + self + } + + /// Build the [`DmabufFeedback`] + /// + /// Returns an error if the format table shared memory file could + /// not be created. + pub fn build(mut self) -> Result { + let formats = self + .formats + .iter() + .copied() + .map(DmabufFeedbackFormat::from) + .flat_map(DmabufFeedbackFormat::to_ne_bytes) + .collect::>(); + + let name = c"smithay-dmabuffeedback-format-table"; + let format_table_file = SealedFile::with_data(name, &formats)?; + + // remove all formats from the main tranche that are already covered + // by a preference tranche + for duplicate_main_tranche in self.preferred_tranches.iter().filter(|tranche| { + tranche.target_device == self.main_tranche.target_device + && tranche.flags == self.main_tranche.flags + }) { + self.main_tranche.indices = self.main_tranche.indices.sub(&duplicate_main_tranche.indices); + } + + if !self.main_tranche.indices.is_empty() { + self.preferred_tranches.push(self.main_tranche); + } + + Ok(DmabufFeedback(Arc::new(DmabufFeedbackInner { + main_device: self.main_device, + format_table: DmabufFeedbackFormatTable { + file: format_table_file, + formats: self.formats, + }, + tranches: self.preferred_tranches, + }))) + } +} + +/// Feedback for dmabuf allocation +/// +/// Use the [`DmabufFeedbackBuilder`] to create a new instance. +#[derive(Debug, Clone)] +pub struct DmabufFeedback(Arc); + +impl PartialEq for DmabufFeedback { + #[inline] + fn eq(&self, other: &Self) -> bool { + // Note: The dmabuf feedback can not change, it is initialized + // with the DmabufFeedbackBuilder, so if the arc ptr equal we + // can short-circuit the expensive equality test saving us + // a few cpu cycles + if Arc::ptr_eq(&self.0, &other.0) { + return true; + } + + self.0 == other.0 + } +} + +impl DmabufFeedback { + /// Send this feedback to the provided [`ZwpLinuxDmabufFeedbackV1`](zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1) + pub fn send(&self, feedback: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1) { + if feedback.version() <= 5 { + feedback.main_device(self.0.main_device.to_ne_bytes().to_vec()); + } + feedback.format_table( + self.0.format_table.file.as_fd(), + self.0.format_table.file.size() as u32, + ); + + for tranche in self + .0 + .tranches + .iter() + .filter(|tranche| tranche.version_range.contains(&feedback.version())) + { + feedback.tranche_target_device(tranche.target_device.to_ne_bytes().to_vec()); + let mut flags = tranche.flags; + if feedback.version() <= 5 { + flags.remove(TrancheFlags::Sampling); + } + feedback.tranche_flags(flags); + feedback.tranche_formats( + tranche + .indices + .iter() + .flat_map(|i| (*i as u16).to_ne_bytes()) + .collect::>(), + ); + feedback.tranche_done(); + } + + feedback.done(); + } + + fn main_formats(&self) -> Vec { + self.0 + .tranches + .iter() + .filter(|tranche| { + tranche.target_device == self.0.main_device + && tranche.flags == TrancheFlags::Sampling + && tranche.version_range.contains(&3) + }) + .map(|tranche| tranche.indices.clone()) + .reduce(|mut acc, item| { + acc.extend(item); + acc + }) + .unwrap_or_default() + .into_iter() + .map(|index| self.0.format_table.formats[index]) + .collect() + } +} + +#[derive(Debug)] +struct SurfaceDmabufFeedbackStateInner { + feedback: DmabufFeedback, + known_instances: Vec>, +} + +/// Feedback state for a surface +#[derive(Debug, Clone)] +pub struct SurfaceDmabufFeedbackState { + inner: Arc>, +} + +impl SurfaceDmabufFeedbackState { + /// Get the surface dmabuf feedback stored in the surface states + /// + /// Returns `None` if no feedback has been requested + pub fn from_states(states: &compositor::SurfaceData) -> Option<&Self> { + states.data_map.get::() + } + + /// Set the feedback for this surface + /// + /// Note: If the feedback equals the current feedback this function does nothing + pub fn set_feedback(&self, feedback: &DmabufFeedback) { + let mut guard = self.inner.lock().unwrap(); + if &guard.feedback == feedback { + return; + } + + for instance in guard.known_instances.iter().filter_map(|i| i.upgrade().ok()) { + feedback.send(&instance); + } + + guard.feedback = feedback.clone(); + } + + fn add_instance( + &self, + instance: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + ) -> DmabufFeedback { + let mut guard = self.inner.lock().unwrap(); + guard.known_instances.push(instance.downgrade()); + guard.feedback.clone() + } + + fn remove_instance(&self, instance: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1) { + self.inner + .lock() + .unwrap() + .known_instances + .retain(|i| i != instance); + } +} + +#[derive(Debug)] +struct DmabufGlobalState { + id: GlobalId, + + default_feedback: Option>>, + known_default_feedbacks: + Arc>>>, +} + +/// Delegate type for all dmabuf globals. +/// +/// Dmabuf globals are created using this type and events will be forwarded to an instance of the dmabuf global. +#[derive(Debug)] +pub struct DmabufState { + /// Globals managed by the dmabuf handler. + globals: HashMap, +} + +impl DmabufState { + /// Creates a new [`DmabufState`] delegate type. + #[allow(clippy::new_without_default)] + pub fn new() -> DmabufState { + DmabufState { + globals: HashMap::new(), + } + } + + /// Creates a dmabuf global with the specified supported formats. + /// + /// Note: This function will create a version 3 dmabuf global and thus not call [`DmabufHandler::new_surface_feedback`], + /// if you want to create a version 4 global you need to call [`DmabufState::create_global_with_default_feedback`]. + pub fn create_global( + &mut self, + display: &DisplayHandle, + formats: impl IntoIterator, + ) -> DmabufGlobal + where + D: GlobalDispatch + + BufferHandler + + DmabufHandler + + 'static, + { + self.create_global_with_filter::(display, formats, |_| true) + } + + /// Creates a dmabuf global with the specified supported formats. + /// + /// This function unlike [`DmabufState::create_global`] also allows you to specify a filter function to + /// determine which clients may see this global. This functionality may be used on multi-gpu systems in + /// order to make a client choose the correct gpu. + /// + /// Note: This function will create a version 3 dmabuf global and thus not call [`DmabufHandler::new_surface_feedback`], + /// if you want to create a version 4 global you need to call [`DmabufState::create_global_with_filter_and_default_feedback`] + pub fn create_global_with_filter( + &mut self, + display: &DisplayHandle, + formats: impl IntoIterator, + filter: F, + ) -> DmabufGlobal + where + D: GlobalDispatch + + BufferHandler + + DmabufHandler + + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let formats = formats.into_iter().collect::>(); + self.create_global_with_filter_and_optional_default_feedback::( + display, + Some(formats), + None, + filter, + ) + } + + /// Creates a dmabuf global with the specified default feedback. + /// + /// Clients binding to version 3 or lower will receive the formats from the main tranche. + pub fn create_global_with_default_feedback( + &mut self, + display: &DisplayHandle, + default_feedback: &DmabufFeedback, + ) -> DmabufGlobal + where + D: GlobalDispatch + + BufferHandler + + DmabufHandler + + 'static, + { + self.create_global_with_filter_and_default_feedback::(display, default_feedback, |_| true) + } + + /// Creates a dmabuf global with the specified supported formats and default feedback + /// + /// This function unlike [`DmabufState::create_global_with_default_feedback`] also allows you to specify a filter function to + /// determine which clients may see this global. This functionality may be used on multi-gpu systems in + /// order to make a client choose the correct gpu. + /// + /// Clients binding to version 3 or lower will receive the formats from the main tranche. + pub fn create_global_with_filter_and_default_feedback( + &mut self, + display: &DisplayHandle, + default_feedback: &DmabufFeedback, + filter: F, + ) -> DmabufGlobal + where + D: GlobalDispatch + + BufferHandler + + DmabufHandler + + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + self.create_global_with_filter_and_optional_default_feedback::( + display, + None, + Some(default_feedback), + filter, + ) + } + + fn create_global_with_filter_and_optional_default_feedback( + &mut self, + display: &DisplayHandle, + formats: Option>, + default_feedback: Option<&DmabufFeedback>, + filter: F, + ) -> DmabufGlobal + where + D: GlobalDispatch + + BufferHandler + + DmabufHandler + + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let id = global_id::next(); + + let formats = formats + .or_else(|| default_feedback.map(|f| f.main_formats())) + .unwrap() + .into_iter() + .fold( + IndexMap::>::new(), + |mut formats, format| { + if let Some(modifiers) = formats.get_mut(&format.code) { + modifiers.insert(format.modifier); + } else { + formats.insert(format.code, IndexSet::from_iter(std::iter::once(format.modifier))); + } + formats + }, + ); + + let formats = Arc::new(formats); + let version = if default_feedback.is_some() { 6 } else { 3 }; + + let known_default_feedbacks = Arc::new(Mutex::new(Vec::new())); + let default_feedback = default_feedback.map(|f| Arc::new(Mutex::new(f.clone()))); + + let data = DmabufGlobalData { + filter: Box::new(filter), + formats, + default_feedback: default_feedback.clone(), + known_default_feedbacks: known_default_feedbacks.clone(), + id, + }; + + let global = display.create_global::(version, data); + self.globals.insert( + id, + DmabufGlobalState { + id: global, + default_feedback, + known_default_feedbacks, + }, + ); + + DmabufGlobal { id } + } + + /// Set the default [`DmabufFeedback`] for the specified global and send it to + /// all known default feedbacks. + /// + /// Note: This will do nothing if the global is not found, the global has been + /// initialized without feedback or the feedback equals the current default feedback. + pub fn set_default_feedback(&self, global: &DmabufGlobal, default_feedback: &DmabufFeedback) { + let Some(global) = self.globals.get(&global.id) else { + return; + }; + + let Some(mut current_feedback) = global.default_feedback.as_ref().map(|f| f.lock().unwrap()) else { + return; + }; + + // No need to update if the feedback did not change + if &*current_feedback == default_feedback { + return; + } + + let known_default_feedbacks = global.known_default_feedbacks.lock().unwrap(); + for feedback in known_default_feedbacks.iter().filter_map(|f| f.upgrade().ok()) { + default_feedback.send(&feedback); + } + + *current_feedback = default_feedback.clone(); + } + + /// Disables a dmabuf global. + /// + /// This operation is permanent and there is no way to re-enable a global. + pub fn disable_global(&mut self, display: &DisplayHandle, global: &DmabufGlobal) { + if let Some(global_state) = self.globals.get(&global.id) { + display.disable_global::(global_state.id.clone()); + } + } + + /// Destroys a dmabuf global. + /// + /// It is highly recommended you disable the global before destroying it and ensure all child objects have + /// been destroyed. + pub fn destroy_global(&mut self, display: &DisplayHandle, global: DmabufGlobal) { + if global_id::remove(global.id) { + if let Some(global_state) = self.globals.remove(&global.id) { + display.remove_global::(global_state.id); + } + } + } +} + +/// Data associated with a dmabuf global. +#[allow(missing_debug_implementations)] +pub struct DmabufGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, + formats: Arc>>, + default_feedback: Option>>, + known_default_feedbacks: + Arc>>>, + id: usize, +} + +/// Data associated with a dmabuf global protocol object. +#[derive(Debug)] +pub struct DmabufData { + formats: Arc>>, + id: usize, + + default_feedback: Option>>, + known_default_feedbacks: + Arc>>>, +} + +/// Data associated with a dmabuf global protocol object. +#[derive(Debug)] +pub struct DmabufFeedbackData { + known_default_feedbacks: + Arc>>>, + surface: Option>, +} + +/// Data associated with a pending [`Dmabuf`] import. +#[derive(Debug)] +pub struct DmabufParamsData { + /// Id of the dmabuf global these params were created from. + id: usize, + + /// Whether the params protocol object has been used before to create a wl_buffer. + used: AtomicBool, + + formats: Arc>>, + + /// Pending planes for the params. + modifier: Mutex>, + planes: Mutex>, + + node: Mutex>, +} + +/// A handle to a registered dmabuf global. +/// +/// This type may be used in equitability checks to determine which global a dmabuf is being imported to. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct DmabufGlobal { + id: usize, +} + +/// An object to allow asynchronous creation of a [`Dmabuf`] backed [`WlBuffer`]. +/// +/// This object is [`Send`] to allow import of a [`Dmabuf`] to take place on another thread if desired. +#[must_use = "This object must be used to notify the client whether dmabuf import succeeded"] +#[derive(Debug)] +pub struct ImportNotifier { + inner: ZwpLinuxBufferParamsV1, + display: DisplayHandle, + dmabuf: Dmabuf, + import: Import, + drop_ignore: bool, +} + +/// Type of dmabuf import. +#[derive(Debug)] +enum Import { + /// The import can fail or create a WlBuffer. + Falliable, + + /// A WlBuffer object has already been created. Failure causes client death. + Infallible(WlBuffer), +} + +impl ImportNotifier { + /// Returns the client trying to import this dmabuf, if not dead. + pub fn client(&self) -> Option { + self.inner.client() + } + + /// Dmabuf import was successful. + /// + /// This can return [`InvalidId`] if the client the buffer was imported from has died. + pub fn successful(mut self) -> Result + where + D: Dispatch + + Dispatch + + BufferHandler + + DmabufHandler + + 'static, + { + let client = self.inner.client(); + + let result = match self.import { + Import::Falliable => { + if let Some(client) = client { + match client.create_resource::( + &self.display, + 1, + self.dmabuf.clone(), + ) { + Ok(buffer) => { + self.inner.created(&buffer); + Ok(buffer) + } + + Err(err) => { + tracing::error!("failed to create protocol object for \"create\" request"); + Err(err) + } + } + } else { + tracing::error!("client was dead while creating wl_buffer resource"); + self.inner.post_error( + zwp_linux_buffer_params_v1::Error::InvalidWlBuffer, + "create_immed failed and produced an invalid wl_buffer", + ); + Err(InvalidId) + } + } + Import::Infallible(ref buffer) => Ok(buffer.clone()), + }; + + self.drop_ignore = true; + result + } + + /// The buffer being imported is incomplete. + /// + /// This may be the result of too few or too many planes being used when creating a buffer. + pub fn incomplete(mut self) { + self.inner.post_error( + zwp_linux_buffer_params_v1::Error::Incomplete, + "missing or too many planes to create a buffer", + ); + self.drop_ignore = true; + } + + /// The buffer being imported has an invalid width or height. + pub fn invalid_dimensions(mut self) { + self.inner.post_error( + zwp_linux_buffer_params_v1::Error::InvalidDimensions, + "width or height of dmabuf is invalid", + ); + self.drop_ignore = true; + } + + /// Import failed due to an invalid format and plane combination. + /// + /// This is always a client error and will result in the client being killed. + pub fn invalid_format(mut self) { + self.inner.post_error( + zwp_linux_buffer_params_v1::Error::InvalidFormat, + "format and plane combination are not valid", + ); + self.drop_ignore = true; + } + + /// Import failed for an implementation dependent reason. + pub fn failed(mut self) { + if matches!(self.import, Import::Falliable) { + self.inner.failed(); + } else { + self.inner.post_error( + zwp_linux_buffer_params_v1::Error::InvalidWlBuffer, + "create_immed failed and produced an invalid wl_buffer", + ); + } + self.drop_ignore = true; + } + + fn new(params: ZwpLinuxBufferParamsV1, display: DisplayHandle, dmabuf: Dmabuf, import: Import) -> Self { + Self { + inner: params, + display, + dmabuf, + import, + drop_ignore: false, + } + } +} + +impl Drop for ImportNotifier { + fn drop(&mut self) { + if !self.drop_ignore { + tracing::warn!( + "Compositor bug: Server ignored ImportNotifier for {:?}", + self.inner + ); + } + } +} + +/// Handler trait for [`Dmabuf`] import from the compositor. +pub trait DmabufHandler: BufferHandler { + /// Returns a mutable reference to the [`DmabufState`] delegate type. + fn dmabuf_state(&mut self) -> &mut DmabufState; + + /// This function is called when a client has imported a [`Dmabuf`]. + /// + /// The `global` indicates which [`DmabufGlobal`] the buffer was imported to. You should import the dmabuf + /// into your renderer to ensure the dmabuf may be used later when rendering. + /// + /// Whether dmabuf import succeeded is notified through the [`ImportNotifier`] object provided in this function. + fn dmabuf_imported(&mut self, global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier); + + /// This function allows to override the default [`DmabufFeedback`] for a surface + /// + /// Note: This will only be called if this will be the first time a surface requests feedback. + /// + /// Returning `None` will use the default [`DmabufFeedback`] from the global + fn new_surface_feedback( + &mut self, + _surface: &WlSurface, + _global: &DmabufGlobal, + ) -> Option { + None + } +} + +/// Gets the contents of a [`Dmabuf`] backed [`WlBuffer`]. +/// +/// If the buffer is managed by the dmabuf handler, the [`Dmabuf`] is returned. +/// +/// If the buffer is not managed by the dmabuf handler (whether the buffer is a different kind of buffer, +/// such as an shm buffer or is not managed by smithay), this function will return an [`UnmanagedResource`] +/// error. +/// +/// [`WlBuffer`]: wl_buffer::WlBuffer +pub fn get_dmabuf(buffer: &wl_buffer::WlBuffer) -> Result<&Dmabuf, UnmanagedResource> { + buffer.data::().ok_or(UnmanagedResource) +} + +impl DmabufParamsData { + /// Emits a protocol error if the params have already been used to create a dmabuf. + /// + /// This returns true if the protocol object has not been used. + fn ensure_unused(&self, params: &ZwpLinuxBufferParamsV1) -> bool { + if !self.used.load(Ordering::Relaxed) { + return true; + } + + params.post_error( + zwp_linux_buffer_params_v1::Error::AlreadyUsed, + "This buffer_params has already been used to create a buffer.", + ); + + false + } + + /// Attempt to create a Dmabuf from the parameters. + /// + /// This function will perform the necessary validation of all the parameters, emitting protocol errors as + /// needed. + /// + /// A return value of [`None`] indicates buffer import has failed and the client has been killed. + fn create_dmabuf( + &self, + params: &ZwpLinuxBufferParamsV1, + width: i32, + height: i32, + format: u32, + flags: WEnum, + _node: Option, + ) -> Option { + // We cannot create a dmabuf if the parameters have already been used. + if !self.ensure_unused(params) { + return None; + } + + self.used.store(true, Ordering::Relaxed); + + let format = match Fourcc::try_from(format) { + Ok(format) => format, + Err(_) => { + params.post_error( + zwp_linux_buffer_params_v1::Error::InvalidFormat, + format!("Format {format:x} is not supported"), + ); + + return None; + } + }; + + // Validate buffer parameters: + // 1. Must have known format + if !self.formats.contains_key(&format) { + params.post_error( + zwp_linux_buffer_params_v1::Error::InvalidFormat, + format!("Format {format:?}/{:x} is not supported.", format as u32), + ); + return None; + } + + // 2. Width and height must be positive + if width < 1 { + params.post_error( + zwp_linux_buffer_params_v1::Error::InvalidDimensions, + "invalid width", + ); + } + + if height < 1 { + params.post_error( + zwp_linux_buffer_params_v1::Error::InvalidDimensions, + "invalid height", + ); + } + + // 3. Validate all the planes + let mut planes = self.planes.lock().unwrap(); + + for plane in &*planes { + // Must not overflow + let end = match plane + .stride + .checked_mul(height as u32) + .and_then(|o| o.checked_add(plane.offset)) + { + Some(e) => e, + + None => { + params.post_error( + zwp_linux_buffer_params_v1::Error::OutOfBounds, + format!("Size overflow for plane {}.", plane.plane_idx), + ); + + return None; + } + }; + + if let Ok(size) = seek(&plane.fd, SeekFrom::End(0)) { + // Reset seek point + let _ = seek(&plane.fd, SeekFrom::Start(0)); + + if plane.offset as u64 > size { + params.post_error( + zwp_linux_buffer_params_v1::Error::OutOfBounds, + format!("Invalid offset {} for plane {}.", plane.offset, plane.plane_idx), + ); + + return None; + } + + if (plane.offset + plane.stride) as u64 > size { + params.post_error( + zwp_linux_buffer_params_v1::Error::OutOfBounds, + format!("Invalid stride {} for plane {}.", plane.stride, plane.plane_idx), + ); + + return None; + } + + // Planes > 0 can be subsampled, in which case 'size' will be smaller than expected. + if plane.plane_idx == 0 && end as u64 > size { + params.post_error( + zwp_linux_buffer_params_v1::Error::OutOfBounds, + format!( + "Invalid stride ({}) or height ({}) for plane {}.", + plane.stride, height, plane.plane_idx + ), + ); + + return None; + } + } + } + + let modifier = self.modifier.lock().unwrap().unwrap_or(Modifier::Invalid); + let mut buf = Dmabuf::builder( + (width, height), + format, + modifier, + dmabuf_flags_from_wire(flags.into()), + ); + + planes.sort_by_key(|plane| plane.plane_idx); + for (i, plane) in planes.drain(..).enumerate() { + if plane.plane_idx != i as u32 { + // After sorting, plane indices should be consecutive and start at 0. + params.post_error( + zwp_linux_buffer_params_v1::Error::Incomplete, + "missing or too many planes to create a buffer", + ); + return None; + } + buf.add_plane(plane.fd, plane.offset, plane.stride); + } + + #[cfg(feature = "backend_drm")] + if let Some(node) = _node.and_then(|node| DrmNode::from_dev_id(node).ok()) { + buf.set_node(node); + } + + let dmabuf = match buf.build() { + Some(buf) => buf, + + None => { + params.post_error( + zwp_linux_buffer_params_v1::Error::Incomplete, + "Provided buffer is incomplete, it has zero planes", + ); + return None; + } + }; + + Some(dmabuf) + } +} + +id_gen!(global_id); diff --git a/vendor/smithay/src/wayland/drm_lease/mod.rs b/vendor/smithay/src/wayland/drm_lease/mod.rs new file mode 100644 index 0000000..0fd220a --- /dev/null +++ b/vendor/smithay/src/wayland/drm_lease/mod.rs @@ -0,0 +1,929 @@ +//! DRM Lease protocol +//! +//! This module provides helpers to handle the wp_drm_lease_v1 protocol, +//! which allows clients to lease DRM resources from the compositor. +//! +//! This is in particular useful for VR applications, that would like to take over +//! a directly attached VR display from the DRM device otherwise controlled by the compositor. +//! In theory however any sorts of DRM resources can be lend out to less privileged clients via this protocol. +//! +//! ## How to use +//! +//! To setup the drm_lease global, you will need to first provide the `DrmNode` you want to lease resources from. +//! You can usually get that from your [`DrmDevice`]. Once the global is up, +//! you can advertise connectors as available through [`DrmLeaseState::add_connector`]. +//! Should a connector become unavailable or is used by the compositor, you may remove it again using [`DrmLeaseState::withdraw_connector`]. +//! +//! Any client requests will be issued through [`DrmLeaseHandler::lease_request`], which +//! allows you to add additional needed DRM resources to the lease and accept or decline the request. +//! +//! ```no_run +//! # use smithay::wayland::drm_lease::*; +//! # use smithay::backend::drm::{DrmDevice, DrmNode}; +//! +//! pub struct State { +//! drm_lease_state: DrmLeaseState, +//! active_leases: Vec, +//! } +//! +//! # impl State { +//! # fn get_device_for_node(&self, node: &DrmNode) -> &DrmDevice { todo!() } +//! # } +//! +//! impl DrmLeaseHandler for State { +//! fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState { &mut self.drm_lease_state } +//! fn lease_request(&mut self, node: DrmNode, request: DrmLeaseRequest) -> Result { +//! let device = self.get_device_for_node(&node); +//! let mut builder = DrmLeaseBuilder::new(device); +//! for connector in request.connectors { +//! builder.add_connector(connector); +//! # let some_free_crtc = todo!(); +//! # let primary_plane_for_connector = todo!(); +//! # let primary_plane_claim_for_connector = todo!(); +//! builder.add_crtc(some_free_crtc); +//! builder.add_plane(primary_plane_for_connector, primary_plane_claim_for_connector); +//! } +//! Ok(builder) +//! } +//! fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) { self.active_leases.push(lease); } +//! fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) { self.active_leases.retain(|l| l.id() != lease_id); } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! # let drm_device: DrmDevice = todo!(); +//! let mut drm_lease_state = DrmLeaseState::new::( +//! &display.handle(), +//! &DrmNode::from_file( +//! drm_device.device_fd() +//! ).unwrap() +//! ).unwrap(); +//! +//! // Add some connectors +//! # let some_connector = todo!(); +//! drm_lease_state.add_connector::(some_connector, "Unknown".into(), "Unknown".into()); +//! +//! let state = State { +//! drm_lease_state, +//! active_leases: Vec::new(), +//! }; +//! +//! // Rest of the compositor goes here +//! ``` + +use std::{ + collections::{HashMap, HashSet}, + fmt, io, + num::NonZeroU32, + os::unix::{io::OwnedFd, prelude::AsFd}, + path::{Path, PathBuf}, + sync::{ + Arc, Mutex, Weak, + atomic::{AtomicBool, Ordering}, + }, +}; + +use drm::control::{Device, RawResourceHandle, connector, crtc, plane}; +use rustix::fs::OFlags; +use wayland_protocols::wp::drm_lease::v1::server::*; +use wayland_server::backend::GlobalId; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; + +use crate::{ + backend::drm::{DrmDevice, DrmDeviceFd, DrmNode, NodeType, PlaneClaim}, + wayland::{Dispatch2, GlobalDispatch2}, +}; + +/// Delegate type for a drm_lease global +#[derive(Debug)] +pub struct DrmLeaseState { + node: DrmNode, + dh: DisplayHandle, + global: Option, + connectors: Vec, + known_lease_devices: Vec, + active_leases: Vec, +} + +#[derive(Debug)] +struct DrmLeaseConnector { + name: String, + description: String, + node: DrmNode, + handle: connector::Handle, + enabled: bool, + known_instances: Vec, +} + +/// Data attached to wp_drm_lease_request_v1 objects +#[derive(Debug)] +pub struct DrmLeaseRequestData { + node: DrmNode, + connectors: Mutex>, +} + +/// DRM lease request containing a set of requested connectors +#[derive(Debug)] +pub struct DrmLeaseRequest { + /// requested connectors + pub connectors: Vec, +} + +/// Builder struct to collect DRM resources to be leased +#[derive(Debug)] +pub struct DrmLeaseBuilder { + drm: DrmDeviceFd, + planes: HashMap, + connectors: HashSet, + crtcs: HashSet, +} + +impl DrmLeaseBuilder { + /// Create a new builder from a DRM Device + pub fn new(drm: &DrmDevice) -> DrmLeaseBuilder { + DrmLeaseBuilder { + drm: drm.device_fd().clone(), + planes: HashMap::new(), + connectors: HashSet::new(), + crtcs: HashSet::new(), + } + } + + /// Add a CRTC to the to be leased resources + pub fn add_crtc(&mut self, crtc: crtc::Handle) { + self.crtcs.insert(crtc); + } + + /// Add a connector to the to be leased resources + pub fn add_connector(&mut self, conn: connector::Handle) { + self.connectors.insert(conn); + } + + /// Add a plane to the to be leased resources + pub fn add_plane(&mut self, plane: plane::Handle, claim: PlaneClaim) { + self.planes.insert(plane, claim); + } + + fn build(self) -> io::Result { + let objects: Vec = self + .planes + .keys() + .cloned() + .map(Into::into) + .chain(self.connectors.iter().cloned().map(Into::into)) + .chain(self.crtcs.iter().cloned().map(Into::into)) + .collect(); + let (id, fd) = self.drm.create_lease(&objects, OFlags::CLOEXEC.bits())?; + + Ok(DrmLease { + drm: self.drm.clone(), + planes: self.planes, + connectors: self.connectors, + crtcs: self.crtcs, + lease_id: id, + obj: Arc::new(Mutex::new(None)), + fd: Arc::new(Mutex::new(Some(fd))), + revoked: Arc::new(AtomicBool::new(false)), + }) + } +} + +/// Active DRM Lease +/// +/// Dropping will revoke the lease +#[derive(Debug, Clone)] +pub struct DrmLease { + drm: DrmDeviceFd, + planes: HashMap, + connectors: HashSet, + crtcs: HashSet, + lease_id: NonZeroU32, + obj: Arc>>, + fd: Arc>>, + revoked: Arc, +} + +impl DrmLease { + /// CRTCs being leased + pub fn crtcs(&self) -> impl Iterator { + self.crtcs.iter() + } + /// Connectors being leased + pub fn connectors(&self) -> impl Iterator { + self.connectors.iter() + } + /// Planes being leased + pub fn planes(&self) -> impl Iterator { + self.planes.keys() + } + /// Lease Id + pub fn id(&self) -> u32 { + self.lease_id.get() + } + + fn set_obj(&self, obj: wp_drm_lease_v1::WpDrmLeaseV1) { + *self.obj.lock().unwrap() = Some(obj); + } + fn take_fd(&mut self) -> Option { + self.fd.lock().unwrap().take() + } +} + +#[derive(Debug)] +struct DrmLeaseRef { + drm: DrmDeviceFd, + obj: Weak>>, + lease_id: NonZeroU32, + revoked: Arc, + connectors: HashSet, +} + +impl Drop for DrmLease { + fn drop(&mut self) { + if let Some(obj) = &self.obj.lock().unwrap().take() { + obj.finished(); + } + if !self.revoked.swap(true, Ordering::SeqCst) { + tracing::info!(?self.lease_id, "Revoking lease"); + if let Err(err) = self.drm.revoke_lease(self.lease_id) { + tracing::warn!(?err, "Error revoking lease"); + }; + } + } +} + +impl DrmLeaseRef { + fn force_close(&self) { + if let Some(obj) = Weak::upgrade(&self.obj).and_then(|obj| obj.lock().unwrap().take()) { + obj.finished(); + } + if !self.revoked.swap(true, Ordering::SeqCst) { + tracing::info!(?self.lease_id, "Revoking lease"); + if let Err(err) = self.drm.revoke_lease(self.lease_id) { + tracing::warn!(?err, "Error revoking lease"); + }; + } + } +} + +/// Data attached to wp_drm_lease_v1 objects +#[derive(Debug)] +pub struct DrmLeaseData { + id: u32, + node: DrmNode, +} + +/// DRM lease was rejected by the compositor. +#[derive(Debug, Default)] +pub struct LeaseRejected(Option>); + +impl fmt::Display for LeaseRejected { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + Some(reason) => f.write_fmt(format_args!("Lease rejected, reason: {}", &**reason)), + None => f.write_str("Lease rejected"), + } + } +} + +impl std::error::Error for LeaseRejected { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.as_deref() + } +} + +impl LeaseRejected { + /// Wrap any error type in a `LeaseRejected` error to be returned from [`DrmLeaseHandler::lease_request`]. + pub fn with_cause(err: T) -> Self { + LeaseRejected(Some(Box::new(err))) + } +} + +/// Handler trait for drm leasing from the compositor. +pub trait DrmLeaseHandler { + /// Returns a mutable reference to the [`DrmLeaseState`] delegate type + fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState; + /// A client has issued a new request. + /// + /// The request only contains connectors and the compositor is lease any resources it may seem fit. + /// It is recommended however to lease the requested connectors and any resources necessary to drive them + /// (free CRTCs per connector and at least their primary plane) or reject the request. + /// + /// To reject the request return `Err(LeaseRejected::default())`, otherwise return a [`DrmLeaseBuilder`] with all the resources added. + fn lease_request( + &mut self, + node: DrmNode, + request: DrmLeaseRequest, + ) -> Result; + /// A new DRM lease is active. Dropping the provided [`DrmLease`] will revoke the lease. + fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease); + /// A DRM lease was destroyed, the previously given [`DrmLease`] should be cleaned up, if not revoked already. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32); +} + +/// Data attached to a wp_drm_lease_device_v1 global +pub struct DrmLeaseDeviceGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, + path: PathBuf, + node: DrmNode, +} + +impl fmt::Debug for DrmLeaseDeviceGlobalData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DrmLeaseDeviceGlobalData") + .field("path", &self.path) + .field("node", &self.node) + .finish() + } +} + +/// Errors thrown by the DRM lease global +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Unable to figure out the path of the drm device used + #[error("Unable to get modesetting node for drm device")] + UnableToGetPath, + /// Unable to get a new file descriptor for the drm device used + #[error("Unable to get file descriptor for drm device")] + UnableToGetFd, + /// Unable to open a new file descriptor for the drm device used + #[error("Unable to open new file descriptor for drm device")] + UnableToOpenNode(#[source] rustix::io::Errno), + /// Unable to drop DRM Master on a new file descriptor + #[error("Unable to drop master status on file descriptor for drm device")] + UnableToDropMaster(#[source] io::Error), +} + +fn get_non_master_fd>(path: P) -> Result { + let fd = rustix::fs::open( + path.as_ref(), + OFlags::RDWR | OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .map_err(Error::UnableToOpenNode)?; + + // Attempt to drop master unconditionally. EINVAL means the fd never had + // master to begin with (common on drivers like nvidia-drm that mark all + // clients as authenticated regardless of master status). EPERM means + // the file descriptor is not authenticated in the first place. + match drm_ffi::auth::release_master(fd.as_fd()) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::InvalidInput || e.kind() == io::ErrorKind::PermissionDenied => {} + Err(e) => return Err(Error::UnableToDropMaster(e)), + } + + Ok(fd) +} + +impl DrmLeaseState { + /// Create a new DRM lease global for a given [`DrmNode`]. + pub fn new(display: &DisplayHandle, drm_node: &DrmNode) -> Result + where + D: DrmLeaseHandler + + GlobalDispatch + + 'static, + { + Self::new_with_filter::(display, drm_node, |_| true) + } + + /// Create a new DRM lease global for a given [`DrmNode`] and a filter. + /// + /// Filters can be used to limit visibility of a global to certain clients. + pub fn new_with_filter( + display: &DisplayHandle, + drm_node: &DrmNode, + filter: F, + ) -> Result + where + D: DrmLeaseHandler + + GlobalDispatch + + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let path = drm_node + .dev_path_with_type(NodeType::Primary) + .ok_or(Error::UnableToGetPath)?; + + // test once if we can get a non-master fd before initializing the global + let _ = get_non_master_fd(&path)?; + + let data = DrmLeaseDeviceGlobalData { + filter: Box::new(filter), + path, + node: *drm_node, + }; + + let global = display.create_global::(1, data); + + Ok(DrmLeaseState { + node: *drm_node, + dh: display.clone(), + global: Some(global), + connectors: Vec::new(), + known_lease_devices: Vec::new(), + active_leases: Vec::new(), + }) + } + + /// Add a connector, that is free to be leased to clients. + pub fn add_connector(&mut self, connector: connector::Handle, name: String, description: String) + where + D: DrmLeaseHandler + + Dispatch + + 'static, + { + if self.connectors.iter().any(|conn| conn.handle == connector) { + return; + } + + let mut instances = Vec::new(); + for instance in &self.known_lease_devices { + if let Ok(client) = self.dh.get_client(instance.id()) { + if let Ok(lease_connector) = client + .create_resource::( + &self.dh, + 1, + DrmLeaseConnectorData { node: self.node }, + ) + { + instance.connector(&lease_connector); + lease_connector.name(name.clone()); + lease_connector.description(description.clone()); + lease_connector.connector_id(connector.into()); + instances.push(lease_connector); + instance.done(); + } + } + } + + self.connectors.push(DrmLeaseConnector { + name, + description, + node: self.node, + handle: connector, + enabled: true, + known_instances: instances, + }); + } + + /// Withdraw a connector from the set of connectors available for leasing + pub fn withdraw_connector(&mut self, connector: connector::Handle) { + #[allow(clippy::mutable_key_type)] + let mut clients = HashSet::new(); + if let Some(pos) = self.connectors.iter().position(|conn| conn.handle == connector) { + let lease_connector = self.connectors.remove(pos); + for instance in &lease_connector.known_instances { + instance.withdrawn(); + if let Some(client) = instance.client() { + clients.insert(client.id()); + } + } + } + for instance in &self.known_lease_devices { + if let Some(client) = instance.client() { + if clients.contains(&client.id()) { + instance.done(); + } + } + } + + self.active_leases + .retain(|lease| !lease.connectors.contains(&connector)); + } + + /// Suspend all connectors temporarily (e.g. upon losing DRM master as the session becomes inactive) + pub fn suspend(&mut self) { + self.suspend_internal(None); + } + + fn suspend_internal(&mut self, connectors: Option<&HashSet>) { + #[allow(clippy::mutable_key_type)] + let mut clients = HashSet::new(); + for connector in self.connectors.iter_mut().filter(|c| { + connectors + .map(|filter| filter.contains(&c.handle)) + .unwrap_or(true) + }) { + for instance in connector.known_instances.drain(..) { + instance.withdrawn(); + if let Some(client) = instance.client() { + clients.insert(client.id()); + } + } + connector.enabled = false; + } + for (instance, client) in self + .known_lease_devices + .iter() + .flat_map(|instance| instance.client().map(|client| (instance, client))) + { + if clients.contains(&client.id()) { + instance.done(); + } + } + } + + /// Resume all connectors temporarily (e.g. upon gaining DRM master as the session becomes active) + pub fn resume(&mut self) + where + D: DrmLeaseHandler + + Dispatch + + 'static, + { + self.resume_internal::(None); + } + + fn resume_internal(&mut self, connectors: Option<&HashSet>) + where + D: DrmLeaseHandler + + Dispatch + + 'static, + { + for (instance, client) in self + .known_lease_devices + .iter() + .flat_map(|instance| instance.client().map(|client| (instance, client))) + { + let mut once = false; + for connector in self.connectors.iter_mut().filter(|c| { + connectors + .map(|filter| filter.contains(&c.handle)) + .unwrap_or(true) + }) { + if let Some(lease_connector) = connector.new_instance::(&self.dh, &client) { + instance.connector(&lease_connector); + connector.send_info(&lease_connector); + once = true; + } + } + if once { + instance.done(); + } + } + for connector in self.connectors.iter_mut().filter(|c| { + connectors + .map(|filter| filter.contains(&c.handle)) + .unwrap_or(true) + }) { + connector.enabled = true; + } + } + + fn remove_lease(&mut self, id: u32) -> Option + where + D: DrmLeaseHandler + + Dispatch + + 'static, + { + let lease_ref = { + { + let pos = self + .active_leases + .iter() + .position(|lease| lease.lease_id.get() == id)?; + let lease = self.active_leases.remove(pos); + self.resume_internal::(Some(&lease.connectors)); + lease + } + }; + lease_ref.force_close(); + Some(lease_ref) + } + + /// [`DrmNode`] belonging to this DRM lease global + pub fn node(&self) -> DrmNode { + self.node + } + + /// Disable the global, it will no longer be advertised to new clients + pub fn disable_global(&mut self) + where + D: DrmLeaseHandler + 'static, + { + if let Some(global) = self.global.take() { + self.dh.disable_global::(global); + } + } +} + +impl Drop for DrmLeaseState { + fn drop(&mut self) { + // End all leases + self.active_leases.clear(); + + // withdraw all connectors + for connector in &self.connectors { + for instance in &connector.known_instances { + instance.withdrawn(); + } + } + + // end all devices + for device in &*self.known_lease_devices { + device.released(); + } + } +} + +impl DrmLeaseConnector { + fn new_instance( + &mut self, + dh: &DisplayHandle, + client: &Client, + ) -> Option + where + D: DrmLeaseHandler + + Dispatch + + 'static, + { + if let Ok(lease_connector) = client + .create_resource::( + dh, + 1, + DrmLeaseConnectorData { node: self.node }, + ) + { + self.known_instances.push(lease_connector.clone()); + + Some(lease_connector) + } else { + None + } + } + + fn send_info(&self, connector: &wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1) { + connector.name(self.name.clone()); + connector.description(self.description.clone()); + connector.connector_id(self.handle.into()); + connector.done(); + } +} + +impl GlobalDispatch2 for DrmLeaseDeviceGlobalData +where + D: DrmLeaseHandler + + Dispatch + + Dispatch + + 'static, +{ + fn bind( + &self, + state: &mut D, + dh: &DisplayHandle, + client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let drm_lease_state = state.drm_lease_state(self.node); + let wp_drm_lease_device = data_init.init(resource, DrmLeaseDeviceData { node: self.node }); + + let Ok(fd) = get_non_master_fd(&self.path) else { + // nothing we can do + wp_drm_lease_device.released(); + return; + }; + + wp_drm_lease_device.drm_fd(fd.as_fd()); + for connector in drm_lease_state.connectors.iter_mut().filter(|c| c.enabled) { + if let Some(id) = connector.new_instance::(dh, client) { + wp_drm_lease_device.connector(&id); + connector.send_info(&id); + } + } + wp_drm_lease_device.done(); + + drm_lease_state.known_lease_devices.push(wp_drm_lease_device); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct DrmLeaseDeviceData { + node: DrmNode, +} + +impl Dispatch2 for DrmLeaseDeviceData +where + D: DrmLeaseHandler + + Dispatch + + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + resource: &wp_drm_lease_device_v1::WpDrmLeaseDeviceV1, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_drm_lease_device_v1::Request::CreateLeaseRequest { id } => { + data_init.init( + id, + DrmLeaseRequestData { + node: self.node, + connectors: Mutex::new(Vec::new()), + }, + ); + } + wp_drm_lease_device_v1::Request::Release => { + let drm_lease_state = state.drm_lease_state(self.node); + drm_lease_state + .known_lease_devices + .retain(|device| device != resource); + resource.released(); + } + _ => {} + } + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct DrmLeaseConnectorData { + node: DrmNode, +} + +impl Dispatch2 for DrmLeaseConnectorData +where + D: DrmLeaseHandler + 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1, + _request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + resource: &wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1, + ) { + let drm_lease_state = state.drm_lease_state(self.node); + drm_lease_state.connectors.iter_mut().for_each(|connector| { + connector.known_instances.retain(|obj| obj != resource); + }); + } +} + +impl Dispatch2 for DrmLeaseRequestData +where + D: DrmLeaseHandler + Dispatch + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + resource: &wp_drm_lease_request_v1::WpDrmLeaseRequestV1, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_drm_lease_request_v1::Request::RequestConnector { connector } => { + self.connectors.lock().unwrap().push(connector); + } + wp_drm_lease_request_v1::Request::Submit { id } => { + let drm_lease_state = state.drm_lease_state(self.node); + + let mut connector_handles = Vec::new(); + for connector in &*self.connectors.lock().unwrap() { + if let Some(conn) = drm_lease_state + .connectors + .iter() + .find(|conn| conn.known_instances.iter().any(|obj| obj == connector)) + { + if connector_handles.iter().any(|handle| handle == &conn.handle) { + resource.post_error( + wp_drm_lease_request_v1::Error::DuplicateConnector, + format!("Duplicate connector: {}", conn.name), + ); + return; + } else { + connector_handles.push(conn.handle); + } + } else { + resource.post_error( + wp_drm_lease_request_v1::Error::WrongDevice, + "Requested lease for wrong device", + ); + return; + } + } + + if connector_handles.is_empty() { + resource.post_error( + wp_drm_lease_request_v1::Error::EmptyLease, + "Lease doesn't contain any connectors", + ); + } + + match state.lease_request( + self.node, + DrmLeaseRequest { + connectors: connector_handles, + }, + ) { + Ok(builder) => match builder.build() { + Ok(mut lease) => { + let lease_obj = data_init.init( + id, + DrmLeaseData { + id: lease.lease_id.get(), + node: self.node, + }, + ); + lease.set_obj(lease_obj.clone()); + let fd = lease.take_fd().unwrap(); + lease_obj.lease_fd(fd.as_fd()); + + let lease_ref = DrmLeaseRef { + drm: lease.drm.clone(), + obj: Arc::downgrade(&lease.obj), + lease_id: lease.lease_id, + connectors: lease.connectors.clone(), + revoked: lease.revoked.clone(), + }; + + let drm_lease_state = state.drm_lease_state(self.node); + drm_lease_state.suspend_internal(Some(&lease.connectors)); + drm_lease_state.active_leases.push(lease_ref); + + state.new_active_lease(self.node, lease); + } + Err(err) => { + tracing::error!(?err, "Failed to create lease"); + let lease_obj = data_init.init( + id, + DrmLeaseData { + id: 0, + node: self.node, + }, + ); + lease_obj.finished(); + } + }, + Err(err) => { + tracing::debug!(?err, "Compositor denied lease request"); + let lease_obj = data_init.init( + id, + DrmLeaseData { + id: 0, + node: self.node, + }, + ); + lease_obj.finished(); + } + } + } + _ => {} + } + } +} + +impl Dispatch2 for DrmLeaseData +where + D: DrmLeaseHandler + + Dispatch + + 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &wp_drm_lease_v1::WpDrmLeaseV1, + _request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + _resource: &wp_drm_lease_v1::WpDrmLeaseV1, + ) { + let drm_lease_state = state.drm_lease_state(self.node); + if let Some(lease_ref) = drm_lease_state.remove_lease::(self.id) { + state.lease_destroyed(self.node, lease_ref.lease_id.get()); + } + } +} diff --git a/vendor/smithay/src/wayland/drm_syncobj/mod.rs b/vendor/smithay/src/wayland/drm_syncobj/mod.rs new file mode 100644 index 0000000..7340c18 --- /dev/null +++ b/vendor/smithay/src/wayland/drm_syncobj/mod.rs @@ -0,0 +1,517 @@ +//! DRM syncobj protocol +//! +//! This module implement the `linux-drm-syncobj-v1` protocol, used to support +//! explicit sync. +//! +//! Currently, the implementation here assumes acquire fences are already signalled +//! when the surface transaction is ready. Use [`DrmSyncPointBlocker`]. +//! +//! The server should only expose the protocol if [`supports_syncobj_eventfd`] returns +//! `true`. Or it won't be possible to create the blocker. This is similar to other +//! implementations. +//! +//! The release fence is signalled when all references to a +//! [`Buffer`][crate::backend::renderer::utils::Buffer] are dropped. +//! +//! ```no_run +//! # use smithay::wayland::drm_syncobj::*; +//! +//! pub struct State { +//! syncobj_state: Option, +//! } +//! +//! impl DrmSyncobjHandler for State { +//! fn drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState> { +//! self.syncobj_state.as_mut() +//! } +//! } +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! # let import_device = todo!(); +//! let syncobj_state = if supports_syncobj_eventfd(&import_device) { +//! Some(DrmSyncobjState::new::(&display_handle, import_device)) +//! } else { +//! None +//! }; +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::{ + cell::RefCell, + os::unix::io::AsFd, + sync::{Arc, Weak}, +}; +use tracing::warn; +use wayland_protocols::wp::linux_drm_syncobj::v1::server::{ + wp_linux_drm_syncobj_manager_v1::{self, WpLinuxDrmSyncobjManagerV1}, + wp_linux_drm_syncobj_surface_v1::{self, WpLinuxDrmSyncobjSurfaceV1}, + wp_linux_drm_syncobj_timeline_v1::{self, WpLinuxDrmSyncobjTimelineV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak as WlWeak, + backend::GlobalId, protocol::wl_surface::WlSurface, +}; + +use super::{ + compositor::{self, BufferAssignment, Cacheable, HookId, SurfaceAttributes, with_states}, + dmabuf::get_dmabuf, +}; +use crate::{ + backend::drm::DrmDeviceFd, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +mod sync_point; +pub use sync_point::*; + +/// Test if DRM device supports `syncobj_eventfd`. +// Similar to test used in Mutter +pub fn supports_syncobj_eventfd(device: &DrmDeviceFd) -> bool { + // Pass device as placeholder for eventfd as well, since `drm_ffi` requires + // a valid fd. + match drm_ffi::syncobj::eventfd(device.as_fd(), 0, 0, device.as_fd(), false) { + Ok(_) => unreachable!(), + Err(err) => err.kind() == std::io::ErrorKind::NotFound, + } +} + +/// Handler trait for DRM syncobj protocol. +pub trait DrmSyncobjHandler { + /// Returns a mutable reference to the [`DrmSyncobjState`] delegate type + fn drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState>; +} + +/// Data associated with a drm syncobj global +#[allow(missing_debug_implementations)] +pub struct DrmSyncobjGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +/// Pending DRM syncobj sync point state +#[derive(Debug, Default)] +pub struct DrmSyncobjCachedState { + /// Timeline point signaled when buffer is ready to read + pub acquire_point: Option, + /// Timeline point to be signaled when server is done with buffer + pub release_point: Option, +} + +impl Cacheable for DrmSyncobjCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + Self { + acquire_point: self.acquire_point.take(), + release_point: self.release_point.take(), + } + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + if self.acquire_point.is_some() && self.release_point.is_some() { + if let Some(release_point) = &into.release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } + into.acquire_point = self.acquire_point; + into.release_point = self.release_point; + } + } +} + +/// Delegate type for a `wp_linux_drm_syncobj_manager_v1` global +#[derive(Debug)] +pub struct DrmSyncobjState { + global: GlobalId, + import_device: Option, + known_timelines: Vec>, +} + +impl DrmSyncobjState { + /// Create a new `wp_linux_drm_syncobj_manager_v1` global + /// + /// The `import_device` will be used to import the syncobj fds, and wait on them. + pub fn new(display: &DisplayHandle, import_device: DrmDeviceFd) -> Self + where + D: GlobalDispatch, + D: 'static, + { + Self::new_with_filter::(display, import_device, |_| true) + } + + /// Create a new `wp_linuxdrm_syncobj_manager_v1` global with a client filter + /// + /// The `import_device` will be used to import the syncobj fds, and wait on them. + pub fn new_with_filter(display: &DisplayHandle, import_device: DrmDeviceFd, filter: F) -> Self + where + D: GlobalDispatch, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let global = display.create_global::( + 1, + DrmSyncobjGlobalData { + filter: Box::new(filter), + }, + ); + + Self { + global, + import_device: Some(import_device), + known_timelines: Vec::new(), + } + } + + /// Closes the current `import_device`, allowing compositors to acquire a new fd. + pub fn close_device<'a>(&'a mut self) -> CloseGuard<'a> { + self.import_device.take(); + CloseGuard { state: self } + } + + /// Sets a new `import_device` to import the syncobj fds and wait on them. + pub fn update_device(&mut self, import_device: DrmDeviceFd) { + for timeline in self.known_timelines.iter().filter_map(Weak::upgrade) { + if let Err(err) = timeline.update_device(&import_device) { + warn!(?err, "Failed to update existing timeline"); + } + } + self.import_device = Some(import_device); + } + + /// Destroys the state and returns the `GlobalId` for compositors to disable/destroy. + /// + /// Note: This will cause any future timeline import to raise a protocol error for + /// clients that have bound this protocol until [`DrmSyncobjHandler::drm_syncobj_state`] returns `Some` again. + pub fn into_global(self) -> GlobalId { + for timeline in self.known_timelines.iter().filter_map(Weak::upgrade) { + timeline.invalidate(); + } + self.global + } +} + +/// Guard returned by [`DrmSyncobjState::close_device`] to allow temporarily closing the import device. +#[derive(Debug)] +#[must_use = "You need to assign a new import device to not break the drm_syncobj global."] +pub struct CloseGuard<'a> { + state: &'a mut DrmSyncobjState, +} + +impl<'a> CloseGuard<'a> { + /// Sets a new `import_device` to import the syncobj fds and wait on them. + pub fn update_device(self, import_device: DrmDeviceFd) { + self.state.update_device(import_device); + } +} + +impl GlobalDispatch2 for DrmSyncobjGlobalData +where + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init::<_, _>(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +fn commit_hook(_data: &mut D, _dh: &DisplayHandle, surface: &WlSurface) { + compositor::with_states(surface, |states| { + let mut cached = states.cached_state.get::(); + let pending = cached.pending(); + let new_buffer = pending.buffer.as_ref().and_then(|buffer| match buffer { + BufferAssignment::NewBuffer(buffer) => Some(buffer), + _ => None, + }); + if let Some(data) = states + .data_map + .get::>>() + { + if let Some(syncobj_surface) = data.borrow().as_ref() { + let mut cached = states.cached_state.get::(); + let pending = cached.pending(); + if pending.acquire_point.is_some() && new_buffer.is_none() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoBuffer, + "acquire point without buffer".to_string(), + ); + } else if pending.acquire_point.is_some() && pending.release_point.is_none() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoReleasePoint, + "acquire point without release point".to_string(), + ); + } else if pending.acquire_point.is_none() && pending.release_point.is_some() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoAcquirePoint, + "release point without acquire point".to_string(), + ); + } else if let (Some(acquire), Some(release)) = + (pending.acquire_point.as_ref(), pending.release_point.as_ref()) + { + if acquire.timeline == release.timeline && release.point <= acquire.point { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::ConflictingPoints, + format!( + "release point {} is not greater than acquire point {}", + release.point, acquire.point + ), + ); + } + if let Some(buffer) = new_buffer { + if get_dmabuf(buffer).is_err() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::UnsupportedBuffer, + "sync points with non-dmabuf buffer".to_string(), + ); + } + } + } + } + } + }); +} + +fn destruction_hook(_data: &mut D, surface: &WlSurface) { + compositor::with_states(surface, |states| { + let mut cached = states.cached_state.get::(); + if let Some(release_point) = &cached.pending().release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } + if let Some(release_point) = &cached.current().release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } + }); +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: DrmSyncobjHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + resource: &WpLinuxDrmSyncobjManagerV1, + request: wp_linux_drm_syncobj_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_linux_drm_syncobj_manager_v1::Request::GetSurface { id, surface } => { + let already_exists = with_states(&surface, |states| { + states + .data_map + .get::>>() + .map(|v| v.borrow().is_some()) + .unwrap_or(false) + }); + if already_exists { + resource.post_error( + wp_linux_drm_syncobj_manager_v1::Error::SurfaceExists, + "the surface already has a syncobj_surface object associated".to_string(), + ); + return; + } + let commit_hook_id = compositor::add_pre_commit_hook::(&surface, commit_hook); + let destruction_hook_id = + compositor::add_destruction_hook::(&surface, destruction_hook); + let syncobj_surface = data_init.init::<_, _>( + id, + DrmSyncobjSurfaceData { + surface: surface.downgrade(), + commit_hook_id, + destruction_hook_id, + }, + ); + with_states(&surface, |states| { + states + .data_map + .insert_if_missing(|| RefCell::new(Some(syncobj_surface))) + }); + } + wp_linux_drm_syncobj_manager_v1::Request::ImportTimeline { id, fd } => { + if let Some(state) = state.drm_syncobj_state() { + match state.import_device.as_ref().map(|dev| DrmTimeline::new(dev, fd)) { + Some(Ok(timeline)) => { + state.known_timelines.push(Arc::downgrade(&timeline.0)); + data_init.init::<_, _>(id, DrmSyncobjTimelineData { timeline }); + } + Some(Err(err)) => { + resource.post_error( + wp_linux_drm_syncobj_manager_v1::Error::InvalidTimeline, + format!("failed to import syncobj timeline: {err}"), + ); + } + None => { + resource.post_error( + wp_linux_drm_syncobj_manager_v1::Error::InvalidTimeline, + "failed to import syncobj timeline: No device".to_string(), + ); + } + } + } else { + resource.post_error( + wp_linux_drm_syncobj_manager_v1::Error::InvalidTimeline, + "global orphaned", + ) + } + } + wp_linux_drm_syncobj_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +/// Data attached to wp_linux_drm_syncobj_surface_v1 objects +#[derive(Debug)] +pub struct DrmSyncobjSurfaceData { + surface: WlWeak, + commit_hook_id: HookId, + destruction_hook_id: HookId, +} + +impl Dispatch2 for DrmSyncobjSurfaceData +where + D: DrmSyncobjHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + resource: &WpLinuxDrmSyncobjSurfaceV1, + request: wp_linux_drm_syncobj_surface_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_linux_drm_syncobj_surface_v1::Request::Destroy => { + if let Ok(surface) = self.surface.upgrade() { + compositor::remove_pre_commit_hook(&surface, &self.commit_hook_id); + compositor::remove_destruction_hook(&surface, &self.destruction_hook_id); + with_states(&surface, |states| { + *states + .data_map + .get::>>() + .unwrap() + .borrow_mut() = None; + // Committed sync points should still be used, but pending points can + // be cleared. + let mut cached = states.cached_state.get::(); + cached.pending().acquire_point = None; + if let Some(release_point) = cached.pending().release_point.take() { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } + }); + } + } + wp_linux_drm_syncobj_surface_v1::Request::SetAcquirePoint { + timeline, + point_hi, + point_lo, + } => { + let Ok(surface) = self.surface.upgrade() else { + resource.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoSurface, + "Set acquire point for destroyed surface.", + ); + return; + }; + + let sync_point = DrmSyncPoint { + timeline: timeline + .data::() + .unwrap() + .timeline + .clone(), + point: ((point_hi as u64) << 32) + (point_lo as u64), + }; + with_states(&surface, |states| { + let mut cached = states.cached_state.get::(); + let cached_state = cached.pending(); + cached_state.acquire_point = Some(sync_point); + }); + } + wp_linux_drm_syncobj_surface_v1::Request::SetReleasePoint { + timeline, + point_hi, + point_lo, + } => { + let Ok(surface) = self.surface.upgrade() else { + resource.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoSurface, + "Set release point for destroyed surface.", + ); + return; + }; + + let sync_point = DrmSyncPoint { + timeline: timeline + .data::() + .unwrap() + .timeline + .clone(), + point: ((point_hi as u64) << 32) + (point_lo as u64), + }; + with_states(&surface, |states| { + let mut cached = states.cached_state.get::(); + let cached_state = cached.pending(); + cached_state.release_point = Some(sync_point); + }); + } + _ => unreachable!(), + } + } +} + +/// Data attached to wp_linux_drm_syncobj_timeline_v1 objects +#[derive(Debug)] +pub struct DrmSyncobjTimelineData { + timeline: DrmTimeline, +} + +impl Dispatch2 for DrmSyncobjTimelineData { + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &WpLinuxDrmSyncobjTimelineV1, + request: wp_linux_drm_syncobj_timeline_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_linux_drm_syncobj_timeline_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + _resource: &WpLinuxDrmSyncobjTimelineV1, + ) { + if let Some(state) = state.drm_syncobj_state() { + state + .known_timelines + .retain(|t| t.upgrade().is_some_and(|t| !Arc::ptr_eq(&t, &self.timeline.0))) + } + } +} diff --git a/vendor/smithay/src/wayland/drm_syncobj/sync_point.rs b/vendor/smithay/src/wayland/drm_syncobj/sync_point.rs new file mode 100644 index 0000000..52d6cf3 --- /dev/null +++ b/vendor/smithay/src/wayland/drm_syncobj/sync_point.rs @@ -0,0 +1,338 @@ +use calloop::generic::Generic; +use calloop::{EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory}; +use drm::control::Device; +use std::sync::{Mutex, Weak}; +use std::{ + io, + os::unix::io::{AsFd, BorrowedFd, OwnedFd}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + +use crate::backend::drm::{DrmDeviceFd, WeakDrmDeviceFd}; +use crate::backend::renderer::sync::{Fence, Interrupted}; +use crate::wayland::compositor::{Blocker, BlockerState}; + +#[derive(Debug)] +pub(super) struct DrmTimelineInner { + timeline_fd: OwnedFd, + dev_ctx: Mutex, +} + +impl DrmTimelineInner { + pub(super) fn update_device(&self, device: &DrmDeviceFd) -> io::Result<()> { + let mut ctx = self.dev_ctx.lock().unwrap(); + let mut new = DrmTimelineDeviceSpecific::import(self.timeline_fd.as_fd(), device)?; + for (point, eventfd) in ctx + .event_fds + .iter() + .flat_map(|(p, fd)| fd.upgrade().map(|fd| (p, fd))) + { + device.syncobj_eventfd(new.syncobj, *point, eventfd.as_fd(), false)?; + new.event_fds.push((*point, Arc::downgrade(&eventfd))); + } + *ctx = new; + Ok(()) + } + + pub(super) fn invalidate(&self) { + self.dev_ctx.lock().unwrap().invalidate() + } +} + +#[derive(Debug)] +struct DrmTimelineDeviceSpecific { + device: WeakDrmDeviceFd, + syncobj: drm::control::syncobj::Handle, + event_fds: Vec<(u64, Weak)>, +} + +impl DrmTimelineDeviceSpecific { + fn import(fd: BorrowedFd<'_>, device: &DrmDeviceFd) -> io::Result { + let syncobj = device.fd_to_syncobj(fd, false)?; + Ok(DrmTimelineDeviceSpecific { + device: device.downgrade(), + syncobj, + event_fds: Vec::new(), + }) + } + + fn invalidate(&mut self) { + if let Some(device) = self.device.upgrade() { + let _ = device.destroy_syncobj(self.syncobj); + } + self.device = WeakDrmDeviceFd::new(); + // trigger event fds + for eventfd in self.event_fds.drain(..).filter_map(|(_, x)| Weak::upgrade(&x)) { + let _ = rustix::io::write(&eventfd, &[1]); + } + } +} + +/// DRM timeline syncobj +#[derive(Clone, Debug)] +pub struct DrmTimeline(pub(super) Arc); + +impl PartialEq for DrmTimeline { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl DrmTimeline { + /// Import DRM timeline from file descriptor + pub fn new(device: &DrmDeviceFd, fd: OwnedFd) -> io::Result { + let dev_ctx = Mutex::new(DrmTimelineDeviceSpecific::import(fd.as_fd(), device)?); + Ok(Self(Arc::new(DrmTimelineInner { + timeline_fd: fd, + dev_ctx, + }))) + } + + /// Query the last signalled timeline point + pub fn query_signalled_point(&self) -> io::Result { + let ctx = self.0.dev_ctx.lock().unwrap(); + let device = ctx + .device + .upgrade() + .ok_or::(io::ErrorKind::InvalidInput.into())?; + + let mut points = [0]; + device.syncobj_timeline_query(&[ctx.syncobj], &mut points, false)?; + Ok(points[0]) + } +} + +/// Point on a DRM timeline syncobj +#[derive(Clone, Debug)] +pub struct DrmSyncPoint { + pub(super) timeline: DrmTimeline, + pub(super) point: u64, +} + +impl DrmSyncPoint { + /// Borrow the [`DrmTimeline`] this point lives on. + pub fn timeline(&self) -> &DrmTimeline { + &self.timeline + } + + /// Numeric timeline value for this point. + pub fn point(&self) -> u64 { + self.point + } + + /// Create an eventfd that will be signaled by the syncpoint + pub fn eventfd(&self) -> io::Result> { + let fd = rustix::event::eventfd( + 0, + rustix::event::EventfdFlags::CLOEXEC | rustix::event::EventfdFlags::NONBLOCK, + )?; + let mut ctx = self.timeline.0.dev_ctx.lock().unwrap(); + ctx.device + .upgrade() + .ok_or::(io::ErrorKind::InvalidInput.into())? + .syncobj_eventfd(ctx.syncobj, self.point, fd.as_fd(), false)?; + + let fd = Arc::new(fd); + ctx.event_fds.retain(|(_, fd)| fd.upgrade().is_some()); + ctx.event_fds.push((self.point, Arc::downgrade(&fd))); + Ok(fd) + } + + /// Signal the sync point. + pub fn signal(&self) -> io::Result<()> { + let ctx = self.timeline.0.dev_ctx.lock().unwrap(); + ctx.device + .upgrade() + .ok_or::(io::ErrorKind::InvalidInput.into())? + .syncobj_timeline_signal(&[ctx.syncobj], &[self.point]) + } + + /// Wait for sync point. + pub fn wait(&self, timeout_nsec: i64) -> io::Result<()> { + let ctx = self.timeline.0.dev_ctx.lock().unwrap(); + ctx.device + .upgrade() + .ok_or::(io::ErrorKind::InvalidInput.into())? + .syncobj_timeline_wait(&[ctx.syncobj], &[self.point], timeout_nsec, false, false, false)?; + Ok(()) + } + + /// Export DRM sync file for sync point. + pub fn export_sync_file(&self) -> io::Result { + let ctx = self.timeline.0.dev_ctx.lock().unwrap(); + let Some(device) = ctx.device.upgrade() else { + return Err(io::ErrorKind::InvalidInput.into()); + }; + + let syncobj = device.create_syncobj(false)?; + if let Err(err) = device.syncobj_timeline_transfer(ctx.syncobj, syncobj, self.point, 0) { + let _ = device.destroy_syncobj(syncobj); + return Err(err); + }; + + let res = device.syncobj_to_fd(syncobj, true); + let _ = device.destroy_syncobj(syncobj); + res + } + + /// Import a DRM sync file fd as the materialized fence at this + /// timeline point. Symmetric counterpart of + /// [`DrmSyncPoint::export_sync_file`]. + /// + /// Internally creates a fresh binary syncobj, imports the sync + /// file fence into it via the `IMPORT_SYNC_FILE` ioctl (raw + /// because the `drm` crate's public wrapper hardcodes the + /// destination handle to `0`, which the kernel rejects with + /// `ENOENT` — only the two-step `drmSyncobjCreate` + + /// `drmSyncobjImportSyncFile(existing_handle, fd)` pattern is + /// supported, mirroring libdrm). Then transfers the temp's + /// point 0 into this timeline at `self.point` and destroys the + /// temp. + /// + /// Mirrors `wlr_drm_syncobj_timeline_import_sync_file`. Used by + /// compositors that drive Vulkan explicit sync via + /// `VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT` and need to + /// inject the resulting sync-file fence into the client's + /// release point. + pub fn import_sync_file(&self, fd: BorrowedFd<'_>) -> io::Result<()> { + use rustix::ioctl::{Updater, ioctl, opcode::read_write}; + use std::os::fd::AsRawFd; + + let ctx = self.timeline.0.dev_ctx.lock().unwrap(); + let Some(device) = ctx.device.upgrade() else { + return Err(io::ErrorKind::InvalidInput.into()); + }; + + let tmp = device.create_syncobj(false)?; + + const DRM_IOCTL_SYNCOBJ_FD_TO_HANDLE: rustix::ioctl::Opcode = + read_write::(drm_ffi::DRM_IOCTL_BASE, 0xC2); + + let mut args = drm_ffi::drm_syncobj_handle { + handle: tmp.into(), + flags: drm_ffi::DRM_SYNCOBJ_FD_TO_HANDLE_FLAGS_IMPORT_SYNC_FILE, + fd: fd.as_raw_fd(), + pad: 0, + point: 0, + }; + // SAFETY: `device.as_fd()` is a valid DRM device fd; + // `drm_ffi::drm_syncobj_handle` is the type expected by the + // DRM_IOCTL_SYNCOBJ_FD_TO_HANDLE ioctl. + let res = unsafe { + ioctl( + device.as_fd(), + Updater::::new(&mut args), + ) + }; + if let Err(err) = res { + let _ = device.destroy_syncobj(tmp); + return Err(err.into()); + } + + let res = device.syncobj_timeline_transfer(tmp, ctx.syncobj, 0, self.point); + let _ = device.destroy_syncobj(tmp); + res + } + + /// Create an [`calloop::EventSource`] and [`Blocker`] for this sync point. + /// + /// This will fail if `drmSyncobjEventfd` isn't supported by the device. See + /// [`supports_syncobj_eventfd`](super::supports_syncobj_eventfd). + pub fn generate_blocker(&self) -> io::Result<(DrmSyncPointBlocker, DrmSyncPointSource)> { + let fd = self.eventfd()?; + let signal = Arc::new(AtomicBool::new(false)); + let blocker = DrmSyncPointBlocker { + signal: signal.clone(), + }; + let source = DrmSyncPointSource { + source: Generic::new(fd, Interest::READ, Mode::Level), + signal, + }; + Ok((blocker, source)) + } +} + +impl Fence for DrmSyncPoint { + fn is_signaled(&self) -> bool { + self.timeline + .query_signalled_point() + .ok() + .is_some_and(|point| point >= self.point) + } + + fn wait(&self) -> Result<(), Interrupted> { + self.wait(i64::MAX).map_err(|_| Interrupted) + } + + fn is_exportable(&self) -> bool { + true + } + + fn export(&self) -> Option { + self.export_sync_file().ok() + } +} + +/// Event source generating an event when a [`DrmSyncPoint`] is signalled.. +#[derive(Debug)] +pub struct DrmSyncPointSource { + source: Generic>, + signal: Arc, +} + +impl EventSource for DrmSyncPointSource { + type Event = (); + type Metadata = (); + type Ret = Result<(), std::io::Error>; + type Error = io::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: C, + ) -> Result + where + C: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + self.signal.store(true, Ordering::SeqCst); + self.source + .process_events(readiness, token, |_, _| Ok(PostAction::Remove))?; + callback((), &mut ())?; + Ok(PostAction::Remove) + } + + fn register(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.source.register(poll, token_factory)?; + Ok(()) + } + + fn reregister(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.source.reregister(poll, token_factory)?; + Ok(()) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.source.unregister(poll)?; + Ok(()) + } +} + +/// [`Blocker`] implementation for an accompaning [`DrmSyncPointSource`] +#[derive(Debug)] +pub struct DrmSyncPointBlocker { + signal: Arc, +} + +impl Blocker for DrmSyncPointBlocker { + fn state(&self) -> BlockerState { + if self.signal.load(Ordering::SeqCst) { + BlockerState::Released + } else { + BlockerState::Pending + } + } +} diff --git a/vendor/smithay/src/wayland/fifo/mod.rs b/vendor/smithay/src/wayland/fifo/mod.rs new file mode 100644 index 0000000..9e6b7ef --- /dev/null +++ b/vendor/smithay/src/wayland/fifo/mod.rs @@ -0,0 +1,396 @@ +//! Utilities for handling the `wp-fifo` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation create the [`FifoManagerState`] and store it inside your `State` struct. +//! +//! ``` +//! use smithay::wayland::compositor; +//! use smithay::wayland::fifo::FifoManagerState; +//! +//! # struct State { fifo_manager_state: FifoManagerState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the compositor state +//! let fifo_manager_state = FifoManagerState::new::( +//! &display.handle(), +//! ); +//! +//! // insert the FifoManagerState into your state +//! // .. +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Use the fifo state +//! +//! Whenever the client commits a surface content update requesting to wait on a previously set barrier +//! through [`wp_fifo_v1::Request::WaitBarrier`] the implementation will place a [`Blocker`](crate::wayland::compositor::Blocker) +//! on the surface. +//! A surface content update requesting to set a barrier through [`wp_fifo_v1::Request::SetBarrier`] will update the barrier +//! a later commit can use to wait on. +//! +//! It is your responsibility to query for a set barrier on a surface and signal it to allow clients to make forward progress. +//! +//! You can query the current barrier and signal it as shown in the following example: +//! +//! ```no_run +//! # use wayland_server::{backend::ObjectId, protocol::wl_surface, Resource}; +//! use smithay::wayland::compositor; +//! use smithay::wayland::fifo::FifoBarrierCachedState; +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let dh = display.handle(); +//! # let surface = wl_surface::WlSurface::from_id(&dh, ObjectId::null()).unwrap(); +//! compositor::with_states(&surface, |states| { +//! let fifo_barrier = states +//! .cached_state +//! .get::() +//! .current() +//! .barrier +//! .take(); +//! +//! if let Some(fifo_barrier) = fifo_barrier { +//! fifo_barrier.signal(); +//! // ..signal blocker cleared +//! } +//! }); +//! ``` +//! +//! ### Unmanaged mode +//! +//! If for some reason the integrated solution for fifo does not suit your needs +//! you can create an unmanaged version with [`FifoManagerState::unmanaged`]. +//! In this case it is your responsibility to listen for surface commits and place blockers +//! accordingly to the spec. +//! +//! The double buffered fifo state can be retrieved like shown in the following example: +//! +//! ```no_run +//! # let surface: wayland_server::protocol::wl_surface::WlSurface = todo!(); +//! use smithay::wayland::compositor; +//! use smithay::wayland::fifo::FifoCachedState; +//! +//! let fifo_state = compositor::with_states(&surface, |states| { +//! *states.cached_state.get::().pending() +//! }); +//! ``` +use std::cell::RefCell; + +use wayland_protocols::wp::fifo::v1::server::{ + wp_fifo_manager_v1::{self, WpFifoManagerV1}, + wp_fifo_v1::{self, WpFifoV1}, +}; +use wayland_server::{ + DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, backend::GlobalId, + protocol::wl_surface::WlSurface, +}; + +use crate::wayland::{ + Dispatch2, GlobalDispatch2, + compositor::{add_blocker, add_pre_commit_hook}, +}; + +use super::compositor::{Barrier, Cacheable, is_sync_subsurface, with_states}; + +/// State for the [`WpFifoManagerV1`] global +#[derive(Debug)] +pub struct FifoManagerState { + global: GlobalId, + is_managed: bool, +} + +impl FifoManagerState { + /// Create a new [`WpFifoManagerV1`] global + // + /// The id provided by [`FifoManagerState::global`] may be used to + /// remove or disable this global in the future. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: 'static, + { + Self::new_internal::(display, true) + } + + /// Create a new unmanaged [`WpFifoManagerV1`] global + // + /// The id provided by [`FifoManagerState::global`] may be used to + /// remove or disable this global in the future. + pub fn unmanaged(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: 'static, + { + Self::new_internal::(display, false) + } + + fn new_internal(display: &DisplayHandle, is_managed: bool) -> Self + where + D: GlobalDispatch, + D: 'static, + { + let global = display.create_global::(1, FifoManagerData { is_managed }); + + Self { global, is_managed } + } + + /// Returns the id of the [`WpFifoManagerV1`] global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } + + /// Returns if this [`FifoManagerState`] operates in managed mode. + pub fn is_managed(&self) -> bool { + self.is_managed + } +} + +#[doc(hidden)] +#[derive(Debug, Clone, Copy)] +pub struct FifoManagerData { + is_managed: bool, +} + +impl GlobalDispatch2 for FifoManagerData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, *self); + } +} + +impl Dispatch2 for FifoManagerData +where + D: Dispatch, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &WpFifoManagerV1, + request: wp_fifo_manager_v1::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_fifo_manager_v1::Request::GetFifo { id, surface } => { + let (is_initial, has_active_fifo) = with_states(&surface, |states| { + let marker = states.data_map.get::>(); + ( + marker.is_none(), + marker.map(|m| m.borrow().0.is_some()).unwrap_or(false), + ) + }); + + // The protocol mandates that only a single fifo object is associated with a surface at all times + if has_active_fifo { + surface.post_error( + wp_fifo_manager_v1::Error::AlreadyExists, + "the surface has already a fifo object associated", + ); + return; + } + + // Make sure we do not install the hook more then once in case the surface is being reused + if self.is_managed && is_initial { + add_pre_commit_hook::(&surface, |_, _, surface| { + let fifo_barrier = with_states(surface, |states| { + let fifo_state = *states.cached_state.get::().pending(); + + // The pending state will contain any previously set barrier on this surface + // In case this commit updates the barrier with `set_barrier`, but also mandates to + // wait for a previously set barrier it is important to first retrieve the previously + // set barrier to not overwrite it with our own. + let fifo_barrier = fifo_state + .wait_barrier + .then(|| { + states + .cached_state + .get::() + .pending() + .barrier + .take() + }) + .flatten(); + + // If requested set the barrier for this commit. + // The barrier will be available for the next commit requesting to wait on it + // in the pending state. + // The barrier will also be either put in the current state in case this commit + // is not blocked or into a transaction otherwise eventually ending in the current + // state when it is unblocked. + if fifo_state.set_barrier { + states + .cached_state + .get::() + .pending() + .barrier = Some(Barrier::new(false)); + } + + fifo_barrier + }); + + if let Some(barrier) = fifo_barrier { + // If multiple consecutive commits only call wait_barrier, but not set_barrier + // we might end up with the same barrier in multiple commits. It could happen + // that the barrier is already signaled in which case there is no need to + // further delay this commit + // + // In addition the spec also defines that the constraint must be ignored for + // sync subsurfaces + let skip = barrier.is_signaled() || is_sync_subsurface(surface); + if !skip { + add_blocker(surface, barrier); + } + } + }); + } + + let fifo: WpFifoV1 = data_init.init(id, FifoData(surface.downgrade())); + + with_states(&surface, |states| { + states + .data_map + .get_or_insert(|| RefCell::new(FifoMarker(None))) + .borrow_mut() + .0 = Some(fifo); + }); + } + wp_fifo_manager_v1::Request::Destroy => (), + _ => unreachable!(), + } + } +} + +// Internal marker to track if a fifo object is currently associated with a surface. +// Used to realize the `AlreadyExists` protocol check. +struct FifoMarker(Option); + +#[doc(hidden)] +#[derive(Debug)] +pub struct FifoData(Weak); + +impl Dispatch2 for FifoData +where + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + resource: &WpFifoV1, + request: wp_fifo_v1::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_fifo_v1::Request::SetBarrier => { + let Ok(surface) = self.0.upgrade() else { + resource.post_error( + wp_fifo_v1::Error::SurfaceDestroyed, + "the surface associated with this fifo object has been destroyed".to_string(), + ); + return; + }; + with_states(&surface, move |states| { + states.cached_state.get::().pending().set_barrier = true; + }); + } + wp_fifo_v1::Request::WaitBarrier => { + let Ok(surface) = self.0.upgrade() else { + resource.post_error( + wp_fifo_v1::Error::SurfaceDestroyed, + "the surface associated with this fifo object has been destroyed".to_string(), + ); + return; + }; + with_states(&surface, move |states| { + states + .cached_state + .get::() + .pending() + .wait_barrier = true; + }); + } + wp_fifo_v1::Request::Destroy => { + if let Ok(surface) = self.0.upgrade() { + with_states(&surface, |states| { + states + .data_map + .get::>() + .unwrap() + .borrow_mut() + .0 = None; + }); + } + } + _ => unreachable!(), + } + } +} + +/// State for the [`WpFifoV1`] object +#[derive(Debug, Default, Copy, Clone)] +pub struct FifoCachedState { + /// The content update requested a barrier to be set + pub set_barrier: bool, + + /// The content update requested to wait on a previously + /// set barrier + pub wait_barrier: bool, +} + +impl Cacheable for FifoCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + std::mem::take(self) + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +/// Fifo barrier per surface state +#[derive(Debug, Default)] +pub struct FifoBarrierCachedState { + /// The barrier set for the current content update + pub barrier: Option, +} + +impl Cacheable for FifoBarrierCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + Self { + barrier: self.barrier.clone(), + } + } + + fn merge_into(mut self, into: &mut Self, _dh: &DisplayHandle) { + let Some(barrier) = self.barrier.take() else { + return; + }; + + if into.barrier.as_ref() == Some(&barrier) || barrier.is_signaled() { + return; + } + + if let Some(barrier) = into.barrier.replace(barrier) { + barrier.signal(); + } + } +} diff --git a/vendor/smithay/src/wayland/fixes.rs b/vendor/smithay/src/wayland/fixes.rs new file mode 100644 index 0000000..c1e7b81 --- /dev/null +++ b/vendor/smithay/src/wayland/fixes.rs @@ -0,0 +1,75 @@ +//! `wl_fixes` protocol + +use tracing::error; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::GlobalId, + protocol::wl_fixes, +}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +/// Delegate type for handling wl fixes requests. +#[derive(Debug, Clone)] +pub struct FixesState { + global: GlobalId, +} + +impl FixesState { + /// Creates a new delegate type for handling [`wl_fixes::WlFixes`] events. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: 'static, + { + let global = display.create_global::(1, GlobalData); + Self { global } + } + + /// Returns the wl fixes global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &wl_fixes::WlFixes, + request: wl_fixes::Request, + dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_fixes::Request::DestroyRegistry { registry } => { + if let Err(err) = dh.backend_handle().destroy_object::(®istry.id()) { + error!(?err, "failed to destroy registry"); + } + } + wl_fixes::Request::Destroy => {} + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/foreign_toplevel_list/mod.rs b/vendor/smithay/src/wayland/foreign_toplevel_list/mod.rs new file mode 100644 index 0000000..90d1739 --- /dev/null +++ b/vendor/smithay/src/wayland/foreign_toplevel_list/mod.rs @@ -0,0 +1,542 @@ +//! Foreign toplevel list +//! +//! The purpose of this protocol is to provide protocol object handles for toplevels. +//! +//! ```no_run +//! use smithay::wayland::foreign_toplevel_list::{ForeignToplevelListState, ForeignToplevelListHandler}; +//! +//! pub struct State { +//! foreign_toplevel_list: ForeignToplevelListState, +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! impl ForeignToplevelListHandler for State { +//! fn foreign_toplevel_list_state(&mut self) -> &mut ForeignToplevelListState { +//! &mut self.foreign_toplevel_list +//! } +//! } +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! let mut state = State { +//! foreign_toplevel_list: ForeignToplevelListState::new::(&display_handle), +//! }; +//! +//! let handle = state.foreign_toplevel_list.new_toplevel::("Window Title", "com.example"); +//! +//! // Handle can be used to update title and app_id +//! handle.send_title("Window title has changed"); +//! handle.send_done(); +//! +//! // Handle can also be used to close the window, after this call the handle will become inert, +//! // and the handle will no longer be announced to clients +//! handle.send_closed(); +//! ``` + +use std::sync::{Arc, Mutex}; + +use rand::distr::{Alphanumeric, SampleString}; +use wayland_protocols::ext::foreign_toplevel_list::v1::server::{ + ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, + ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, + backend::{ClientId, GlobalId}, +}; + +use crate::{ + utils::user_data::UserDataMap, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +/// Handler for foreign toplevel list protocol +pub trait ForeignToplevelListHandler: 'static { + /// [ForeignToplevelListState] getter + fn foreign_toplevel_list_state(&mut self) -> &mut ForeignToplevelListState; +} + +#[derive(Debug)] +struct ForeignToplevelHandleInner { + identifier: String, + title: String, + app_id: String, + // Each ExtForeignToplevelHandleV1 contains the handle in it's user data, + // so this ref has to be weak + instances: Vec>, + closed: bool, +} + +impl ForeignToplevelHandleInner { + /// The toplevel has been closed + fn send_closed(&mut self) { + if self.closed { + return; + } + + self.closed = true; + // drain to prevent any events from being sent to closed handles + for toplevel in self.instances.drain(..) { + if let Ok(toplevel) = toplevel.upgrade() { + toplevel.closed(); + } + } + } +} + +impl Drop for ForeignToplevelHandleInner { + fn drop(&mut self) { + self.send_closed() + } +} + +/// Weak version of [ForeignToplevelHandle] +#[derive(Debug, Clone)] +pub struct ForeignToplevelWeakHandle { + inner: std::sync::Weak<(Mutex, UserDataMap)>, +} + +impl ForeignToplevelWeakHandle { + /// Upgrade weak [ForeignToplevelWeakHandle] to strong [ForeignToplevelHandle] + pub fn upgrade(&self) -> Option { + Some(ForeignToplevelHandle { + inner: self.inner.upgrade()?, + }) + } +} + +/// Handle of a toplevel, used to update title or app_id, after initial handle creation +/// +/// eg. +/// ```no_run +/// use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle; +/// let handle: ForeignToplevelHandle = todo!(); +/// +/// // Handle can be used to update title and app_id +/// handle.send_title("abc"); +/// handle.send_app_id("com.example"); +/// handle.send_done(); +/// +/// // Handle can also be used to close the window, after this call the handle will become inert, +/// // and the handle will no longer be announced to clients +/// handle.send_closed(); +/// ``` +#[derive(Debug, Clone)] +pub struct ForeignToplevelHandle { + inner: Arc<(Mutex, UserDataMap)>, +} + +impl ForeignToplevelHandle { + fn new( + title: String, + app_id: String, + identifier: String, + instances: Vec>, + ) -> Self { + Self { + inner: Arc::new(( + Mutex::new(ForeignToplevelHandleInner { + identifier, + title, + app_id, + instances, + closed: false, + }), + UserDataMap::new(), + )), + } + } + + /// Downgrade strong [ForeignToplevelHandle] to weak [ForeignToplevelWeakHandle] + pub fn downgrade(&self) -> ForeignToplevelWeakHandle { + ForeignToplevelWeakHandle { + inner: Arc::downgrade(&self.inner), + } + } + + /// Attempt to retrieve [ForeignToplevelHandle] from an existing resource + pub fn from_resource(resource: &ExtForeignToplevelHandleV1) -> Option { + resource.data::().cloned() + } + + /// Retrieve [`ExtForeignToplevelHandleV1`] + /// instances for this handle. + pub fn resources(&self) -> Vec { + let inner = self.inner.0.lock().unwrap(); + inner + .instances + .iter() + .filter_map(|weak| weak.upgrade().ok()) + .collect() + } + + /// Retrieve [`ExtForeignToplevelHandleV1`] + /// instances for this handle of a given [`Client`]. + pub fn resources_for_client(&self, client: &Client) -> Vec { + self.resources() + .into_iter() + .filter(|handle| handle.client().as_ref().is_some_and(|c| c == client)) + .collect() + } + + /// Access the [UserDataMap] associated with this [ForeignToplevelHandle] + pub fn user_data(&self) -> &UserDataMap { + &self.inner.1 + } + + /// The title of the toplevel has changed. + /// + /// [Self::send_done] has to be called to finalize the update + pub fn send_title(&self, title: &str) { + let mut inner = self.inner.0.lock().unwrap(); + if inner.title == title { + return; + } + + inner.title = title.to_string(); + + for toplevel in inner.instances.iter() { + if let Ok(toplevel) = toplevel.upgrade() { + toplevel.title(title.to_string()); + } + } + } + + /// The app_id of the toplevel has changed. + /// + /// [Self::send_done] has to be called to finalize the update + pub fn send_app_id(&self, app_id: &str) { + let mut inner = self.inner.0.lock().unwrap(); + if inner.app_id == app_id { + return; + } + + inner.app_id = app_id.to_string(); + + for toplevel in inner.instances.iter() { + if let Ok(toplevel) = toplevel.upgrade() { + toplevel.app_id(app_id.to_string()); + } + } + } + + /// This event is should be sent after all changes in the toplevel state have been sent. + pub fn send_done(&self) { + let inner = self.inner.0.lock().unwrap(); + for toplevel in inner.instances.iter() { + if let Ok(toplevel) = toplevel.upgrade() { + toplevel.done(); + } + } + } + + /// The toplevel has been closed + pub fn send_closed(&self) { + self.inner.0.lock().unwrap().send_closed(); + } + + /// A stable identifier for a toplevel + pub fn identifier(&self) -> String { + self.inner.0.lock().unwrap().identifier.clone() + } + + /// The title of the toplevel + pub fn title(&self) -> String { + self.inner.0.lock().unwrap().title.clone() + } + + /// The app id of the toplevel + pub fn app_id(&self) -> String { + self.inner.0.lock().unwrap().app_id.clone() + } + + /// The toplevel has been closed + pub fn is_closed(&self) -> bool { + self.inner.0.lock().unwrap().closed + } + + /// Check if two toplevel handles belong to the same toplevel. + pub fn matches(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + || self.inner.0.lock().unwrap().identifier == other.inner.0.lock().unwrap().identifier + } + + fn init_new_instance(&self, toplevel: ExtForeignToplevelHandleV1) { + debug_assert!( + !self.is_closed(), + "No handles should ever be created for closed toplevel" + ); + + toplevel.identifier(self.identifier()); + toplevel.title(self.title()); + toplevel.app_id(self.app_id()); + toplevel.done(); + + self.inner.0.lock().unwrap().instances.push(toplevel.downgrade()); + } + + fn remove_instance(&self, instance: &ExtForeignToplevelHandleV1) { + let mut inner = self.inner.0.lock().unwrap(); + if let Some(pos) = inner.instances.iter().position(|i| i == instance) { + inner.instances.remove(pos); + } + } +} + +/// State of the [ExtForeignToplevelListV1] global +#[derive(Debug)] +pub struct ForeignToplevelListState { + global: GlobalId, + toplevels: Vec, + list_instances: Vec, + dh: DisplayHandle, +} + +impl ForeignToplevelListState { + /// Register new [ExtForeignToplevelListV1] global + pub fn new(dh: &DisplayHandle) -> Self + where + D: ForeignToplevelListHandler + + GlobalDispatch, + { + Self::new_with_filter::(dh, |_| true) + } + + /// Register new [ExtForeignToplevelListV1] global with filter + pub fn new_with_filter( + dh: &DisplayHandle, + can_view: impl Fn(&Client) -> bool + Send + Sync + 'static, + ) -> Self + where + D: ForeignToplevelListHandler + + GlobalDispatch, + { + let global = dh.create_global::( + 1, + ForeignToplevelListGlobalData { + filter: Box::new(can_view), + }, + ); + + Self { + global, + toplevels: Vec::new(), + list_instances: Vec::new(), + dh: dh.clone(), + } + } + + /// [ExtForeignToplevelListV1] GlobalId getter + pub fn global(&self) -> GlobalId { + self.global.clone() + } + + /// This event is emitted whenever a new toplevel window is created. + /// It is emitted for all toplevels, regardless of the app that has created them. + pub fn new_toplevel( + &mut self, + title: impl Into, + app_id: impl Into, + ) -> ForeignToplevelHandle + where + D: ForeignToplevelListHandler + Dispatch, + { + self.new_toplevel_with_identifier::( + title, + app_id, + Alphanumeric.sample_string(&mut rand::rng(), 32), + ) + } + + /// Create a new [`ForeignToplevelHandle`] using an arbitrary identifier. + /// + /// This function is similar to [`new_toplevel`], but lets the compositor specify the toplevel + /// identifier. The identifier should be a non-empty string composed of up to 32 printable ascii + /// characters. + /// + /// [`new_toplevel`]: ForeignToplevelListState::new_toplevel + pub fn new_toplevel_with_identifier( + &mut self, + title: impl Into, + app_id: impl Into, + identifier: impl Into, + ) -> ForeignToplevelHandle + where + D: ForeignToplevelListHandler + Dispatch, + { + let identifier = identifier.into(); + assert!( + !identifier.is_empty() && identifier.len() <= 32 && identifier.is_ascii(), + "{} is not a valid identifier", + identifier + ); + + let handle = ForeignToplevelHandle::new( + title.into(), + app_id.into(), + identifier, + Vec::with_capacity(self.list_instances.len()), + ); + + for instance in &self.list_instances { + let Ok(client) = self.dh.get_client(instance.id()) else { + continue; + }; + + let Ok(toplevel) = client.create_resource::( + &self.dh, + instance.version(), + handle.clone(), + ) else { + continue; + }; + + instance.toplevel(&toplevel); + handle.init_new_instance(toplevel); + } + + self.toplevels.push(handle.downgrade()); + + handle + } + + /// Remove the toplevel, and send closed event if needed + /// + /// Alternatively, you can just call [ForeignToplevelHandle::send_closed] and the handle will be + /// lazely cleaned up, either by [Self::cleanup_closed_handles], or during next global bind + pub fn remove_toplevel(&mut self, handle: &ForeignToplevelHandle) { + handle.send_closed(); + + if let Some(pos) = self + .toplevels + .iter() + .filter_map(|h| h.upgrade()) + .position(|h| Arc::ptr_eq(&h.inner, &handle.inner)) + { + self.toplevels.remove(pos); + } + } + + /// Auto cleanup closed handles + /// + /// This is not needed if you already manually remove each handle with [Self::remove_toplevel] + pub fn cleanup_closed_handles(&mut self) { + self.toplevels.retain(|handle| { + let Some(handle) = handle.upgrade() else { + return false; + }; + !handle.is_closed() + }); + } +} + +/// Glabal data of [ExtForeignToplevelListV1] +pub struct ForeignToplevelListGlobalData { + filter: Box bool + Send + Sync>, +} + +impl std::fmt::Debug for ForeignToplevelListGlobalData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ForeignToplevelListGlobalData") + .finish_non_exhaustive() + } +} + +impl GlobalDispatch2 + for ForeignToplevelListGlobalData +where + D: Dispatch + + Dispatch, +{ + fn bind( + &self, + state: &mut D, + dh: &DisplayHandle, + client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init(resource, GlobalData); + + let state = state.foreign_toplevel_list_state(); + + state.toplevels.retain(|handle| { + let Some(handle) = handle.upgrade() else { + // Cleanup dead handles + return false; + }; + + if handle.is_closed() { + // Cleanup closed handles + return false; + } + + if let Ok(toplevel) = client.create_resource::( + dh, + instance.version(), + handle.clone(), + ) { + instance.toplevel(&toplevel); + handle.init_new_instance(toplevel); + } + + true + }); + + state.list_instances.push(instance); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData { + fn request( + &self, + state: &mut D, + client: &wayland_server::Client, + manager: &ExtForeignToplevelListV1, + request: ext_foreign_toplevel_list_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + ext_foreign_toplevel_list_v1::Request::Stop => { + self.destroyed(state, client.id(), manager); + manager.finished(); + } + ext_foreign_toplevel_list_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, resource: &ExtForeignToplevelListV1) { + state + .foreign_toplevel_list_state() + .list_instances + .retain(|i| i != resource); + } +} + +impl Dispatch2 for ForeignToplevelHandle { + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _context: &ExtForeignToplevelHandleV1, + request: ext_foreign_toplevel_handle_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + ext_foreign_toplevel_handle_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: ClientId, resource: &ExtForeignToplevelHandleV1) { + self.remove_instance(resource); + } +} diff --git a/vendor/smithay/src/wayland/fractional_scale/mod.rs b/vendor/smithay/src/wayland/fractional_scale/mod.rs new file mode 100644 index 0000000..3429bd4 --- /dev/null +++ b/vendor/smithay/src/wayland/fractional_scale/mod.rs @@ -0,0 +1,263 @@ +//! Utilities for handling the `wp_fractional_scale` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation create the [`FractionalScaleManagerState`], store it inside your `State` struct +//! and implement the [`FractionalScaleHandler`], as shown in this example: +//! +//! ``` +//! use smithay::wayland::compositor; +//! use smithay::reexports::wayland_server::protocol::wl_surface; +//! use smithay::wayland::fractional_scale::{ +//! self, +//! FractionalScaleManagerState, +//! FractionalScaleHandler, +//! }; +//! +//! # struct State { fractional_scale_manager_state: FractionalScaleManagerState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the compositor state +//! let fractional_scale_manager_state = FractionalScaleManagerState::new::( +//! &display.handle(), +//! ); +//! +//! // insert the FractionalScaleManagerState into your state +//! // .. +//! +//! // implement the necessary traits +//! impl FractionalScaleHandler for State { +//! fn new_fractional_scale(&mut self, surface: wl_surface::WlSurface) { +//! // If you set preferred fractional scale for this surface before, +//! // then you don't need to do anything here. +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Use the fractional scale state +//! +//! Whenever the fractional scale for a surface changes set the preferred +//! fractional scale like shown in the example: +//! +//! ```no_run +//! # use wayland_server::{backend::ObjectId, protocol::wl_surface, Resource}; +//! use smithay::wayland::compositor; +//! use smithay::wayland::fractional_scale; +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let dh = display.handle(); +//! # let surface = wl_surface::WlSurface::from_id(&dh, ObjectId::null()).unwrap(); +//! compositor::with_states(&surface, |states| { +//! fractional_scale::with_fractional_scale(states, |fractional_scale| { +//! // set the preferred scale for the surface +//! fractional_scale.set_preferred_scale(1.25); +//! }); +//! }) +//! ``` + +use std::cell::RefCell; + +use wayland_protocols::wp::fractional_scale::v1::server::{ + wp_fractional_scale_manager_v1, wp_fractional_scale_v1, +}; +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, Resource, Weak, backend::GlobalId, protocol::wl_surface, +}; + +use super::compositor::{SurfaceData, with_states}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +/// State of the wp_fractional_scale_manager_v1 Global +#[derive(Debug)] +pub struct FractionalScaleManagerState { + global: GlobalId, +} + +impl FractionalScaleManagerState { + /// Create new [`wp_fraction_scale_manager`](wayland_protocols::wp::fractional_scale::v1::server::wp_fractional_scale_manager_v1) global. + pub fn new(display: &DisplayHandle) -> FractionalScaleManagerState + where + D: GlobalDispatch + + Dispatch + + Dispatch + + 'static, + D: FractionalScaleHandler, + { + FractionalScaleManagerState { + global: display + .create_global::( + 1, GlobalData, + ), + } + } + + /// Returns the fractional scale manager global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch + + Dispatch, + D: FractionalScaleHandler, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: FractionalScaleHandler, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + _resource: &wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wp_fractional_scale_manager_v1::Request::Destroy => { + // All is already handled by our destructor + } + wp_fractional_scale_manager_v1::Request::GetFractionalScale { id, surface } => { + let already_has_fractional_scale = with_states(&surface, |states| { + states + .data_map + .get::() + .map(|v| v.borrow().fractional_scale.is_some()) + .unwrap_or(false) + }); + + if already_has_fractional_scale { + surface.post_error( + wp_fractional_scale_manager_v1::Error::FractionalScaleExists, + "the surface already has a fractional_scale object associated".to_string(), + ); + return; + } + + let fractional_scale: wp_fractional_scale_v1::WpFractionalScaleV1 = + data_init.init(id, FractionalScaleData(surface.downgrade())); + + with_states(&surface, move |states| { + with_fractional_scale(states, move |data| { + // Send the scale that the user may have pre-filled for this surface. + if let Some(scale) = data.preferred_scale { + fractional_scale.preferred_scale(f64::round(scale * 120.0) as u32); + } + data.fractional_scale = Some(fractional_scale); + }); + }); + state.new_fractional_scale(surface); + } + _ => unreachable!(), + } + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct FractionalScaleData(Weak); + +impl Dispatch2 for FractionalScaleData +where + D: FractionalScaleHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &wp_fractional_scale_v1::WpFractionalScaleV1, + request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wp_fractional_scale_v1::Request::Destroy => { + if let Ok(surface) = self.0.upgrade() { + with_states(&surface, |states| { + states + .data_map + .get::() + .and_then(|v| v.borrow_mut().fractional_scale.take()); + }) + } + } + _ => unreachable!(), + } + } +} + +/// Fractional scale handler type +pub trait FractionalScaleHandler { + /// A new fractional scale was instantiated + fn new_fractional_scale(&mut self, _surface: wl_surface::WlSurface) {} +} + +/// Type stored in WlSurface states data_map +/// +/// ```rs +/// compositor::with_states(surface, |states| { +/// let data = states.data_map.get::(); +/// }); +/// ``` +pub type FractionalScaleStateUserData = RefCell; + +/// State for the fractional scale of a surface +#[derive(Debug, Default)] +pub struct FractionalScaleState { + /// The fractional scale object, if one exists for this surface. + fractional_scale: Option, + /// Preferred fractional scale for this surface. + preferred_scale: Option, +} + +impl FractionalScaleState { + /// Set the preferred scale + pub fn set_preferred_scale(&mut self, scale: f64) { + if self.preferred_scale != Some(scale) { + if let Some(obj) = &self.fractional_scale { + obj.preferred_scale(f64::round(scale * 120.0) as u32); + } + self.preferred_scale = Some(scale); + } + } + + /// Returns the current preferred scale + pub fn preferred_scale(&self) -> Option { + self.preferred_scale + } +} + +/// Run a closure on the [`FractionalScaleState`] of a [`WlSurface`](wl_surface::WlSurface) +pub fn with_fractional_scale(states: &SurfaceData, f: F) -> T +where + F: FnOnce(&mut FractionalScaleState) -> T, +{ + let mut fractional_scale = states + .data_map + .get_or_insert(FractionalScaleStateUserData::default) + .borrow_mut(); + + f(&mut fractional_scale) +} diff --git a/vendor/smithay/src/wayland/idle_inhibit/inhibitor.rs b/vendor/smithay/src/wayland/idle_inhibit/inhibitor.rs new file mode 100644 index 0000000..9b5705a --- /dev/null +++ b/vendor/smithay/src/wayland/idle_inhibit/inhibitor.rs @@ -0,0 +1,43 @@ +//! idle-inhibit inhibitor. + +use _idle_inhibit::zwp_idle_inhibitor_v1::{Request, ZwpIdleInhibitorV1}; +use wayland_protocols::wp::idle_inhibit::zv1::server as _idle_inhibit; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::{Client, DataInit, DisplayHandle}; + +use crate::wayland::Dispatch2; +use crate::wayland::idle_inhibit::IdleInhibitHandler; + +/// State of zwp_idle_inhibitor_v1. +#[derive(Debug)] +pub struct IdleInhibitorState { + surface: WlSurface, +} + +impl IdleInhibitorState { + /// Create `zwp_idle_inhibitor_v1` state. + pub fn new(surface: WlSurface) -> Self { + Self { surface } + } +} + +impl Dispatch2 for IdleInhibitorState +where + D: IdleInhibitHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _inhibitor: &ZwpIdleInhibitorV1, + request: Request, + _display: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + Request::Destroy => state.uninhibit(self.surface.clone()), + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/idle_inhibit/mod.rs b/vendor/smithay/src/wayland/idle_inhibit/mod.rs new file mode 100644 index 0000000..6ea1cb0 --- /dev/null +++ b/vendor/smithay/src/wayland/idle_inhibit/mod.rs @@ -0,0 +1,129 @@ +//! Utilities for handling the `zwp_idle_inhibit` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation create the [`IdleInhibitManagerState`] and +//! implement the [`IdleInhibitHandler`], as shown in this example: +//! +//! ``` +//! use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! use smithay::wayland::idle_inhibit::{IdleInhibitManagerState, IdleInhibitHandler}; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the compositor state +//! IdleInhibitManagerState::new::(&display.handle()); +//! +//! // implement the necessary trait +//! impl IdleInhibitHandler for State { +//! fn inhibit(&mut self, surface: WlSurface) { +//! // … +//! } +//! +//! fn uninhibit(&mut self, surface: WlSurface) { +//! // … +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` + +use _idle_inhibit::zwp_idle_inhibit_manager_v1::{Request, ZwpIdleInhibitManagerV1}; +use _idle_inhibit::zwp_idle_inhibitor_v1::ZwpIdleInhibitorV1; +use wayland_protocols::wp::idle_inhibit::zv1::server as _idle_inhibit; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New}; + +use crate::wayland::idle_inhibit::inhibitor::IdleInhibitorState; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +pub mod inhibitor; + +const MANAGER_VERSION: u32 = 1; + +/// State of the zwp_idle_inhibit_manager_v1 Global +#[derive(Debug)] +pub struct IdleInhibitManagerState { + global: GlobalId, +} + +impl IdleInhibitManagerState { + /// Create new [`zwp_idle_inhibit_manager`](ZwpIdleInhibitManagerV1) global. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: IdleInhibitHandler, + D: 'static, + { + let global = display.create_global::(MANAGER_VERSION, GlobalData); + + Self { global } + } + + /// Returns the fractional scale manager global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: IdleInhibitHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _display: &DisplayHandle, + _client: &Client, + manager: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(manager, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: IdleInhibitHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _manager: &ZwpIdleInhibitManagerV1, + request: Request, + _display: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + Request::CreateInhibitor { id, surface } => { + state.inhibit(surface.clone()); + data_init.init(id, IdleInhibitorState::new(surface)); + } + Request::Destroy => (), + _ => unreachable!(), + } + } +} + +/// Handler trait for idle-inhibit. +pub trait IdleInhibitHandler { + /// Enable idle inhibition for the output of the provided surface. + fn inhibit(&mut self, surface: WlSurface); + + /// Stop inhibition for the provided surface. + /// + /// This function is only called when a client explicitly removes the session + /// inhibition. It is up to the compositor to ignore inhibiting surfaces which + /// are invisible or dead. + fn uninhibit(&mut self, surface: WlSurface); +} diff --git a/vendor/smithay/src/wayland/idle_notify/mod.rs b/vendor/smithay/src/wayland/idle_notify/mod.rs new file mode 100644 index 0000000..f15f6b8 --- /dev/null +++ b/vendor/smithay/src/wayland/idle_notify/mod.rs @@ -0,0 +1,366 @@ +//! This interface allows clients to monitor user idle status. +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::idle_notify::{IdleNotifierState, IdleNotifierHandler}; +//! # use smithay::input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}; +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! struct State { idle_notifier: IdleNotifierState } +//! # let mut event_loop = smithay::reexports::calloop::EventLoop::::try_new().unwrap(); +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the idle_notifier state +//! let idle_notifier = IdleNotifierState::::new( +//! &display.handle(), +//! event_loop.handle(), +//! ); +//! +//! let state = State { idle_notifier }; +//! +//! // Implement the necessary trait +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! # impl SeatHandler for State { +//! # type KeyboardFocus = WlSurface; +//! # type PointerFocus = WlSurface; +//! # type TouchFocus = WlSurface; +//! # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! # } +//! # impl PointerConstraintsHandler for State {} +//! impl IdleNotifierHandler for State { +//! fn idle_notifier_state(&mut self) -> &mut IdleNotifierState { +//! &mut self.idle_notifier +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // On input you should notify the idle_notifier +//! // state.idle_notifier.notify_activity(&seat); +//! ``` + +use std::{ + collections::HashMap, + sync::{ + Mutex, + atomic::{self, AtomicBool}, + }, + time::Duration, +}; + +use calloop::{LoopHandle, RegistrationToken, timer::TimeoutAction}; +use wayland_protocols::ext::idle_notify::v1::server::{ + ext_idle_notification_v1::{self, ExtIdleNotificationV1}, + ext_idle_notifier_v1::{self, ExtIdleNotifierV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId}, + protocol::wl_seat::WlSeat, +}; + +use crate::{ + input::{Seat, SeatHandler}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +/// Handler trait for ext-idle-notify +pub trait IdleNotifierHandler: Sized { + /// [`IdleNotifierState`] getter + fn idle_notifier_state(&mut self) -> &mut IdleNotifierState; +} + +/// User data of the [`ExtIdleNotificationV1`] resource +#[derive(Debug)] +pub struct IdleNotificationUserData { + seat: WlSeat, + is_idle: AtomicBool, + timeout: Duration, + timer_token: Mutex>, + + /// If listener was created with `get_input_idle_notification` + ignore_inhibitor: bool, +} + +impl IdleNotificationUserData { + fn take_timer_token(&self) -> Option { + self.timer_token.lock().unwrap().take() + } + + fn set_timer_token(&self, idle: Option) { + *self.timer_token.lock().unwrap() = idle; + } + + fn set_idle(&self, idle: bool) { + self.is_idle.store(idle, atomic::Ordering::Release); + } + + fn is_idle(&self) -> bool { + self.is_idle.load(atomic::Ordering::Acquire) + } +} + +/// State of ext-idle-notify module +#[derive(Debug)] +pub struct IdleNotifierState { + global: GlobalId, + notifications: HashMap>, + loop_handle: LoopHandle<'static, D>, + is_inhibited: bool, +} + +impl IdleNotifierState { + /// Create new [`ExtIdleNotifierV1`] global. + pub fn new(display: &DisplayHandle, loop_handle: LoopHandle<'static, D>) -> Self + where + D: GlobalDispatch, + D: IdleNotifierHandler, + D: 'static, + { + let global = display.create_global::(2, GlobalData); + Self { + global, + notifications: HashMap::new(), + loop_handle, + is_inhibited: false, + } + } + + /// Inhibit entering idle state, eg. by the idle-inhibit protocol + pub fn set_is_inhibited(&mut self, is_inhibited: bool) { + if self.is_inhibited == is_inhibited { + return; + } + + self.is_inhibited = is_inhibited; + + for notification in self.notifications() { + let data = notification.data::().unwrap(); + + if data.ignore_inhibitor { + continue; + } + + if is_inhibited { + if data.is_idle() { + notification.resumed(); + data.set_idle(false); + } + + if let Some(token) = data.take_timer_token() { + self.loop_handle.remove(token); + } + } else { + self.reinsert_timer(notification); + } + } + } + + /// Is idle state inhibited, eg. by the idle-inhibit protocol + pub fn is_inhibited(&mut self) -> bool { + self.is_inhibited + } + + /// Should be called whenever activity occurs on a seat, eg. mouse/keyboard input. + /// + /// You may want to use [`Self::notify_activity`] instead which accepts a [`Seat`]. + pub fn notify_activity_for_wl_seat(&mut self, seat: &WlSeat) { + let Some(notifications) = self.notifications.get(seat) else { + return; + }; + + for notification in notifications { + let data = notification.data::().unwrap(); + + if data.is_idle() { + notification.resumed(); + data.set_idle(false); + } + + self.reinsert_timer(notification); + } + } + + /// Returns the [`ExtIdleNotifierV1`] global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } + + fn notifications(&self) -> impl Iterator { + self.notifications.values().flatten() + } + + fn reinsert_timer(&self, notification: &ExtIdleNotificationV1) { + let data = notification.data::().unwrap(); + + if let Some(token) = data.take_timer_token() { + self.loop_handle.remove(token); + } + + if !data.ignore_inhibitor && self.is_inhibited { + return; + } + + let token = self + .loop_handle + .insert_source(calloop::timer::Timer::from_duration(data.timeout), { + let idle_notification = notification.clone(); + move |_, _, state| { + let data = idle_notification.data::().unwrap(); + + let is_inhibited = !data.ignore_inhibitor && state.idle_notifier_state().is_inhibited; + let is_idle_already = data.is_idle(); + + if !is_inhibited && !is_idle_already { + idle_notification.idled(); + data.set_idle(true); + } + + data.set_timer_token(None); + TimeoutAction::Drop + } + }); + + data.set_timer_token(token.ok()); + } +} + +impl IdleNotifierState { + /// Should be called whenever activity occurs on a seat, eg. mouse/keyboard input. + pub fn notify_activity(&mut self, seat: &Seat) { + for seat in &seat.arc.inner.lock().unwrap().known_seats { + if let Ok(seat) = seat.upgrade() { + self.notify_activity_for_wl_seat(&seat); + } + } + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: IdleNotifierHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: IdleNotifierHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ExtIdleNotifierV1, + request: ext_idle_notifier_v1::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_idle_notifier_v1::Request::GetIdleNotification { id, timeout, seat } => { + let timeout = Duration::from_millis(timeout as u64); + + let idle_notifier_state = state.idle_notifier_state(); + + let idle_notification = data_init.init( + id, + IdleNotificationUserData { + seat: seat.clone(), + is_idle: AtomicBool::new(false), + timeout, + timer_token: Mutex::new(None), + ignore_inhibitor: false, + }, + ); + + idle_notifier_state.reinsert_timer(&idle_notification); + + state + .idle_notifier_state() + .notifications + .entry(seat) + .or_default() + .push(idle_notification); + } + ext_idle_notifier_v1::Request::GetInputIdleNotification { id, timeout, seat } => { + let timeout = Duration::from_millis(timeout as u64); + + let idle_notifier_state = state.idle_notifier_state(); + + let idle_notification = data_init.init( + id, + IdleNotificationUserData { + seat: seat.clone(), + is_idle: AtomicBool::new(false), + timeout, + timer_token: Mutex::new(None), + ignore_inhibitor: true, + }, + ); + + idle_notifier_state.reinsert_timer(&idle_notification); + + state + .idle_notifier_state() + .notifications + .entry(seat) + .or_default() + .push(idle_notification); + } + ext_idle_notifier_v1::Request::Destroy => {} + _ => unimplemented!(), + } + } +} + +impl Dispatch2 for IdleNotificationUserData +where + D: IdleNotifierHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ExtIdleNotificationV1, + request: ext_idle_notification_v1::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_idle_notification_v1::Request::Destroy => {} + _ => unimplemented!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, notification: &ExtIdleNotificationV1) { + let state = state.idle_notifier_state(); + if let Some(notifications) = state.notifications.get_mut(&self.seat) { + notifications.retain(|x| x != notification); + } + + state + .notifications + .retain(|seat, notifications| !notifications.is_empty() && seat.is_alive()); + } +} diff --git a/vendor/smithay/src/wayland/image_capture_source/mod.rs b/vendor/smithay/src/wayland/image_capture_source/mod.rs new file mode 100644 index 0000000..099a8ca --- /dev/null +++ b/vendor/smithay/src/wayland/image_capture_source/mod.rs @@ -0,0 +1,614 @@ +//! Image capture source protocol +//! +//! This module implements the `ext-image-capture-source-v1` protocol, which provides +//! opaque image capture source objects that can be created from outputs, toplevels, +//! or custom compositor-defined sources. +//! +//! ## Architecture +//! +//! This implementation is **modular**. Each source type (output, toplevel, custom) has +//! its own independent state and handler: +//! +//! - [`ImageCaptureSourceState`] + [`ImageCaptureSourceHandler`]: Core source handling +//! - [`OutputCaptureSourceState`] + [`OutputCaptureSourceHandler`]: Output capture sources +//! - [`ToplevelCaptureSourceState`] + [`ToplevelCaptureSourceHandler`]: Toplevel capture sources +//! +//! Compositors only implement the source types they need. Custom source types (like +//! workspace capture) can be implemented by compositors using [`ImageCaptureSource::new()`] +//! directly. +//! +//! ## How to use it +//! +//! ### Output Capture Only +//! +//! ```no_run +//! use smithay::output::Output; +//! use smithay::wayland::image_capture_source::{ +//! ImageCaptureSourceState, ImageCaptureSourceHandler, ImageCaptureSource, +//! OutputCaptureSourceState, OutputCaptureSourceHandler, +//! }; +//! +//! pub struct State { +//! image_capture_source: ImageCaptureSourceState, +//! output_capture_source: OutputCaptureSourceState, +//! } +//! +//! impl ImageCaptureSourceHandler for State { +//! fn source_destroyed(&mut self, source: ImageCaptureSource) { +//! // Optional: clean up compositor-side state +//! } +//! } +//! +//! impl OutputCaptureSourceHandler for State { +//! fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState { +//! &mut self.output_capture_source +//! } +//! +//! fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) { +//! source.user_data().insert_if_missing(|| output.downgrade()); +//! } +//! } +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! let image_capture_source = ImageCaptureSourceState::new(); +//! let output_capture_source = OutputCaptureSourceState::new::(&display_handle); +//! +//! smithay::delegate_dispatch2!(State); +//! ``` +//! +//! ### With Toplevel Capture +//! +//! ```no_run +//! use smithay::output::Output; +//! use smithay::wayland::image_capture_source::{ +//! ImageCaptureSourceState, ImageCaptureSourceHandler, ImageCaptureSource, +//! OutputCaptureSourceState, OutputCaptureSourceHandler, +//! ToplevelCaptureSourceState, ToplevelCaptureSourceHandler, +//! }; +//! use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle; +//! +//! pub struct State { +//! image_capture_source: ImageCaptureSourceState, +//! output_capture_source: OutputCaptureSourceState, +//! toplevel_capture_source: ToplevelCaptureSourceState, +//! } +//! +//! impl ImageCaptureSourceHandler for State { +//! fn source_destroyed(&mut self, source: ImageCaptureSource) { +//! // Optional cleanup +//! } +//! } +//! +//! impl OutputCaptureSourceHandler for State { +//! fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState { +//! &mut self.output_capture_source +//! } +//! +//! fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) { +//! source.user_data().insert_if_missing(|| output.downgrade()); +//! } +//! } +//! +//! impl ToplevelCaptureSourceHandler for State { +//! fn toplevel_capture_source_state(&mut self) -> &mut ToplevelCaptureSourceState { +//! &mut self.toplevel_capture_source +//! } +//! +//! fn toplevel_source_created(&mut self, source: ImageCaptureSource, toplevel: ForeignToplevelHandle) { +//! source.user_data().insert_if_missing(|| toplevel.downgrade()); +//! } +//! } +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! let image_capture_source = ImageCaptureSourceState::new(); +//! let output_capture_source = OutputCaptureSourceState::new::(&display_handle); +//! let toplevel_capture_source = ToplevelCaptureSourceState::new::(&display_handle); +//! +//! smithay::delegate_dispatch2!(State); +//! ``` +//! +//! ### Custom Capture Sources +//! +//! Compositors can implement custom source types (e.g., workspace capture) by handling +//! their own protocol and creating sources directly: +//! +//! ```ignore +//! // In your custom protocol's create_source request handler: +//! fn handle_workspace_create_source(&mut self, workspace: &WorkspaceHandle, source_id: New) { +//! let source = ImageCaptureSource::new(); +//! source.user_data().insert_if_missing(|| MyWorkspaceData::from(workspace)); +//! data_init.init(source_id, ImageCaptureSourceData { source }); +//! } +//! ``` + +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicUsize, Ordering}, +}; + +use wayland_protocols::ext::image_capture_source::v1::server::{ + ext_foreign_toplevel_image_capture_source_manager_v1::{ + self, ExtForeignToplevelImageCaptureSourceManagerV1, + }, + ext_image_capture_source_v1::{self, ExtImageCaptureSourceV1}, + ext_output_image_capture_source_manager_v1::{self, ExtOutputImageCaptureSourceManagerV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, backend::GlobalId, +}; + +use crate::output::Output; +use crate::utils::user_data::UserDataMap; +use crate::wayland::foreign_toplevel_list::ForeignToplevelHandle; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +// ============================================================================ +// Core types +// ============================================================================ + +/// Counter for generating unique source IDs. +static NEXT_SOURCE_ID: AtomicUsize = AtomicUsize::new(1); + +/// Inner state for an image capture source. +#[derive(Debug)] +struct ImageCaptureSourceInner { + id: usize, + alive: AtomicBool, + instances: Mutex>>, +} + +/// Weak reference to an [`ImageCaptureSource`]. +#[derive(Debug, Clone)] +pub struct ImageCaptureSourceWeakHandle { + inner: std::sync::Weak<(ImageCaptureSourceInner, UserDataMap)>, +} + +impl ImageCaptureSourceWeakHandle { + /// Attempt to upgrade to a strong [`ImageCaptureSource`] reference. + pub fn upgrade(&self) -> Option { + Some(ImageCaptureSource { + inner: self.inner.upgrade()?, + }) + } +} + +/// An opaque handle to an image capture source. +/// +/// This represents a capturable resource. The actual type of resource +/// (output, toplevel, or custom) is determined by what the compositor stores +/// in [`Self::user_data()`]. +#[derive(Debug, Clone)] +pub struct ImageCaptureSource { + inner: Arc<(ImageCaptureSourceInner, UserDataMap)>, +} + +impl PartialEq for ImageCaptureSource { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for ImageCaptureSource {} + +impl std::hash::Hash for ImageCaptureSource { + fn hash(&self, state: &mut H) { + self.id().hash(state); + } +} + +impl ImageCaptureSource { + /// Create a new image capture source. + /// + /// Used internally when sources are created from outputs or toplevels. + /// Compositors implementing custom source types can also use this directly. + pub fn new() -> Self { + Self { + inner: Arc::new(( + ImageCaptureSourceInner { + id: NEXT_SOURCE_ID.fetch_add(1, Ordering::Relaxed), + alive: AtomicBool::new(true), + instances: Mutex::new(Vec::new()), + }, + UserDataMap::new(), + )), + } + } + + /// Get the unique identifier for this capture source. + pub fn id(&self) -> usize { + self.inner.0.id + } + + /// Check if this capture source is still valid. + pub fn alive(&self) -> bool { + self.inner.0.alive.load(Ordering::Acquire) + } + + /// Access the [`UserDataMap`] for storing compositor-specific data. + pub fn user_data(&self) -> &UserDataMap { + &self.inner.1 + } + + /// Create a weak reference to this source. + pub fn downgrade(&self) -> ImageCaptureSourceWeakHandle { + ImageCaptureSourceWeakHandle { + inner: Arc::downgrade(&self.inner), + } + } + + /// Retrieve an [`ImageCaptureSource`] from an existing protocol resource. + pub fn from_resource(resource: &ExtImageCaptureSourceV1) -> Option { + resource + .data::() + .map(|d| d.source.clone()) + } + + /// Add a protocol resource instance to this source. + pub fn add_instance(&self, resource: &ExtImageCaptureSourceV1) { + self.inner.0.instances.lock().unwrap().push(resource.downgrade()); + } + + /// Mark this source as destroyed. + fn mark_destroyed(&self) { + self.inner.0.alive.store(false, Ordering::Release); + } +} + +impl Default for ImageCaptureSource { + fn default() -> Self { + Self::new() + } +} + +/// User data associated with an [`ExtImageCaptureSourceV1`] resource. +#[derive(Debug)] +pub struct ImageCaptureSourceData { + /// The capture source this resource represents. + pub source: ImageCaptureSource, +} + +// ============================================================================ +// Core state and handler +// ============================================================================ + +/// Core state for image capture source handling. +/// +/// This handles the [`ExtImageCaptureSourceV1`] resource itself. You must also +/// use [`OutputCaptureSourceState`] and/or [`ToplevelCaptureSourceState`] to +/// actually create sources. +#[derive(Debug, Default)] +pub struct ImageCaptureSourceState; + +impl ImageCaptureSourceState { + /// Create a new [`ImageCaptureSourceState`]. + pub fn new() -> Self { + Self + } +} + +/// Core handler for image capture sources. +/// +/// This is required by both [`OutputCaptureSourceHandler`] and +/// [`ToplevelCaptureSourceHandler`]. +pub trait ImageCaptureSourceHandler: + Dispatch + 'static +{ + /// Called when a capture source is destroyed. + /// + /// Use this to clean up any compositor-side state associated with the source. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn source_destroyed(&mut self, source: ImageCaptureSource) { + let _ = source; + } +} + +// Dispatch for the capture source resource +impl Dispatch2 for ImageCaptureSourceData +where + D: ImageCaptureSourceHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ExtImageCaptureSourceV1, + request: ext_image_capture_source_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_capture_source_v1::Request::Destroy => { + // Cleanup is handled in the `destroyed` callback + } + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + _resource: &ExtImageCaptureSourceV1, + ) { + self.source.mark_destroyed(); + state.source_destroyed(self.source.clone()); + } +} + +// ============================================================================ +// Output capture source manager +// ============================================================================ + +/// Data for the output capture source manager global. +#[allow(missing_debug_implementations)] +pub struct OutputCaptureSourceGlobalData { + filter: Box bool + Send + Sync>, +} + +/// State for the output image capture source manager. +/// +/// This binds the [`ExtOutputImageCaptureSourceManagerV1`] global, allowing +/// clients to create capture sources from outputs. +#[derive(Debug)] +pub struct OutputCaptureSourceState { + global: GlobalId, +} + +impl OutputCaptureSourceState { + /// Register the output capture source manager global. + pub fn new(display: &DisplayHandle) -> Self + where + D: OutputCaptureSourceHandler, + { + Self::new_with_filter::(display, |_| true) + } + + /// Register the output capture source manager global with a client filter. + pub fn new_with_filter(display: &DisplayHandle, filter: F) -> Self + where + D: OutputCaptureSourceHandler, + F: Fn(&Client) -> bool + Send + Sync + 'static, + { + let global = display.create_global::( + 1, + OutputCaptureSourceGlobalData { + filter: Box::new(filter), + }, + ); + + Self { global } + } + + /// Get the [`GlobalId`] of this manager. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +/// Handler for output capture sources. +/// +/// Implement this to enable output capture. Requires [`ImageCaptureSourceHandler`]. +pub trait OutputCaptureSourceHandler: + ImageCaptureSourceHandler + + GlobalDispatch + + Dispatch +{ + /// Returns a mutable reference to the [`OutputCaptureSourceState`]. + fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState; + + /// Called when a capture source is created from an output. + /// + /// Use [`ImageCaptureSource::user_data()`] to store your representation + /// of this output. + /// + /// This method is **not** called if the output handle is invalid at the time the + /// source was created. + fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) { + let _ = (source, output); + } +} + +impl GlobalDispatch2 for OutputCaptureSourceGlobalData +where + D: OutputCaptureSourceHandler, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: OutputCaptureSourceHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ExtOutputImageCaptureSourceManagerV1, + request: ext_output_image_capture_source_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_output_image_capture_source_manager_v1::Request::CreateSource { source, output } => { + let output_inner = Output::from_resource(&output); + + let capture_source = ImageCaptureSource::new(); + + let source_resource = data_init.init( + source, + ImageCaptureSourceData { + source: capture_source.clone(), + }, + ); + + capture_source.add_instance(&source_resource); + if let Some(output_inner) = output_inner { + state.output_source_created(capture_source, &output_inner); + } + } + ext_output_image_capture_source_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +// ============================================================================ +// Toplevel capture source manager +// ============================================================================ + +/// Data for the toplevel capture source manager global. +#[allow(missing_debug_implementations)] +pub struct ToplevelCaptureSourceGlobalData { + filter: Box bool + Send + Sync>, +} + +/// State for the foreign toplevel image capture source manager. +/// +/// This binds the [`ExtForeignToplevelImageCaptureSourceManagerV1`] global, +/// allowing clients to create capture sources from foreign toplevels. +/// +/// This implementation uses Smithay's [`ForeignToplevelHandle`]. Compositors +/// with custom foreign-toplevel implementations should handle the global +/// themselves and use [`ImageCaptureSource::new()`] directly. +#[derive(Debug)] +pub struct ToplevelCaptureSourceState { + global: GlobalId, +} + +impl ToplevelCaptureSourceState { + /// Register the toplevel capture source manager global. + pub fn new(display: &DisplayHandle) -> Self + where + D: ToplevelCaptureSourceHandler, + { + Self::new_with_filter::(display, |_| true) + } + + /// Register the toplevel capture source manager global with a client filter. + pub fn new_with_filter(display: &DisplayHandle, filter: F) -> Self + where + D: ToplevelCaptureSourceHandler, + F: Fn(&Client) -> bool + Send + Sync + 'static, + { + let global = display.create_global::( + 1, + ToplevelCaptureSourceGlobalData { + filter: Box::new(filter), + }, + ); + + Self { global } + } + + /// Get the [`GlobalId`] of this manager. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +/// Handler for toplevel capture sources. +/// +/// Implement this to enable toplevel capture using Smithay's +/// [`ForeignToplevelHandle`]. Requires [`ImageCaptureSourceHandler`]. +/// +/// Compositors with custom foreign-toplevel implementations should NOT use +/// this. Instead, handle the protocol directly and use [`ImageCaptureSource::new()`]. +pub trait ToplevelCaptureSourceHandler: + ImageCaptureSourceHandler + + GlobalDispatch + + Dispatch +{ + /// Returns a mutable reference to the [`ToplevelCaptureSourceState`]. + fn toplevel_capture_source_state(&mut self) -> &mut ToplevelCaptureSourceState; + + /// Called when a capture source is created from a foreign toplevel. + /// + /// Use [`ImageCaptureSource::user_data()`] to store your representation + /// of this toplevel. + /// + /// This method is **not** called if the toplevel handle is invalid at the time the + /// source was created. + fn toplevel_source_created(&mut self, source: ImageCaptureSource, toplevel: ForeignToplevelHandle) { + let _ = (source, toplevel); + } +} + +impl GlobalDispatch2 for ToplevelCaptureSourceGlobalData +where + D: ToplevelCaptureSourceHandler, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: ToplevelCaptureSourceHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ExtForeignToplevelImageCaptureSourceManagerV1, + request: ext_foreign_toplevel_image_capture_source_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_foreign_toplevel_image_capture_source_manager_v1::Request::CreateSource { + source, + toplevel_handle, + } => { + let handle = ForeignToplevelHandle::from_resource(&toplevel_handle); + + let capture_source = ImageCaptureSource::new(); + + let source_resource = data_init.init( + source, + ImageCaptureSourceData { + source: capture_source.clone(), + }, + ); + + capture_source.add_instance(&source_resource); + if let Some(handle) = handle { + state.toplevel_source_created(capture_source, handle); + } + } + ext_foreign_toplevel_image_capture_source_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/image_copy_capture/mod.rs b/vendor/smithay/src/wayland/image_copy_capture/mod.rs new file mode 100644 index 0000000..b1e2d08 --- /dev/null +++ b/vendor/smithay/src/wayland/image_copy_capture/mod.rs @@ -0,0 +1,1409 @@ +//! Image copy capture protocol (screencopy) +//! +//! This module implements the `ext-image-copy-capture-v1` protocol, which allows +//! clients to capture the contents of outputs, toplevels, or other image sources. +//! +//! ## Overview +//! +//! The protocol works as follows: +//! +//! 1. Client creates a capture session from an [`ImageCaptureSource`] +//! 2. Compositor sends buffer constraints (size, formats) via the session +//! 3. Client allocates a matching buffer +//! 4. Client creates a frame, attaches the buffer, and requests capture +//! 5. Compositor renders to the buffer and signals completion +//! +//! ## How to use it +//! +//! ```no_run +//! use smithay::wayland::image_copy_capture::{ +//! ImageCopyCaptureState, ImageCopyCaptureHandler, BufferConstraints, +//! Session, SessionRef, Frame, +//! }; +//! use smithay::wayland::image_capture_source::{ +//! ImageCaptureSourceState, ImageCaptureSourceHandler, ImageCaptureSource, +//! OutputCaptureSourceState, OutputCaptureSourceHandler, +//! }; +//! use smithay::output::Output; +//! +//! pub struct State { +//! image_capture_source: ImageCaptureSourceState, +//! output_capture_source: OutputCaptureSourceState, +//! image_copy_capture: ImageCopyCaptureState, +//! } +//! +//! impl ImageCopyCaptureHandler for State { +//! fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState { +//! &mut self.image_copy_capture +//! } +//! +//! fn capture_constraints(&mut self, source: &ImageCaptureSource) -> Option { +//! // Return buffer constraints for the given source +//! // Return None to reject capture of this source +//! todo!() +//! } +//! +//! fn new_session(&mut self, session: Session) { +//! // Store the session for later use +//! } +//! +//! fn frame(&mut self, session: &SessionRef, frame: Frame) { +//! // Perform the actual capture +//! // Call frame.success(...) or frame.fail(...) when done +//! } +//! } +//! +//! # impl ImageCaptureSourceHandler for State {} +//! # impl OutputCaptureSourceHandler for State { +//! # fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState { +//! # &mut self.output_capture_source +//! # } +//! # fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) { +//! # source.user_data().insert_if_missing(|| output.downgrade()); +//! # } +//! # } +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! let state = ImageCopyCaptureState::new::(&display_handle); +//! +//! smithay::delegate_dispatch2!(State); +//! ``` +//! +//! ## Session Lifecycle +//! +//! Sessions are managed using RAII patterns: +//! +//! - [`Session`] is an owned wrapper that sends `stopped` when dropped +//! - [`SessionRef`] is a cloneable reference for passing to frame handlers +//! - When a session is stopped (by dropping or explicit `stop()`), all pending frames are failed +//! +//! The same pattern applies to [`CursorSession`] and [`CursorSessionRef`]. + +// Based on cosmic-comp's screencopy implementation by Victoria Brekenfeld (@Drakulix) +// and Ian Douglas Scott (@ids1024) at System76. +// Original source: https://github.com/pop-os/cosmic-comp/blob/master/src/wayland/protocols/screencopy.rs + +use std::{ + ops, + sync::{Arc, Mutex}, + time::Duration, +}; + +use wayland_protocols::ext::image_copy_capture::v1::server::{ + ext_image_copy_capture_cursor_session_v1::{self, ExtImageCopyCaptureCursorSessionV1}, + ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1, FailureReason}, + ext_image_copy_capture_manager_v1::{self, ExtImageCopyCaptureManagerV1}, + ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, + backend::GlobalId, + protocol::{wl_buffer::WlBuffer, wl_pointer::WlPointer, wl_shm}, +}; + +#[cfg(feature = "backend_drm")] +use crate::backend::{ + allocator::{Buffer as AllocBuffer, Fourcc, Modifier}, + drm::DrmNode, +}; +use crate::utils::{Buffer as BufferCoords, IsAlive, Rectangle, Size, Transform, user_data::UserDataMap}; +use crate::wayland::image_capture_source::ImageCaptureSource; + +// Buffer validation imports +use crate::backend::renderer::{BufferType, buffer_type}; +#[cfg(feature = "backend_drm")] +use crate::wayland::dmabuf::get_dmabuf; +use crate::wayland::shm::with_buffer_contents; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +// Re-export FailureReason for convenience +pub use wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_frame_v1::FailureReason as CaptureFailureReason; + +/// Buffer constraints that the compositor requires for capture. +/// +/// Clients must allocate buffers matching these constraints. +#[derive(Debug, Clone)] +pub struct BufferConstraints { + /// Required buffer size in pixels. + pub size: Size, + /// Supported SHM buffer formats. + pub shm: Vec, + /// DMA-BUF constraints, if DMA-BUF capture is supported. + #[cfg(feature = "backend_drm")] + pub dma: Option, +} + +/// DMA-BUF specific constraints. +#[cfg(feature = "backend_drm")] +#[derive(Debug, Clone)] +pub struct DmabufConstraints { + /// The DRM render node for buffer allocation. + pub node: DrmNode, + /// Supported format/modifier combinations. + pub formats: Vec<(Fourcc, Vec)>, +} + +// ============================================================================ +// Buffer validation +// ============================================================================ + +/// Validate that a buffer meets the capture constraints. +/// +/// Returns `Ok(())` if the buffer is valid, or a `FailureReason` if not. +fn validate_buffer(buffer: &WlBuffer, constraints: &BufferConstraints) -> Result<(), FailureReason> { + match buffer_type(buffer) { + Some(BufferType::Shm) => validate_shm_buffer(buffer, constraints), + #[cfg(feature = "backend_drm")] + Some(BufferType::Dma) => validate_dmabuf(buffer, constraints), + _ => Err(FailureReason::BufferConstraints), + } +} + +/// Validate an SHM buffer against constraints. +fn validate_shm_buffer(buffer: &WlBuffer, constraints: &BufferConstraints) -> Result<(), FailureReason> { + with_buffer_contents(buffer, |_, _, data| { + if data.width < constraints.size.w || data.height < constraints.size.h { + return Err(FailureReason::BufferConstraints); + } + + if !constraints.shm.contains(&data.format) { + return Err(FailureReason::BufferConstraints); + } + + Ok(()) + }) + .map_err(|_| FailureReason::BufferConstraints)? +} + +/// Validate a DMA-BUF against constraints. +#[cfg(feature = "backend_drm")] +fn validate_dmabuf(buffer: &WlBuffer, constraints: &BufferConstraints) -> Result<(), FailureReason> { + let dmabuf = get_dmabuf(buffer).map_err(|_| FailureReason::BufferConstraints)?; + + let dma_constraints = constraints.dma.as_ref().ok_or(FailureReason::BufferConstraints)?; + + let size = dmabuf.size(); + if size.w < constraints.size.w || size.h < constraints.size.h { + return Err(FailureReason::BufferConstraints); + } + + let format = dmabuf.format(); + let format_valid = dma_constraints + .formats + .iter() + .any(|(f, modifiers)| *f == format.code && modifiers.contains(&format.modifier)); + + if !format_valid { + return Err(FailureReason::BufferConstraints); + } + + Ok(()) +} + +// ============================================================================ +// Session types +// ============================================================================ + +/// Inner state for a capture session. +#[derive(Debug)] +struct SessionInner { + stopped: bool, + constraints: Option, + draw_cursors: bool, + source: ImageCaptureSource, + active_frame: Option, +} + +impl SessionInner { + fn new(source: ImageCaptureSource, draw_cursors: bool) -> Self { + Self { + stopped: false, + constraints: None, + draw_cursors, + source, + active_frame: None, + } + } +} + +/// A cloneable reference to a capture session. +/// +/// Use this to check session state or access the capture source. +/// For methods that might stop the session, use the owned [`Session`]. +#[derive(Debug, Clone)] +pub struct SessionRef { + obj: ExtImageCopyCaptureSessionV1, + inner: Arc>, + user_data: Arc, +} + +impl PartialEq for SessionRef { + fn eq(&self, other: &Self) -> bool { + self.obj == other.obj + } +} + +impl IsAlive for SessionRef { + fn alive(&self) -> bool { + self.obj.is_alive() && !self.inner.lock().unwrap().stopped + } +} + +impl SessionRef { + /// Update the buffer constraints for this session. + /// + /// This sends the constraint events to the client and stores them + /// for frame validation. + pub fn update_constraints(&self, constraints: BufferConstraints) { + let mut inner = self.inner.lock().unwrap(); + + if !self.obj.is_alive() || inner.stopped { + return; + } + + self.obj + .buffer_size(constraints.size.w as u32, constraints.size.h as u32); + + for fmt in &constraints.shm { + self.obj.shm_format(*fmt); + } + + #[cfg(feature = "backend_drm")] + if let Some(dma) = constraints.dma.as_ref() { + let node = Vec::from(dma.node.dev_id().to_ne_bytes()); + self.obj.dmabuf_device(node); + for (fmt, modifiers) in &dma.formats { + let modifiers = modifiers + .iter() + .flat_map(|modifier| u64::from(*modifier).to_ne_bytes()) + .collect::>(); + self.obj.dmabuf_format(*fmt as u32, modifiers); + } + } + + self.obj.done(); + inner.constraints = Some(constraints); + } + + /// Get the current buffer constraints, if set. + pub fn current_constraints(&self) -> Option { + self.inner.lock().unwrap().constraints.clone() + } + + /// Get the capture source for this session. + pub fn source(&self) -> ImageCaptureSource { + self.inner.lock().unwrap().source.clone() + } + + /// Whether the compositor should draw cursors in the captured content. + pub fn draw_cursor(&self) -> bool { + self.inner.lock().unwrap().draw_cursors + } + + /// Access the [`UserDataMap`] for storing compositor-specific session data. + pub fn user_data(&self) -> &UserDataMap { + &self.user_data + } + + /// The client that created this session. + /// + /// Returns `None` once the client has disconnected. + pub fn client(&self) -> Option { + self.obj.client() + } +} + +/// An owned capture session. +/// +/// When dropped, this session will be stopped and all pending frames will be failed. +/// The compositor receives sessions via [`ImageCopyCaptureHandler::new_session`] and +/// should store them for the duration of the capture. +#[derive(Debug)] +pub struct Session(SessionRef); + +impl ops::Deref for Session { + type Target = SessionRef; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for Session { + fn eq(&self, other: &SessionRef) -> bool { + self.0 == *other + } +} + +impl Drop for Session { + fn drop(&mut self) { + let mut inner = self.0.inner.lock().unwrap(); + + if !self.0.obj.is_alive() || inner.stopped { + return; + } + + // Fail the active frame + if let Some(frame) = inner.active_frame.take() { + frame + .inner + .lock() + .unwrap() + .fail(&frame.obj, FailureReason::Stopped); + } + + self.0.obj.stopped(); + inner.constraints.take(); + inner.stopped = true; + } +} + +impl Session { + /// Explicitly stop this session. + /// + /// This is equivalent to dropping the session. + pub fn stop(self) { + drop(self); + } + + /// Get a cloneable reference to this session. + pub fn as_ref(&self) -> SessionRef { + self.0.clone() + } +} + +// ============================================================================ +// Cursor session types +// ============================================================================ + +/// Inner state for a cursor capture session. +#[derive(Debug)] +struct CursorSessionInner { + session_obj: Option, + stopped: bool, + constraints: Option, + source: ImageCaptureSource, + position: Option>, + hotspot: crate::utils::Point, + active_frame: Option, + pointer: WlPointer, +} + +impl CursorSessionInner { + fn new(source: ImageCaptureSource, pointer: WlPointer) -> Self { + Self { + session_obj: None, + stopped: false, + constraints: None, + source, + position: None, + hotspot: crate::utils::Point::from((0, 0)), + active_frame: None, + pointer, + } + } +} + +/// A cloneable reference to a cursor capture session. +#[derive(Debug, Clone)] +pub struct CursorSessionRef { + obj: ExtImageCopyCaptureCursorSessionV1, + inner: Arc>, + user_data: Arc, +} + +impl PartialEq for CursorSessionRef { + fn eq(&self, other: &Self) -> bool { + self.obj == other.obj + } +} + +impl IsAlive for CursorSessionRef { + fn alive(&self) -> bool { + self.obj.is_alive() && !self.inner.lock().unwrap().stopped + } +} + +impl CursorSessionRef { + /// Update the buffer constraints for cursor capture. + pub fn update_constraints(&self, constraints: BufferConstraints) { + let mut inner = self.inner.lock().unwrap(); + + if !self.obj.is_alive() || inner.stopped { + return; + } + + if let Some(session_obj) = inner.session_obj.as_ref() { + session_obj.buffer_size(constraints.size.w as u32, constraints.size.h as u32); + for fmt in &constraints.shm { + session_obj.shm_format(*fmt); + } + #[cfg(feature = "backend_drm")] + if let Some(dma) = constraints.dma.as_ref() { + let node = Vec::from(dma.node.dev_id().to_ne_bytes()); + session_obj.dmabuf_device(node); + for (fmt, modifiers) in &dma.formats { + let modifiers = modifiers + .iter() + .flat_map(|modifier| u64::from(*modifier).to_ne_bytes()) + .collect::>(); + session_obj.dmabuf_format(*fmt as u32, modifiers); + } + } + session_obj.done(); + } + + inner.constraints = Some(constraints); + } + + /// Get the current buffer constraints, if set. + pub fn current_constraints(&self) -> Option { + self.inner.lock().unwrap().constraints.clone() + } + + /// Get the capture source for this session. + pub fn source(&self) -> ImageCaptureSource { + self.inner.lock().unwrap().source.clone() + } + + /// Get the pointer for the session. + pub fn pointer(&self) -> WlPointer { + self.inner.lock().unwrap().pointer.clone() + } + + /// Whether the cursor is currently on this capture source. + pub fn has_cursor(&self) -> bool { + self.inner.lock().unwrap().position.is_some() + } + + /// Update the cursor position on this capture source. + /// + /// Pass `None` when the cursor leaves the capture source. + pub fn set_cursor_pos(&self, position: Option>) { + if !self.obj.is_alive() { + return; + } + + let mut inner = self.inner.lock().unwrap(); + + if inner.position == position { + return; + } + + if inner.position.is_none() && position.is_some() { + self.obj.enter(); + self.obj.hotspot(inner.hotspot.x, inner.hotspot.y); + } + + if let Some(new_pos) = position { + self.obj.position(new_pos.x, new_pos.y); + } else if inner.position.is_some() { + self.obj.leave(); + } + + inner.position = position; + } + + /// Update the cursor hotspot. + pub fn set_cursor_hotspot(&self, hotspot: impl Into>) { + if !self.obj.is_alive() { + return; + } + + let hotspot = hotspot.into(); + let mut inner = self.inner.lock().unwrap(); + + if inner.hotspot == hotspot { + return; + } + + inner.hotspot = hotspot; + if inner.position.is_some() { + self.obj.hotspot(hotspot.x, hotspot.y); + } + } + + /// Access the [`UserDataMap`] for storing compositor-specific session data. + pub fn user_data(&self) -> &UserDataMap { + &self.user_data + } + + /// The client that created this session. + /// + /// Returns `None` once the client has disconnected. + pub fn client(&self) -> Option { + self.obj.client() + } +} + +/// An owned cursor capture session. +#[derive(Debug)] +pub struct CursorSession(CursorSessionRef); + +impl ops::Deref for CursorSession { + type Target = CursorSessionRef; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for CursorSession { + fn eq(&self, other: &CursorSessionRef) -> bool { + self.0 == *other + } +} + +impl Drop for CursorSession { + fn drop(&mut self) { + let mut inner = self.0.inner.lock().unwrap(); + + if !self.0.obj.is_alive() || inner.stopped { + return; + } + + if let Some(session_obj) = inner.session_obj.as_ref() { + session_obj.stopped(); + } + inner.constraints.take(); + + if let Some(frame) = inner.active_frame.take() { + frame + .inner + .lock() + .unwrap() + .fail(&frame.obj, FailureReason::Stopped); + } + + inner.stopped = true; + } +} + +impl CursorSession { + /// Explicitly stop this session. + pub fn stop(self) { + drop(self); + } + + /// Get a cloneable reference to this session. + pub fn as_ref(&self) -> CursorSessionRef { + self.0.clone() + } +} + +// ============================================================================ +// Frame types +// ============================================================================ + +/// Inner state for a capture frame. +#[derive(Debug)] +struct FrameInner { + /// Buffer constraints for validation. + constraints: Option, + buffer: Option, + damage: Vec>, + /// Reference to parent session (stored for future use). + #[allow(dead_code)] + session_obj: Weak, + capture_requested: bool, + failed: Option, + ready: bool, +} + +impl FrameInner { + fn new(session_obj: ExtImageCopyCaptureSessionV1, constraints: Option) -> Self { + Self { + constraints, + buffer: None, + damage: Vec::new(), + session_obj: session_obj.downgrade(), + capture_requested: false, + failed: None, + ready: false, + } + } + + fn fail(&mut self, frame: &ExtImageCopyCaptureFrameV1, reason: FailureReason) { + if self.ready || self.failed.is_some() { + return; + } + self.failed = Some(reason); + if self.capture_requested { + frame.failed(reason); + } + } +} + +/// A cloneable reference to a capture frame. +/// +/// Used for tracking the active frame within a session. +#[derive(Clone, Debug)] +pub struct FrameRef { + obj: ExtImageCopyCaptureFrameV1, + inner: Arc>, +} + +impl PartialEq for FrameRef { + fn eq(&self, other: &Self) -> bool { + self.obj == other.obj + } +} + +impl FrameRef { + /// Get the buffer attached to this frame. + /// + /// # Panics + /// + /// Panics if no buffer has been attached (should only be called after + /// the capture request has been received). + pub fn buffer(&self) -> WlBuffer { + self.inner + .lock() + .unwrap() + .buffer + .clone() + .expect("no buffer attached") + } + + /// Get the damage regions for this frame. + pub fn damage(&self) -> Vec> { + self.inner.lock().unwrap().damage.clone() + } + + /// Check if this frame has already failed. + pub fn has_failed(&self) -> bool { + self.inner.lock().unwrap().failed.is_some() + } +} + +/// An owned capture frame. +/// +/// The compositor receives frames via [`ImageCopyCaptureHandler::frame`] and +/// must call either [`Frame::success`] or [`Frame::fail`] to complete the capture. +/// +/// If the frame is dropped without calling either method, it will automatically fail. +#[derive(Debug)] +pub struct Frame { + frame: FrameRef, + completed: bool, +} + +impl ops::Deref for Frame { + type Target = FrameRef; + fn deref(&self) -> &FrameRef { + &self.frame + } +} + +impl PartialEq for Frame { + fn eq(&self, other: &FrameRef) -> bool { + self.frame == *other + } +} + +impl Frame { + /// Signal successful capture. + /// + /// # Arguments + /// + /// * `transform` - The transform applied to the captured content + /// * `damage` - Optional damage regions, or `None` for full damage + /// * `presented` - The presentation timestamp + pub fn success( + mut self, + transform: impl Into, + damage: impl Into>>>, + presented: impl Into, + ) { + { + let inner = self.frame.inner.lock().unwrap(); + if !inner.capture_requested || inner.failed.is_some() { + return; + } + } + + self.frame.obj.transform(transform.into().into()); + for damage in damage.into().into_iter().flatten() { + self.frame + .obj + .damage(damage.loc.x, damage.loc.y, damage.size.w, damage.size.h); + } + + let time = presented.into(); + let tv_sec_hi = (time.as_secs() >> 32) as u32; + let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32; + let tv_nsec = time.subsec_nanos(); + self.frame.obj.presentation_time(tv_sec_hi, tv_sec_lo, tv_nsec); + + self.frame.inner.lock().unwrap().ready = true; + self.frame.obj.ready(); + + // Prevent drop from sending fail + self.completed = true; + } + + /// Signal failed capture. + pub fn fail(mut self, reason: FailureReason) { + self.frame.inner.lock().unwrap().fail(&self.frame.obj, reason); + // Prevent drop from sending fail again + self.completed = true; + } +} + +impl Drop for Frame { + fn drop(&mut self) { + if !self.completed { + // If success() or fail() wasn't called, send Unknown failure + self.frame + .inner + .lock() + .unwrap() + .fail(&self.frame.obj, FailureReason::Unknown); + } + } +} + +// ============================================================================ +// Handler trait +// ============================================================================ + +/// Handler trait for the image copy capture protocol. +/// +/// Implement this on your compositor's state type to handle capture requests. +pub trait ImageCopyCaptureHandler: + GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + 'static +{ + /// Returns a mutable reference to the [`ImageCopyCaptureState`] delegate type. + fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState; + + /// Return buffer constraints for capturing the given source. + /// + /// Return `None` to reject capture of this source (e.g., if permissions + /// are not granted or the source is invalid). + fn capture_constraints(&mut self, source: &ImageCaptureSource) -> Option; + + /// Return buffer constraints for capturing the cursor on the given source. + /// + /// Return `None` if cursor capture is not supported for this source. + fn cursor_capture_constraints( + &mut self, + source: &ImageCaptureSource, + pointer: &WlPointer, + ) -> Option { + let _ = source; + let _ = pointer; + None + } + + /// Called when a new capture session is created. + /// + /// The compositor should store this session and call + /// [`SessionRef::update_constraints`] when the capture constraints change. + fn new_session(&mut self, session: Session); + + /// Called when a new cursor capture session is created. + fn new_cursor_session(&mut self, session: CursorSession) { + let _ = session; + } + + /// Called when a frame capture is requested. + /// + /// The compositor should render the captured content to the frame's buffer + /// and call either [`Frame::success`] or [`Frame::fail`]. + fn frame(&mut self, session: &SessionRef, frame: Frame); + + /// Called when a cursor frame capture is requested. + fn cursor_frame(&mut self, session: &CursorSessionRef, frame: Frame) { + let _ = session; + frame.fail(FailureReason::Unknown); + } + + /// Called when a frame is aborted (e.g., buffer destroyed before capture) + /// or the client disconnects. + /// + /// Note: In case of implicit destruction the order is undefined and might + /// not follow explicit protocol definitions. + fn frame_aborted(&mut self, frame: FrameRef) { + let _ = frame; + } + + /// Called when a session is destroyed. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn session_destroyed(&mut self, session: SessionRef) { + let _ = session; + } + + /// Called when a cursor session is destroyed. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn cursor_session_destroyed(&mut self, session: CursorSessionRef) { + let _ = session; + } +} + +// ============================================================================ +// State and data types +// ============================================================================ + +/// Data associated with the image copy capture manager global. +#[allow(missing_debug_implementations)] +pub struct ImageCopyCaptureGlobalData { + filter: Box bool + Send + Sync>, +} + +impl std::fmt::Debug for ImageCopyCaptureGlobalData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ImageCopyCaptureGlobalData") + .finish_non_exhaustive() + } +} + +/// User data for session protocol resources. +#[doc(hidden)] +#[derive(Debug)] +pub struct SessionData { + inner: Arc>, + user_data: Arc, +} + +/// User data for cursor session protocol resources. +#[doc(hidden)] +#[derive(Debug)] +pub struct CursorSessionData { + inner: Arc>, + user_data: Arc, +} + +/// User data for frame protocol resources. +#[doc(hidden)] +#[derive(Debug)] +pub struct FrameData { + inner: Arc>, +} + +/// State of the image copy capture protocol. +#[derive(Debug)] +pub struct ImageCopyCaptureState { + global: GlobalId, + sessions: Vec, + cursor_sessions: Vec, +} + +impl ImageCopyCaptureState { + /// Register a new [`ExtImageCopyCaptureManagerV1`] global. + pub fn new(display: &DisplayHandle) -> Self + where + D: ImageCopyCaptureHandler, + { + Self::new_with_filter::(display, |_| true) + } + + /// Register a new [`ExtImageCopyCaptureManagerV1`] global with a client filter. + pub fn new_with_filter(display: &DisplayHandle, filter: F) -> Self + where + D: ImageCopyCaptureHandler, + F: Fn(&Client) -> bool + Clone + Send + Sync + 'static, + { + let global = display.create_global::( + 1, + ImageCopyCaptureGlobalData { + filter: Box::new(filter), + }, + ); + + Self { + global, + sessions: Vec::new(), + cursor_sessions: Vec::new(), + } + } + + /// Get the [`GlobalId`] of the image copy capture manager. + pub fn global(&self) -> GlobalId { + self.global.clone() + } + + /// Get all active sessions. + pub fn sessions(&self) -> &[SessionRef] { + &self.sessions + } + + /// Get all active cursor sessions. + pub fn cursor_sessions(&self) -> &[CursorSessionRef] { + &self.cursor_sessions + } + + /// Clean up dead sessions. + pub fn cleanup(&mut self) { + self.sessions.retain(|s| s.alive()); + self.cursor_sessions.retain(|s| s.alive()); + } +} + +// ============================================================================ +// Dispatch implementations +// ============================================================================ + +impl GlobalDispatch2 for ImageCopyCaptureGlobalData +where + D: ImageCopyCaptureHandler, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: ImageCopyCaptureHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ExtImageCopyCaptureManagerV1, + request: ext_image_copy_capture_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_manager_v1::Request::CreateSession { + session, + source, + options, + } => { + let Some(capture_source) = ImageCaptureSource::from_resource(&source) else { + // Invalid source - create stopped session + let inner = Arc::new(Mutex::new(SessionInner { + stopped: true, + constraints: None, + draw_cursors: false, + source: ImageCaptureSource::new(), + active_frame: None, + })); + let user_data = Arc::new(UserDataMap::new()); + let obj = data_init.init( + session, + SessionData { + inner: inner.clone(), + user_data: user_data.clone(), + }, + ); + obj.stopped(); + return; + }; + + let draw_cursors = options + .into_result() + .map(|o| o.contains(ext_image_copy_capture_manager_v1::Options::PaintCursors)) + .unwrap_or(false); + let inner = Arc::new(Mutex::new(SessionInner::new( + capture_source.clone(), + draw_cursors, + ))); + let user_data = Arc::new(UserDataMap::new()); + + let obj = data_init.init( + session, + SessionData { + inner: inner.clone(), + user_data: user_data.clone(), + }, + ); + + let session_ref = SessionRef { + obj, + inner, + user_data, + }; + + if let Some(constraints) = state.capture_constraints(&capture_source) { + session_ref.update_constraints(constraints); + state + .image_copy_capture_state() + .sessions + .push(session_ref.clone()); + state.new_session(Session(session_ref)); + } else { + // Source rejected + session_ref.obj.stopped(); + session_ref.inner.lock().unwrap().stopped = true; + } + } + ext_image_copy_capture_manager_v1::Request::CreatePointerCursorSession { + session, + source, + pointer, + } => { + let Some(capture_source) = ImageCaptureSource::from_resource(&source) else { + // Invalid source - create stopped session + let inner = Arc::new(Mutex::new(CursorSessionInner::new( + ImageCaptureSource::new(), + pointer, + ))); + inner.lock().unwrap().stopped = true; + let user_data = Arc::new(UserDataMap::new()); + let obj = data_init.init(session, CursorSessionData { inner, user_data }); + // Note: cursor session doesn't have a stopped event + let _ = obj; + return; + }; + + let inner = Arc::new(Mutex::new(CursorSessionInner::new( + capture_source.clone(), + pointer.clone(), + ))); + let user_data = Arc::new(UserDataMap::new()); + + let obj = data_init.init( + session, + CursorSessionData { + inner: inner.clone(), + user_data: user_data.clone(), + }, + ); + + let session_ref = CursorSessionRef { + obj, + inner, + user_data, + }; + + if let Some(constraints) = state.cursor_capture_constraints(&capture_source, &pointer) { + session_ref.update_constraints(constraints); + state + .image_copy_capture_state() + .cursor_sessions + .push(session_ref.clone()); + state.new_cursor_session(CursorSession(session_ref)); + } else { + session_ref.inner.lock().unwrap().stopped = true; + } + } + ext_image_copy_capture_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for SessionData +where + D: ImageCopyCaptureHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + resource: &ExtImageCopyCaptureSessionV1, + request: ext_image_copy_capture_session_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_session_v1::Request::CreateFrame { frame } => { + if self.inner.lock().unwrap().active_frame.is_some() { + resource.post_error( + ext_image_copy_capture_session_v1::Error::DuplicateFrame, + "create_frame sent before the previous frame was destroyed", + ); + return; + } + let constraints = self.inner.lock().unwrap().constraints.clone(); + let inner = Arc::new(Mutex::new(FrameInner::new(resource.clone(), constraints))); + let obj = data_init.init(frame, FrameData { inner: inner.clone() }); + self.inner.lock().unwrap().active_frame = Some(FrameRef { obj, inner }); + } + ext_image_copy_capture_session_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + resource: &ExtImageCopyCaptureSessionV1, + ) { + let session_ref = SessionRef { + obj: resource.clone(), + inner: self.inner.clone(), + user_data: self.user_data.clone(), + }; + state.session_destroyed(session_ref); + } +} + +// Dispatch for session created from cursor session's get_capture_session +impl Dispatch2 for CursorSessionData +where + D: ImageCopyCaptureHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + resource: &ExtImageCopyCaptureSessionV1, + request: ext_image_copy_capture_session_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_session_v1::Request::CreateFrame { frame } => { + if self.inner.lock().unwrap().active_frame.is_some() { + resource.post_error( + ext_image_copy_capture_session_v1::Error::DuplicateFrame, + "create_frame sent before the previous frame was destroyed", + ); + return; + } + let constraints = self.inner.lock().unwrap().constraints.clone(); + let inner = Arc::new(Mutex::new(FrameInner::new(resource.clone(), constraints))); + let obj = data_init.init(frame, FrameData { inner: inner.clone() }); + self.inner.lock().unwrap().active_frame = Some(FrameRef { obj, inner }); + } + ext_image_copy_capture_session_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for CursorSessionData +where + D: ImageCopyCaptureHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ExtImageCopyCaptureCursorSessionV1, + request: ext_image_copy_capture_cursor_session_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_cursor_session_v1::Request::GetCaptureSession { session } => { + let mut inner = self.inner.lock().unwrap(); + + if inner.session_obj.is_some() { + // Protocol error: only one session allowed + return; + } + + let obj = data_init.init( + session, + CursorSessionData { + inner: self.inner.clone(), + user_data: self.user_data.clone(), + }, + ); + + if inner.stopped { + obj.stopped(); + } else if let Some(constraints) = inner.constraints.as_ref() { + obj.buffer_size(constraints.size.w as u32, constraints.size.h as u32); + for fmt in &constraints.shm { + obj.shm_format(*fmt); + } + #[cfg(feature = "backend_drm")] + if let Some(dma) = constraints.dma.as_ref() { + let node = Vec::from(dma.node.dev_id().to_ne_bytes()); + obj.dmabuf_device(node); + for (fmt, modifiers) in &dma.formats { + let modifiers = modifiers + .iter() + .flat_map(|modifier| u64::from(*modifier).to_ne_bytes()) + .collect::>(); + obj.dmabuf_format(*fmt as u32, modifiers); + } + } + obj.done(); + } + + inner.session_obj = Some(obj); + } + ext_image_copy_capture_cursor_session_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + resource: &ExtImageCopyCaptureCursorSessionV1, + ) { + let session_ref = CursorSessionRef { + obj: resource.clone(), + inner: self.inner.clone(), + user_data: self.user_data.clone(), + }; + state.cursor_session_destroyed(session_ref); + } +} + +impl Dispatch2 for FrameData +where + D: ImageCopyCaptureHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + resource: &ExtImageCopyCaptureFrameV1, + request: ext_image_copy_capture_frame_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_frame_v1::Request::AttachBuffer { buffer } => { + let mut inner = self.inner.lock().unwrap(); + if inner.capture_requested { + // Protocol error: can't attach after capture + return; + } + inner.buffer = Some(buffer); + } + ext_image_copy_capture_frame_v1::Request::DamageBuffer { x, y, width, height } => { + let mut inner = self.inner.lock().unwrap(); + if inner.capture_requested { + return; + } + // Validate coordinates + if x < 0 || y < 0 || width <= 0 || height <= 0 { + return; + } + inner.damage.push(Rectangle { + loc: (x, y).into(), + size: (width, height).into(), + }); + } + ext_image_copy_capture_frame_v1::Request::Capture => { + let frame_ref = FrameRef { + obj: resource.clone(), + inner: self.inner.clone(), + }; + + { + let mut inner = self.inner.lock().unwrap(); + if inner.capture_requested || inner.failed.is_some() { + return; + } + inner.capture_requested = true; + + let buffer = match inner.buffer.as_ref() { + Some(b) => b, + None => { + inner.fail(resource, FailureReason::BufferConstraints); + return; + } + }; + + match inner.constraints.as_ref() { + Some(constraints) => { + if let Err(reason) = validate_buffer(buffer, constraints) { + inner.fail(resource, reason); + return; + } + } + None => { + // Session was never properly initialized + inner.fail(resource, FailureReason::BufferConstraints); + return; + } + } + } + + // Find the session this frame belongs to + let copy_capture_state = state.image_copy_capture_state(); + + // Try regular sessions first + for session in ©_capture_state.sessions { + let session_inner = session.inner.lock().unwrap(); + if session_inner.active_frame.as_ref() == Some(&frame_ref) { + drop(session_inner); + let session_ref = session.clone(); + let frame = Frame { + frame: frame_ref, + completed: false, + }; + state.frame(&session_ref, frame); + return; + } + } + + // Try cursor sessions + for session in ©_capture_state.cursor_sessions { + let session_inner = session.inner.lock().unwrap(); + if session_inner.active_frame.as_ref() == Some(&frame_ref) { + drop(session_inner); + let session_ref = session.clone(); + let frame = Frame { + frame: frame_ref, + completed: false, + }; + state.cursor_frame(&session_ref, frame); + return; + } + } + + // Frame not found in any session + self.inner.lock().unwrap().fail(resource, FailureReason::Unknown); + } + ext_image_copy_capture_frame_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + resource: &ExtImageCopyCaptureFrameV1, + ) { + let frame_ref = FrameRef { + obj: resource.clone(), + inner: self.inner.clone(), + }; + + // Remove the frame from whichever session holds it + for session in &state.image_copy_capture_state().sessions { + session + .inner + .lock() + .unwrap() + .active_frame + .take_if(|f| f == &frame_ref); + } + for session in &state.image_copy_capture_state().cursor_sessions { + session + .inner + .lock() + .unwrap() + .active_frame + .take_if(|f| f == &frame_ref); + } + + state.frame_aborted(frame_ref); + } +} diff --git a/vendor/smithay/src/wayland/input_method/input_method_handle.rs b/vendor/smithay/src/wayland/input_method/input_method_handle.rs new file mode 100644 index 0000000..5e6c8d0 --- /dev/null +++ b/vendor/smithay/src/wayland/input_method/input_method_handle.rs @@ -0,0 +1,380 @@ +use std::{ + fmt, + sync::{Arc, Mutex}, +}; + +use tracing::warn; +use wayland_protocols_misc::zwp_input_method_v2::server::{ + zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2, + zwp_input_method_v2::{self, ZwpInputMethodV2}, + zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, Resource, protocol::wl_keyboard::KeymapFormat, +}; +use wayland_server::{backend::ClientId, protocol::wl_surface::WlSurface}; + +use crate::{ + input::{SeatHandler, keyboard::KeyboardHandle}, + utils::{Logical, Rectangle, SERIAL_COUNTER, alive_tracker::AliveTracker}, + wayland::{Dispatch2, compositor, seat::WaylandFocus, text_input::TextInputHandle}, +}; + +use super::{ + INPUT_POPUP_SURFACE_ROLE, InputMethodHandler, InputMethodKeyboardUserData, + InputMethodPopupSurfaceUserData, + input_method_keyboard_grab::InputMethodKeyboardGrab, + input_method_popup_surface::{PopupHandle, PopupParent, PopupSurface}, +}; + +#[derive(Default, Debug)] +pub(crate) struct InputMethod { + pub instance: Option, + pub popup_handle: PopupHandle, + pub keyboard_grab: InputMethodKeyboardGrab, +} + +#[derive(Debug)] +pub(crate) struct Instance { + pub object: ZwpInputMethodV2, + pub serial: u32, +} + +impl Instance { + /// Send the done incrementing the serial. + pub(crate) fn done(&mut self) { + self.object.done(); + self.serial += 1; + } +} + +/// Handle to an input method instance +#[derive(Default, Debug, Clone)] +pub struct InputMethodHandle { + pub(crate) inner: Arc>, +} + +impl InputMethodHandle { + pub(super) fn add_instance(&self, instance: &ZwpInputMethodV2) { + let mut inner = self.inner.lock().unwrap(); + if inner.instance.is_some() { + // Only one input method per seat: a newcomer is made unavailable and inert. + instance.unavailable(); + } else { + inner.instance = Some(Instance { + object: instance.clone(), + serial: 0, + }); + } + } + + /// Whether there's an active instance of input-method. + pub fn has_instance(&self) -> bool { + self.inner.lock().unwrap().instance.is_some() + } + + /// Whether `object` is the seat's active input method instance. + pub(super) fn is_active_instance(&self, object: &ZwpInputMethodV2) -> bool { + self.inner + .lock() + .unwrap() + .instance + .as_ref() + .is_some_and(|i| i.object.id() == object.id()) + } + + /// Callback function to access the input method object + pub(crate) fn with_instance(&self, f: F) + where + F: FnOnce(&mut Instance), + { + let mut inner = self.inner.lock().unwrap(); + if let Some(instance) = inner.instance.as_mut() { + f(instance); + } + } + + /// Callback function to access the input method. + pub(crate) fn with_input_method(&self, mut f: F) + where + F: FnMut(&mut InputMethod), + { + let mut inner = self.inner.lock().unwrap(); + f(&mut inner); + } + + /// Indicates that an input method has grabbed a keyboard + pub fn keyboard_grabbed(&self) -> bool { + let inner = self.inner.lock().unwrap(); + let keyboard = inner.keyboard_grab.inner.lock().unwrap(); + keyboard.grab.is_some() + } + + /// The client of the input method that has grabbed the keyboard, if any. + /// + /// This is necessary since input methods like fcitx typically grab the + /// keyboard, and forward unhandled keys through its own virtual keyboard. + /// These may result in an infinite loop if forwarded back through the grab. + /// The compositor should compare the client of virtual keyboards with this, + /// and forward keys from them with + /// [`KeyboardHandle::input_forward_bypassing_grab`]. + pub fn keyboard_grab_client(&self) -> Option { + let inner = self.inner.lock().unwrap(); + let keyboard = inner.keyboard_grab.inner.lock().unwrap(); + keyboard.grab.as_ref()?.client() + } + + pub(crate) fn set_text_input_rectangle( + &self, + state: &mut D, + rect: Rectangle, + ) { + let mut inner = self.inner.lock().unwrap(); + inner.popup_handle.rectangle = rect; + + let mut popup_surface = match inner.popup_handle.surface.clone() { + Some(popup_surface) => popup_surface, + None => return, + }; + + popup_surface.set_text_input_rectangle(rect.loc.x, rect.loc.y, rect.size.w, rect.size.h); + + if let Some(instance) = &inner.instance { + let data = instance.object.data::>().unwrap(); + (data.popup_repositioned)(state, popup_surface); + }; + } + + /// Activate input method on the given surface. + pub(crate) fn activate_input_method(&self, state: &mut D, surface: &WlSurface) { + self.with_input_method(|im| { + if let Some(instance) = im.instance.as_ref() { + instance.object.activate(); + if let Some(popup) = im.popup_handle.surface.as_mut() { + let data = instance.object.data::>().unwrap(); + let location = (data.popup_geometry_callback)(state, surface); + // Remove old popup. + (data.dismiss_popup)(state, popup.clone()); + + // Add a new one with updated parent. + let parent = PopupParent { + surface: surface.clone(), + location, + }; + popup.set_parent(Some(parent)); + (data.new_popup)(state, popup.clone()); + } + } + }); + } + + /// Deactivate the active input method. + /// + /// The `done` is always send when deactivating IME. + pub(crate) fn deactivate_input_method(&self, state: &mut D) { + self.with_input_method(|im| { + if let Some(instance) = im.instance.as_mut() { + instance.object.deactivate(); + instance.done(); + if let Some(popup) = im.popup_handle.surface.as_mut() { + let data = instance.object.data::>().unwrap(); + if popup.get_parent().is_some() { + (data.dismiss_popup)(state, popup.clone()); + } + popup.set_parent(None); + } + } + }); + } +} + +/// User data of ZwpInputMethodV2 object +pub struct InputMethodUserData { + pub(super) handle: InputMethodHandle, + pub(crate) text_input_handle: TextInputHandle, + pub(crate) keyboard_handle: KeyboardHandle, + pub(crate) popup_geometry_callback: fn(&D, &WlSurface) -> Rectangle, + pub(crate) new_popup: fn(&mut D, PopupSurface), + pub(crate) popup_repositioned: fn(&mut D, PopupSurface), + pub(crate) dismiss_popup: fn(&mut D, PopupSurface), +} + +impl fmt::Debug for InputMethodUserData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InputMethodUserData") + .field("handle", &self.handle) + .field("text_input_handle", &self.text_input_handle) + .field("keyboard_handle", &self.keyboard_handle) + .finish() + } +} + +impl Dispatch2 for InputMethodUserData +where + D: Dispatch, + D: Dispatch>, + D: SeatHandler, + D: InputMethodHandler, + ::KeyboardFocus: WaylandFocus, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + seat: &ZwpInputMethodV2, + request: zwp_input_method_v2::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + // An inert newcomer drives nothing, but new-id requests must still init a child. + if !self.handle.is_active_instance(seat) { + match request { + zwp_input_method_v2::Request::GetInputPopupSurface { id, .. } => { + data_init.init( + id, + InputMethodPopupSurfaceUserData { + alive_tracker: AliveTracker::default(), + }, + ); + } + zwp_input_method_v2::Request::GrabKeyboard { keyboard } => { + let keyboard_grab = self.handle.inner.lock().unwrap().keyboard_grab.clone(); + data_init.init( + keyboard, + InputMethodKeyboardUserData { + handle: keyboard_grab, + keyboard_handle: self.keyboard_handle.clone(), + }, + ); + } + _ => {} + } + return; + } + match request { + zwp_input_method_v2::Request::CommitString { text } => { + self.text_input_handle.with_active_text_input(|ti, _surface| { + ti.commit_string(Some(text.clone())); + }); + } + zwp_input_method_v2::Request::SetPreeditString { + text, + cursor_begin, + cursor_end, + } => { + self.text_input_handle.with_active_text_input(|ti, _surface| { + ti.preedit_string(Some(text.clone()), cursor_begin, cursor_end); + }); + } + zwp_input_method_v2::Request::DeleteSurroundingText { + before_length, + after_length, + } => { + self.text_input_handle.with_active_text_input(|ti, _surface| { + ti.delete_surrounding_text(before_length, after_length); + }); + } + zwp_input_method_v2::Request::Commit { serial } => { + let current_serial = self + .handle + .inner + .lock() + .unwrap() + .instance + .as_ref() + .map(|i| i.serial) + .unwrap_or(0); + + self.text_input_handle.done(serial != current_serial); + } + zwp_input_method_v2::Request::GetInputPopupSurface { id, surface } => { + if compositor::give_role(&surface, INPUT_POPUP_SURFACE_ROLE).is_err() + && compositor::get_role(&surface) != Some(INPUT_POPUP_SURFACE_ROLE) + { + // Protocol requires this raise an error, but doesn't define an error enum + seat.post_error(0u32, "Surface already has a role."); + return; + } + + let parent = match self.text_input_handle.focus().clone() { + Some(parent) => { + let location = state.parent_geometry(&parent); + Some(PopupParent { + surface: parent, + location, + }) + } + None => None, + }; + let mut input_method = self.handle.inner.lock().unwrap(); + + let instance = data_init.init( + id, + InputMethodPopupSurfaceUserData { + alive_tracker: AliveTracker::default(), + }, + ); + let popup_rect = Arc::new(Mutex::new(input_method.popup_handle.rectangle)); + let popup = PopupSurface::new(instance, surface, popup_rect, parent); + input_method.popup_handle.surface = Some(popup.clone()); + if popup.get_parent().is_some() { + state.new_popup(popup); + } + } + zwp_input_method_v2::Request::GrabKeyboard { keyboard } => { + let input_method = self.handle.inner.lock().unwrap(); + self.keyboard_handle.set_grab( + state, + input_method.keyboard_grab.clone(), + SERIAL_COUNTER.next_serial(), + ); + let instance = data_init.init( + keyboard, + InputMethodKeyboardUserData { + handle: input_method.keyboard_grab.clone(), + keyboard_handle: self.keyboard_handle.clone(), + }, + ); + let mut keyboard = input_method.keyboard_grab.inner.lock().unwrap(); + keyboard.grab = Some(instance.clone()); + keyboard.text_input_handle = self.text_input_handle.clone(); + let guard = self.keyboard_handle.arc.internal.lock().unwrap(); + instance.repeat_info(guard.repeat_rate, guard.repeat_delay); + let keymap_file = self.keyboard_handle.arc.keymap.lock().unwrap(); + let res = keymap_file.with_fd(false, |fd, size| { + instance.keymap(KeymapFormat::XkbV1, fd, size as u32); + }); + + if let Err(err) = res { + warn!(err = ?err, "Failed to send keymap to client"); + } else { + keyboard.keymap_id = Some(keymap_file.id()); + + // Modifiers can be latched when taking the grab, thus we must send them to keep + // them in sync. + let mods = guard.mods_state.serialized; + instance.modifiers( + SERIAL_COUNTER.next_serial().into(), + mods.depressed, + mods.latched, + mods.locked, + mods.layout_effective, + ); + } + } + zwp_input_method_v2::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: ClientId, input_method: &ZwpInputMethodV2) { + // Only the active instance leaving frees the seat. + if self.handle.is_active_instance(input_method) { + self.handle.inner.lock().unwrap().instance = None; + self.text_input_handle.leave(); + } + } +} diff --git a/vendor/smithay/src/wayland/input_method/input_method_keyboard_grab.rs b/vendor/smithay/src/wayland/input_method/input_method_keyboard_grab.rs new file mode 100644 index 0000000..7507441 --- /dev/null +++ b/vendor/smithay/src/wayland/input_method/input_method_keyboard_grab.rs @@ -0,0 +1,165 @@ +use std::{ + fmt, + sync::{Arc, Mutex}, +}; + +use tracing::warn; +use wayland_protocols_misc::zwp_input_method_v2::server::zwp_input_method_keyboard_grab_v2::{ + self, ZwpInputMethodKeyboardGrabV2, +}; +use wayland_server::{backend::ClientId, protocol::wl_keyboard::KeymapFormat}; + +use crate::input::{ + SeatHandler, + keyboard::{ + GrabStartData as KeyboardGrabStartData, KeyboardGrab, KeyboardHandle, KeyboardInnerHandle, + KeymapFileId, ModifiersState, SerializedMods, + }, +}; +use crate::wayland::text_input::TextInputHandle; +use crate::{ + backend::input::{InputTime, KeyState, Keycode}, + utils::Serial, + wayland::Dispatch2, +}; + +#[derive(Default, Debug)] +pub(crate) struct InputMethodKeyboard { + pub grab: Option, + pub text_input_handle: TextInputHandle, + pub keymap_id: Option, +} + +/// Handle to an input method instance +#[derive(Default, Debug, Clone)] +pub struct InputMethodKeyboardGrab { + pub(crate) inner: Arc>, +} + +impl KeyboardGrab for InputMethodKeyboardGrab +where + D: SeatHandler + 'static, +{ + fn input( + &mut self, + _data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + keycode: Keycode, + key_state: KeyState, + modifiers: Option, + serial: Serial, + time: InputTime, + ) { + let mut inner = self.inner.lock().unwrap(); + + // The keyboard's keymap can change while the grab is active (e.g. when a virtual keyboard + // with its own keymap is in use, e.g., wtype's synthetic keymap), and the keycodes must be + // interpreted using it, so keep the grab in sync like wlroots does. The id is a hash of the + // content, so an input method sending the same keymap back through its own virtual keyboard + // will not result in a loop of keymap updates. + let keymap_changed = handle.with_keymap_file(|keymap_file| { + let id = keymap_file.id(); + if inner.keymap_id == Some(id) { + return false; + } + let keyboard = inner.grab.as_ref().unwrap(); + let res = keymap_file.with_fd(false, |fd, size| { + keyboard.keymap(KeymapFormat::XkbV1, fd, size as u32); + }); + if let Err(err) = res { + warn!(err = ?err, "Failed to send keymap to client"); + return false; + } + inner.keymap_id = Some(id); + true + }); + + // The client resets its state when it receives a new keymap, so send the full modifier + // state before the key in that case. Otherwise, only send the modifiers if they changed. + let (mods_before_key, mods_after_key) = if keymap_changed { + (Some(handle.modifier_state().serialized), None) + } else { + (None, modifiers.map(|m| m.serialized)) + }; + + let keyboard = inner.grab.as_ref().unwrap(); + let send_modifiers = |serial, serialized: SerializedMods| { + keyboard.modifiers( + serial, + serialized.depressed, + serialized.latched, + serialized.locked, + serialized.layout_effective, + ) + }; + inner + .text_input_handle + .active_text_input_serial_or_default(serial.0, |serial| { + if let Some(serialized) = mods_before_key { + send_modifiers(serial, serialized); + } + keyboard.key(serial, time.millis(), keycode.raw() - 8, key_state.into()); + if let Some(serialized) = mods_after_key { + send_modifiers(serial, serialized); + } + }); + } + + fn set_focus( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + focus: Option<::KeyboardFocus>, + serial: crate::utils::Serial, + ) { + handle.set_focus(data, focus, serial) + } + + fn start_data(&self) -> &KeyboardGrabStartData { + &KeyboardGrabStartData { focus: None } + } + + fn unset(&mut self, _data: &mut D) {} +} + +/// User data of ZwpInputKeyboardGrabV2 object +pub struct InputMethodKeyboardUserData { + pub(super) handle: InputMethodKeyboardGrab, + pub(crate) keyboard_handle: KeyboardHandle, +} + +impl fmt::Debug for InputMethodKeyboardUserData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InputMethodKeyboardUserData") + .field("handle", &self.handle) + .field("keyboard_handle", &self.keyboard_handle) + .finish() + } +} + +impl Dispatch2 for InputMethodKeyboardUserData { + fn destroyed(&self, state: &mut D, _client: ClientId, _object: &ZwpInputMethodKeyboardGrabV2) { + let mut inner = self.handle.inner.lock().unwrap(); + inner.grab = None; + inner.keymap_id = None; + drop(inner); + self.keyboard_handle.unset_grab(state); + } + + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &ZwpInputMethodKeyboardGrabV2, + request: zwp_input_method_keyboard_grab_v2::Request, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_input_method_keyboard_grab_v2::Request::Release => { + // Nothing to do + } + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/input_method/input_method_popup_surface.rs b/vendor/smithay/src/wayland/input_method/input_method_popup_surface.rs new file mode 100644 index 0000000..30befc9 --- /dev/null +++ b/vendor/smithay/src/wayland/input_method/input_method_popup_surface.rs @@ -0,0 +1,154 @@ +use std::sync::{Arc, Mutex}; + +use wayland_protocols_misc::zwp_input_method_v2::server::zwp_input_popup_surface_v2::{ + self, ZwpInputPopupSurfaceV2, +}; +use wayland_server::{Resource, backend::ClientId, protocol::wl_surface::WlSurface}; + +use crate::{ + utils::{ + Logical, Point, Rectangle, + alive_tracker::{AliveTracker, IsAlive}, + }, + wayland::Dispatch2, +}; + +/// Handle to a popup surface +#[derive(Debug, Clone, Default)] +pub struct PopupHandle { + pub surface: Option, + pub rectangle: Rectangle, +} + +/// A handle to an input method popup surface +#[derive(Debug, Clone)] +pub struct PopupSurface { + /// The surface role for the input method popup + pub surface_role: ZwpInputPopupSurfaceV2, + surface: WlSurface, + /// Protected cursor area. + pub(crate) rectangle: Arc>>, + /// Location of the popup surface. + location: Arc>>, + /// Current parent of the IME popup. + parent: Option, +} + +impl PopupSurface { + pub(crate) fn new( + surface_role: ZwpInputPopupSurfaceV2, + surface: WlSurface, + rectangle: Arc>>, + parent: Option, + ) -> Self { + let location = Arc::new(Mutex::new(rectangle.lock().unwrap().loc)); + Self { + surface_role, + rectangle, + location, + surface, + parent, + } + } + + /// Is the input method popup surface referred by this handle still alive? + #[inline] + pub fn alive(&self) -> bool { + // TODO other things to check? This may not sufice. + let role_data: &InputMethodPopupSurfaceUserData = self.surface_role.data().unwrap(); + self.surface.alive() && role_data.alive_tracker.alive() + } + + /// Access to the underlying `wl_surface` of this popup + #[inline] + pub fn wl_surface(&self) -> &WlSurface { + &self.surface + } + + /// Access to the parent surface associated with this popup + pub fn get_parent(&self) -> Option<&PopupParent> { + self.parent.as_ref() + } + + /// Set the IME popup surface parent. + pub fn set_parent(&mut self, parent: Option) { + self.parent = parent; + } + + /// Used to access the location of an input popup surface relative to the parent + pub fn location(&self) -> Point { + *self.location.lock().unwrap() + } + + /// Set location of the popup surface relative to the parent. The primary use for this function + /// is to adjust the popup during rendering. + /// + /// Setting this value **won't update** the [`text_input_rectangle`]. + /// + /// [`text_input_rectangle`]: Self::text_input_rectangle + pub fn set_location(&self, location: Point) { + *self.location.lock().unwrap() = location; + } + + /// The region compositor shouldn't obscure when placing the popup within the + /// client. + pub fn text_input_rectangle(&self) -> Rectangle { + *self.rectangle.lock().unwrap() + } + + /// Set relative location of text cursor. + /// + /// Setting this value **will update** the [`location`] to the new `x` and `y`. + /// + /// [`location`]: Self::location + pub fn set_text_input_rectangle(&mut self, x: i32, y: i32, width: i32, height: i32) { + *self.rectangle.lock().unwrap() = Rectangle::new((x, y).into(), (width, height).into()); + *self.location.lock().unwrap() = (x, y).into(); + self.surface_role.text_input_rectangle(x, y, width, height); + } +} + +impl std::cmp::PartialEq for PopupSurface { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.surface_role == other.surface_role + } +} + +/// Parent surface and location for the IME popup. +#[derive(Debug, Clone)] +pub struct PopupParent { + /// The surface IME popup is present over. + pub surface: WlSurface, + /// The location of the parent surface. + pub location: Rectangle, +} + +/// User data of ZwpInputPopupSurfaceV2 object +#[derive(Debug)] +pub struct InputMethodPopupSurfaceUserData { + pub(super) alive_tracker: AliveTracker, +} + +impl Dispatch2 for InputMethodPopupSurfaceUserData { + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &ZwpInputPopupSurfaceV2, + request: zwp_input_popup_surface_v2::Request, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_input_popup_surface_v2::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: ClientId, _object: &ZwpInputPopupSurfaceV2) { + self.alive_tracker.destroy_notify(); + } +} diff --git a/vendor/smithay/src/wayland/input_method/mod.rs b/vendor/smithay/src/wayland/input_method/mod.rs new file mode 100644 index 0000000..79e402b --- /dev/null +++ b/vendor/smithay/src/wayland/input_method/mod.rs @@ -0,0 +1,231 @@ +//! Utilities for input method support +//! +//! This module provides you with utilities to handle input methods, +//! it must be used in conjunction with the text input module to work. +//! +//! ``` +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::input_method::{InputMethodManagerState, InputMethodHandler, PopupSurface}; +//! use smithay::wayland::text_input::{TextInputActivation, TextInputManagerState}; +//! use smithay::reexports::wayland_server::{Display, protocol::wl_surface::WlSurface}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! # use smithay::reexports::wayland_server::Client; +//! use smithay::utils::{Rectangle, Logical}; +//! +//! # struct State { seat_state: SeatState }; +//! +//! impl InputMethodHandler for State { +//! fn new_popup(&mut self, surface: PopupSurface) {} +//! fn dismiss_popup(&mut self, surface: PopupSurface) {} +//! fn popup_repositioned(&mut self, surface: PopupSurface) {} +//! fn parent_geometry(&self, parent: &WlSurface) -> Rectangle { +//! Rectangle::default() +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! let mut seat_state = SeatState::::new(); +//! +//! // implement the required traits +//! impl SeatHandler for State { +//! type KeyboardFocus = WlSurface; +//! type PointerFocus = WlSurface; +//! type TouchFocus = WlSurface; +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! } +//! # impl PointerConstraintsHandler for State {} +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &WlSurface) {} +//! # } +//! +//! impl TextInputActivation for State {} +//! +//! // Add the seat state to your state and create manager globals +//! InputMethodManagerState::new::(&display_handle, |_client| true); +//! // Add text input capabilities, needed for the input method to work +//! TextInputManagerState::new::(&display_handle); +//! +//! ``` + +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, backend::GlobalId, + protocol::wl_surface::WlSurface, +}; + +use wayland_protocols_misc::zwp_input_method_v2::server::{ + zwp_input_method_manager_v2::{self, ZwpInputMethodManagerV2}, + zwp_input_method_v2::ZwpInputMethodV2, +}; + +use crate::{ + input::{Seat, SeatHandler}, + utils::{Logical, Rectangle}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +pub use input_method_handle::{InputMethodHandle, InputMethodUserData}; +pub use input_method_keyboard_grab::{InputMethodKeyboardGrab, InputMethodKeyboardUserData}; +pub use input_method_popup_surface::InputMethodPopupSurfaceUserData; + +use super::text_input::TextInputHandle; + +const MANAGER_VERSION: u32 = 1; + +/// The role of the input method popup. +pub const INPUT_POPUP_SURFACE_ROLE: &str = "zwp_input_popup_surface_v2"; + +mod input_method_handle; +mod input_method_keyboard_grab; +mod input_method_popup_surface; +pub use input_method_popup_surface::{PopupParent, PopupSurface}; + +/// Adds input method popup to compositor state +pub trait InputMethodHandler { + /// Add a popup surface to compositor state. + fn new_popup(&mut self, surface: PopupSurface); + + /// Dismiss a popup surface from the compositor state. + fn dismiss_popup(&mut self, surface: PopupSurface); + + /// Popup location has changed. + fn popup_repositioned(&mut self, surface: PopupSurface); + + /// Sets the parent location so the popup surface can be placed correctly + fn parent_geometry(&self, parent: &WlSurface) -> Rectangle; +} + +/// Extends [Seat] with input method functionality +pub trait InputMethodSeat { + /// Get an input method associated with this seat + fn input_method(&self) -> &InputMethodHandle; +} + +impl InputMethodSeat for Seat { + fn input_method(&self) -> &InputMethodHandle { + let user_data = self.user_data(); + user_data.insert_if_missing(InputMethodHandle::default); + user_data.get::().unwrap() + } +} + +/// Data associated with a InputMethodManager global. +#[allow(missing_debug_implementations)] +pub struct InputMethodManagerGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +/// State of wp misc input method protocol +#[derive(Debug)] +pub struct InputMethodManagerState { + global: GlobalId, +} + +impl InputMethodManagerState { + /// Initialize a text input manager global. + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch>, + D: SeatHandler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = InputMethodManagerGlobalData { + filter: Box::new(filter), + }; + let global = display.create_global::(MANAGER_VERSION, data); + + Self { global } + } + + /// Get the id of ZwpTextInputManagerV3 global + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for InputMethodManagerGlobalData +where + D: Dispatch, + D: Dispatch>, + D: SeatHandler, + D: 'static, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch>, + D: SeatHandler + InputMethodHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _: &ZwpInputMethodManagerV2, + request: zwp_input_method_manager_v2::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_input_method_manager_v2::Request::GetInputMethod { seat, input_method } => { + let seat = Seat::::from_resource(&seat).unwrap(); + + let user_data = seat.user_data(); + user_data.insert_if_missing(TextInputHandle::default); + user_data.insert_if_missing(InputMethodHandle::default); + let handle = user_data.get::().unwrap(); + let text_input_handle = user_data.get::().unwrap(); + text_input_handle.with_focused_text_input(|ti, surface| { + ti.enter(surface); + }); + let keyboard_handle = seat.get_keyboard().unwrap(); + let instance = data_init.init( + input_method, + InputMethodUserData { + handle: handle.clone(), + text_input_handle: text_input_handle.clone(), + keyboard_handle, + popup_geometry_callback: D::parent_geometry, + popup_repositioned: D::popup_repositioned, + new_popup: D::new_popup, + dismiss_popup: D::dismiss_popup, + }, + ); + handle.add_instance(&instance); + } + zwp_input_method_manager_v2::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/keyboard_shortcuts_inhibit/dispatch.rs b/vendor/smithay/src/wayland/keyboard_shortcuts_inhibit/dispatch.rs new file mode 100644 index 0000000..b29d883 --- /dev/null +++ b/vendor/smithay/src/wayland/keyboard_shortcuts_inhibit/dispatch.rs @@ -0,0 +1,150 @@ +use std::{ + collections::hash_map::Entry, + sync::atomic::{self, AtomicBool}, +}; + +use wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::server::{ + zwp_keyboard_shortcuts_inhibit_manager_v1::{self, ZwpKeyboardShortcutsInhibitManagerV1}, + zwp_keyboard_shortcuts_inhibitor_v1::{self, ZwpKeyboardShortcutsInhibitorV1}, +}; +use wayland_server::{ + Dispatch, Resource, + backend::{ClientId, ObjectId}, + protocol::wl_surface::WlSurface, +}; + +use crate::{ + input::{Seat, SeatHandler}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +use super::KeyboardShortcutsInhibitHandler; + +/// User data of [ZwpKeyboardShortcutsInhibitorV1] object +#[derive(Debug)] +pub struct KeyboardShortcutsInhibitorUserData { + /// Seat that is being inhibited + pub(crate) seat: ObjectId, + /// Surface that is inhibiting shortcuts + pub(crate) surface: WlSurface, + pub(crate) is_active: AtomicBool, +} + +impl GlobalDispatch2 for GlobalData +where + D: KeyboardShortcutsInhibitHandler, + D: Dispatch, + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _handle: &wayland_server::DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: KeyboardShortcutsInhibitHandler, + D: SeatHandler, + D: Dispatch, +{ + fn request( + &self, + handler: &mut D, + _client: &wayland_server::Client, + resource: &ZwpKeyboardShortcutsInhibitManagerV1, + request: zwp_keyboard_shortcuts_inhibit_manager_v1::Request, + _dhandle: &wayland_server::DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_keyboard_shortcuts_inhibit_manager_v1::Request::InhibitShortcuts { id, surface, seat } => { + let seat_id = seat.id(); + + if handler + .keyboard_shortcuts_inhibit_state() + .inhibitors + .get(&seat_id) + .map(|list| list.borrow().surface_has_inhibitor(&surface)) + .unwrap_or(false) + { + resource.post_error( + zwp_keyboard_shortcuts_inhibit_manager_v1::Error::AlreadyInhibited, + "Keyboare shortcuts for this surface are already inhibited", + ); + return; + } + + let seat = Seat::::from_resource(&seat).unwrap(); + let seat_data = super::SeatData::get(&seat); + + let inhibitor = data_init.init( + id, + KeyboardShortcutsInhibitorUserData { + seat: seat_id.clone(), + surface, + is_active: AtomicBool::new(false), + }, + ); + + let inhibitor = super::KeyboardShortcutsInhibitor { inhibitor }; + + let list = handler + .keyboard_shortcuts_inhibit_state() + .inhibitors + .entry(seat_id) + .or_insert_with(|| seat_data.borrow().inhibitors.clone()); + + list.borrow_mut().push(inhibitor.clone()); + + handler.new_inhibitor(inhibitor); + } + zwp_keyboard_shortcuts_inhibit_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for KeyboardShortcutsInhibitorUserData +where + D: KeyboardShortcutsInhibitHandler, +{ + fn request( + &self, + _handler: &mut D, + _client: &wayland_server::Client, + _inhibitor: &ZwpKeyboardShortcutsInhibitorV1, + request: zwp_keyboard_shortcuts_inhibitor_v1::Request, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_keyboard_shortcuts_inhibitor_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, handler: &mut D, _client: ClientId, wl_inhibitor: &ZwpKeyboardShortcutsInhibitorV1) { + self.is_active.store(false, atomic::Ordering::Release); + + let state = handler.keyboard_shortcuts_inhibit_state(); + + if let Entry::Occupied(mut entry) = state.inhibitors.entry(self.seat.clone()) { + let inhibitor = entry.get_mut().borrow_mut().remove(wl_inhibitor.id()); + + if entry.get().borrow().is_empty() { + entry.remove(); + } + + if let Some(inhibitor) = inhibitor { + handler.inhibitor_destroyed(inhibitor) + } + } + } +} diff --git a/vendor/smithay/src/wayland/keyboard_shortcuts_inhibit/mod.rs b/vendor/smithay/src/wayland/keyboard_shortcuts_inhibit/mod.rs new file mode 100644 index 0000000..92fcf6f --- /dev/null +++ b/vendor/smithay/src/wayland/keyboard_shortcuts_inhibit/mod.rs @@ -0,0 +1,250 @@ +//! Protocol for inhibiting the compositor keyboard shortcuts +//! +//! This protocol specifies a way for a client to request the compositor +//! to ignore its own keyboard shortcuts for a given seat, +//! so that all key events from that seat get forwarded to a surface. + +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::atomic}; +use wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::server::{ + zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1, + zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1, +}; +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, Resource, + backend::{GlobalId, ObjectId}, + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, +}; + +mod dispatch; +pub use dispatch::KeyboardShortcutsInhibitorUserData; + +use crate::{ + input::{Seat, SeatHandler}, + wayland::GlobalData, +}; + +type SeatId = ObjectId; + +/// List of inhibitors associated with WlSeat +#[derive(Debug, Default)] +struct SeatInhibitors(Vec); + +impl SeatInhibitors { + fn push(&mut self, value: KeyboardShortcutsInhibitor) { + self.0.push(value) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Check if any inhibitor is active + fn is_inhibited(&self) -> bool { + self.0.iter().any(|i| i.is_active()) + } + + fn remove(&mut self, id: ObjectId) -> Option { + self.0 + .iter() + .position(|i| i.inhibitor.id() == id) + .map(|id| self.0.remove(id)) + } + + fn surface_has_inhibitor(&self, surface: &WlSurface) -> bool { + self.inhibitor_for_surface(surface).is_some() + } + + /// Find inhibitor_for WlSurface + fn inhibitor_for_surface(&self, surface: &WlSurface) -> Option<&KeyboardShortcutsInhibitor> { + self.0.iter().find(|i| i.wl_surface() == surface) + } +} + +/// Delegate type for KeyboardShortcutsInhibit global. +#[derive(Debug)] +pub struct KeyboardShortcutsInhibitState { + manager_global: GlobalId, + inhibitors: HashMap>>, +} + +impl KeyboardShortcutsInhibitState { + /// Regiseter new [ZwpKeyboardShortcutsInhibitManagerV1] global + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: 'static, + { + let manager_global = + display.create_global::(1, GlobalData); + Self { + manager_global, + inhibitors: HashMap::new(), + } + } + + /// [ZwpKeyboardShortcutsInhibitManagerV1] GlobalId getter + pub fn global(&self) -> GlobalId { + self.manager_global.clone() + } +} + +/// Context object for keyboard shortcuts inhibitor +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeyboardShortcutsInhibitor { + inhibitor: ZwpKeyboardShortcutsInhibitorV1, +} + +impl KeyboardShortcutsInhibitor { + fn data(&self) -> &KeyboardShortcutsInhibitorUserData { + self.inhibitor.data().unwrap() + } + + fn set_is_active(&self, v: bool) { + self.data().is_active.store(v, atomic::Ordering::Release); + } + + /// Seat that is being inhibited + fn seat_id(&self) -> &SeatId { + &self.data().seat + } + + /// Seat that is being inhibited + pub fn seat(&self, dh: &DisplayHandle) -> Option { + WlSeat::from_id(dh, self.seat_id().clone()).ok() + } + + /// Seat that is being inhibited + #[inline] + pub fn wl_surface(&self) -> &WlSurface { + &self.data().surface + } + + /// Is inhibitor active + pub fn is_active(&self) -> bool { + self.data().is_active.load(atomic::Ordering::Acquire) + } + + /// This method indicates that the shortcut inhibitor is active. + /// + /// When active, the client may receive input events normally reserved by the compositor. + /// + /// Typically this is called when user instructs the compositor to enable inhibitor using any mechanism offered by the compositor, + /// either by accepting new inhibitor or re-enabling already existing inhibitor. + pub fn activate(&self) { + self.inhibitor.active(); + self.set_is_active(true); + } + + /// This event indicates that the shortcuts inhibitor is inactive, normal shortcuts processing is restored by the compositor. + pub fn inactivate(&self) { + self.inhibitor.inactive(); + self.set_is_active(false); + } +} + +#[derive(Debug, Default)] +struct SeatData { + inhibitors: Rc>, +} + +impl SeatData { + fn get(seat: &Seat) -> &RefCell + where + D: SeatHandler, + D: 'static, + { + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::default())); + seat.user_data().get().unwrap() + } + + /// Check if any inhibitor is active + fn is_inhibited(&self) -> bool { + self.inhibitors.borrow().is_inhibited() + } + + /// Find inhibitor for WlSurface + fn inhibitor_for_surface(&self, surface: &WlSurface) -> Option { + self.inhibitors.borrow().inhibitor_for_surface(surface).cloned() + } +} + +/// Seat extension used to check if shortcuts are inhibited +pub trait KeyboardShortcutsInhibitorSeat { + /// Check if keyboard_shortcuts are inhibited + fn keyboard_shortcuts_inhibited(&self) -> bool; + + /// Get inhibitors associated with given WlSurface + /// + /// Can be used to check if certain surface has inhibitor on it + /// ```no_run + /// use smithay::input::Seat; + /// # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; + /// use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat; + /// # use smithay::input::{SeatHandler, SeatState, pointer::CursorImageStatus}; + /// # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; + /// # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; + /// # struct State; + /// # impl CompositorHandler for State { + /// # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } + /// # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } + /// # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} + /// # } + /// # impl SeatHandler for State { + /// # type KeyboardFocus = WlSurface; + /// # type PointerFocus = WlSurface; + /// # type TouchFocus = WlSurface; + /// # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } + /// # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } + /// # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } + /// # } + /// # impl PointerConstraintsHandler for State {} + /// + /// # let wl_surface: WlSurface = todo!(); + /// + /// let seat: Seat = todo!(); + /// + /// if let Some(inhibitor) = seat.keyboard_shortcuts_inhibitor_for_surface(&wl_surface) { + /// dbg!(inhibitor.is_active()); + /// } + /// ``` + fn keyboard_shortcuts_inhibitor_for_surface( + &self, + surface: &WlSurface, + ) -> Option; +} + +impl KeyboardShortcutsInhibitorSeat for Seat +where + D: SeatHandler, + D: 'static, +{ + fn keyboard_shortcuts_inhibited(&self) -> bool { + SeatData::get(self).borrow().is_inhibited() + } + + fn keyboard_shortcuts_inhibitor_for_surface( + &self, + surface: &WlSurface, + ) -> Option { + SeatData::get(self).borrow().inhibitor_for_surface(surface) + } +} + +/// WP Keyboard shortcuts inhibit handler +#[allow(unused_variables)] +pub trait KeyboardShortcutsInhibitHandler { + /// [KeyboardShortcutsInhibitState] getter + fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState; + + /// New keyboard shortcuts inhibitor got created by the client + /// + /// In response to this event compositor can decide if inhibitor should be activated or not, usually based on user decision. + /// You may also postpone activation based on your compositor specific policy. + fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {} + + /// Inhibitor got destroyed + fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {} +} diff --git a/vendor/smithay/src/wayland/mod.rs b/vendor/smithay/src/wayland/mod.rs new file mode 100644 index 0000000..c346308 --- /dev/null +++ b/vendor/smithay/src/wayland/mod.rs @@ -0,0 +1,105 @@ +// Allow in this module because of existing usage +#![allow(clippy::uninlined_format_args)] +//! Protocol-related utilities +//! +//! This module contains several handlers to manage the Wayland protocol +//! and the clients. +//! +//! ## General structure +//! +//! Most utilities provided in this module work in the same way: +//! +//! - A module specific `*State` struct will take the wayland display as argument and +//! insert one or more globals into it through its constructor. +//! - The module-`State` will have to be stored inside your global compositor state. +//! (The same type you parametrized [`wayland_server::Display`] over.) +//! - You need to implement a module-specific `*Handler`-trait for your compositor state. +//! This implementation will be called when wayland events require custom handling. +//! - Call the matching `delegate_*!` macro from smithay on your state to implement +//! some required `wayland_server` traits. +//! - If you want to remove a previously inserted global, just drop the `*State`. +//! +//! ## Provided helpers +//! +//! ### Core functionality +//! +//! The most fundamental module is the [`compositor`] module, which provides the necessary +//! logic to handle the fundamental component by which clients build their windows: surfaces. +//! Following this, the [`shell`] module contains the logic allowing clients to use their +//! surface to build concrete windows with the usual interactions. Different kind of shells +//! exist, but in general you will want to support at least the [`xdg`](shell::xdg) variant, +//! which is the standard used by most applications. +//! +//! Then, the [`seat`] module contains logic related to input handling. These helpers are used +//! to forward input (such as pointer action or keystrokes) to clients, and manage the input +//! focus of clients. Tightly coupled with it is the [`selection`] module, which handles +//! cross-client interactions such as accessing the clipboard, or drag'n'drop actions. +//! +//! The [`shm`] module provides the necessary logic for client to provide buffers defining the +//! contents of their windows using shared memory. This is the main mechanism used by clients +//! that are not hardware accelerated. As a complement, the [`dmabuf`] module provides support +//! hardware-accelerated clients; it is tightly linked to the +//! [`backend::allocator`](crate::backend::allocator) module. +//! +//! The [`output`] module helps forwarding to clients information about the display monitors that +//! are available. This notably plays a key role in HiDPI handling, and more generally notifying +//! clients about whether they are currently visible or not (allowing them to stop drawing if they +//! are not, for example). +//! + +pub mod alpha_modifier; +pub mod background_effect; +pub mod buffer; +pub mod commit_timing; +pub mod compositor; +pub mod content_type; +pub mod cursor_shape; +mod dispatch2; +pub use dispatch2::{Dispatch2, GlobalDispatch2}; +pub mod dmabuf; +#[cfg(feature = "backend_drm")] +pub mod drm_lease; +#[cfg(feature = "backend_drm")] +pub mod drm_syncobj; +pub mod fifo; +pub mod fixes; +pub mod foreign_toplevel_list; +pub mod fractional_scale; +pub mod idle_inhibit; +pub mod idle_notify; +pub mod image_capture_source; +pub mod image_copy_capture; +pub mod input_method; +pub mod keyboard_shortcuts_inhibit; +pub mod output; +pub mod pointer_constraints; +pub mod pointer_gestures; +pub mod pointer_warp; +pub mod presentation; +pub mod relative_pointer; +pub mod seat; +pub mod security_context; +pub mod selection; +pub mod session_lock; +pub mod shell; +pub mod shm; +pub mod single_pixel_buffer; +pub mod socket; +pub mod tablet_manager; +pub mod text_input; +pub mod viewporter; +pub mod virtual_keyboard; +pub mod virtual_pointer; +pub mod xdg_activation; +pub mod xdg_foreign; +pub mod xdg_system_bell; +pub mod xdg_toplevel_icon; +pub mod xdg_toplevel_tag; +#[cfg(feature = "xwayland")] +pub mod xwayland_keyboard_grab; +#[cfg(feature = "xwayland")] +pub mod xwayland_shell; + +/// Empty user-data +#[derive(Debug)] +pub struct GlobalData; diff --git a/vendor/smithay/src/wayland/output/handlers.rs b/vendor/smithay/src/wayland/output/handlers.rs new file mode 100644 index 0000000..0565593 --- /dev/null +++ b/vendor/smithay/src/wayland/output/handlers.rs @@ -0,0 +1,228 @@ +use std::sync::{Arc, atomic::Ordering}; + +use portable_atomic::AtomicF64; +use tracing::{trace, warn, warn_span}; +use wayland_protocols::xdg::xdg_output::zv1::server::{ + zxdg_output_manager_v1::{self, ZxdgOutputManagerV1}, + zxdg_output_v1::{self, ZxdgOutputV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, New, Resource, + protocol::wl_output::{self, Mode as WMode, WlOutput}, +}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2, compositor::CompositorHandler}; + +use super::{Output, OutputHandler, OutputUserData, WlOutputData, xdg::XdgOutput}; + +/* + * Wl Output + */ + +impl GlobalDispatch2 for WlOutputData +where + D: Dispatch, + D: OutputHandler, + D: CompositorHandler, + D: 'static, +{ + fn bind( + &self, + state: &mut D, + _dh: &DisplayHandle, + client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let client_scale = state.client_compositor_state(client).clone_client_scale(); + let output = data_init.init( + resource, + OutputUserData { + output: self.output.downgrade(), + last_client_scale: AtomicF64::new(client_scale.load(Ordering::Acquire)), + client_scale, + }, + ); + + let mut inner = self.output.inner.0.lock().unwrap(); + + let span = warn_span!("output_bind", name = inner.name); + let _enter = span.enter(); + + trace!("New WlOutput global instantiated"); + + if inner.modes.is_empty() { + warn!("Output is used with no modes set"); + } + if inner.current_mode.is_none() { + warn!("Output is used with no current mod set"); + } + if inner.preferred_mode.is_none() { + warn!("Output is used with not preferred mode set"); + } + + inner.send_geometry_to(&output); + + for &mode in &inner.modes { + let mut flags = WMode::empty(); + if Some(mode) == inner.current_mode { + flags |= WMode::Current; + } + if Some(mode) == inner.preferred_mode { + flags |= WMode::Preferred; + } + output.mode(flags, mode.size.w, mode.size.h, mode.refresh); + } + + if output.version() >= 4 { + output.name(inner.name.clone()); + output.description(inner.description.clone()) + } + + if output.version() >= 2 { + output.scale(inner.scale.integer_scale()); + output.done(); + } + + // Send enter for surfaces already on this output. + for surface in &inner.surfaces { + if let Ok(surface) = surface.upgrade() { + if surface.client().as_ref() == Some(client) { + surface.enter(&output); + } + } + } + + inner.instances.push(output.downgrade()); + + drop(inner); + state.output_bound(self.output.clone(), output); + } +} + +impl Dispatch2 for OutputUserData { + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &WlOutput, + _request: wl_output::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } + + fn destroyed(&self, _state: &mut D, _client_id: wayland_server::backend::ClientId, output: &WlOutput) { + if let Some(o) = self.output.upgrade() { + o.inner + .0 + .lock() + .unwrap() + .instances + .retain(|o| o.id() != output.id()); + } + } +} + +/* + * XDG Output + */ + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: CompositorHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + client: &Client, + _resource: &ZxdgOutputManagerV1, + request: zxdg_output_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zxdg_output_manager_v1::Request::GetXdgOutput { + id, + output: wl_output, + } => { + let output = Output::from_resource(&wl_output).unwrap(); + let mut inner = output.inner.0.lock().unwrap(); + + if inner.xdg_output.is_none() { + inner.xdg_output = Some(XdgOutput::new(&inner)); + } + let xdg_output = inner.xdg_output.as_ref().unwrap(); + + let client_scale = state.client_compositor_state(client).clone_client_scale(); + let id = data_init.init( + id, + XdgOutputUserData { + xdg_output: xdg_output.clone(), + last_client_scale: AtomicF64::new(client_scale.load(Ordering::Acquire)), + client_scale, + }, + ); + + xdg_output.add_instance(&id, &wl_output); + } + zxdg_output_manager_v1::Request::Destroy => {} + _ => {} + } + } +} + +/// User data of Xdg Output +#[derive(Debug)] +pub struct XdgOutputUserData { + xdg_output: XdgOutput, + pub(super) last_client_scale: AtomicF64, + pub(super) client_scale: Arc, +} + +impl Dispatch2 for XdgOutputUserData { + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ZxdgOutputV1, + _request: zxdg_output_v1::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } + + fn destroyed( + &self, + _state: &mut D, + _client_id: wayland_server::backend::ClientId, + xdg_output: &ZxdgOutputV1, + ) { + self.xdg_output + .inner + .lock() + .unwrap() + .instances + .retain(|o| o.id() != xdg_output.id()); + } +} diff --git a/vendor/smithay/src/wayland/output/mod.rs b/vendor/smithay/src/wayland/output/mod.rs new file mode 100644 index 0000000..27f1203 --- /dev/null +++ b/vendor/smithay/src/wayland/output/mod.rs @@ -0,0 +1,374 @@ +//! WlOutput advertising capabilities +//! +//! This module provides additional implementations for [`crate::output::Output`] +//! to handle advertising of your compositor's output and their capabilities to your client, +//! as well as mapping your clients output request to your physical outputs. +//! +//! # How to use it +//! +//! After you have instantiated an [`Output`] you need to use [`Output::create_global`] +//! to advertise a new output global to clients. +//! The resulting `GlobalId` can later be destroyed again to stop advertising it +//! without destroying it's state. E.g. in case the matching physical output got disabled at runtime. +//! +//! If you change the properties of your output (if the current resolution mode changes for example), +//! it'll automatically forward any changes to the clients. +//! +//! Additional protocols may piggy-back on this type. +//! E.g. to also advertise an xdg-output for every wl-output you can use +//! [`OutputManagerState::new_with_xdg_output`]. +//! +//! ``` +//! # extern crate wayland_server; +//! # extern crate smithay; +//! use smithay::output::{Output, PhysicalProperties, Scale, Mode, Subpixel}; +//! use smithay::utils::Transform; +//! use smithay::wayland::output::OutputHandler; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! # use smithay::reexports::wayland_server::{Client, protocol::wl_surface::WlSurface}; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! // Create the Output with given name and physical properties. +//! let output = Output::new( +//! "output-0".into(), // the name of this output, +//! PhysicalProperties { +//! size: (200, 150).into(), // dimensions (width, height) in mm +//! subpixel: Subpixel::HorizontalRgb, // subpixel information +//! make: "Screens Inc".into(), // make of the monitor +//! model: "Monitor Ultra".into(), // model of the monitor +//! serial_number: "KL11C28J04XJ".into(), // serial number of the monitor +//! }, +//! ); +//! // create a global, if you want to advertise it to clients +//! let _global = output.create_global::( +//! &display_handle, // the display +//! ); // you can drop the global, if you never intend to destroy it. +//! // Now you can configure it +//! output.change_current_state( +//! Some(Mode { size: (1920, 1080).into(), refresh: 60000 }), // the resolution mode, +//! Some(Transform::Normal), // global screen transformation +//! Some(Scale::Integer(1)), // global screen scaling factor +//! Some((0,0).into()) // output position +//! ); +//! // set the preferred mode +//! output.set_preferred(Mode { size: (1920, 1080).into(), refresh: 60000 }); +//! // add other supported modes +//! output.add_mode(Mode { size: (800, 600).into(), refresh: 60000 }); +//! output.add_mode(Mode { size: (1024, 768).into(), refresh: 60000 }); +//! +//! impl OutputHandler for State {} +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &WlSurface) {} +//! # } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +mod handlers; +pub(crate) mod xdg; + +use std::mem; +use std::sync::{Arc, atomic::Ordering}; + +use crate::{ + output::{Inner, Mode, Output, Scale, Subpixel, WeakOutput}, + utils::iter::new_locked_obj_iter, +}; + +use portable_atomic::AtomicF64; +use tracing::info; +use wayland_protocols::xdg::xdg_output::zv1::server::zxdg_output_manager_v1::ZxdgOutputManagerV1; +use wayland_protocols::xdg::xdg_output::zv1::server::zxdg_output_v1::ZxdgOutputV1; +use wayland_server::{ + Client, DisplayHandle, GlobalDispatch, Resource, + backend::{ClientId, GlobalId}, + protocol::{ + wl_output::{Mode as WMode, Subpixel as WlSubpixel, Transform, WlOutput}, + wl_surface, + }, +}; + +use crate::{ + utils::{Logical, Point}, + wayland::GlobalData, +}; + +pub use self::handlers::XdgOutputUserData; + +/// State of Smithay output manager +#[derive(Debug, Default)] +pub struct OutputManagerState { + xdg_output_manager: Option, +} + +/// Internal data of a wl_output global +#[derive(Debug)] +pub struct WlOutputData { + output: Output, +} + +/// Events initiated by the clients interacting with outputs +pub trait OutputHandler { + /// A client bound a new `wl_output` instance. + fn output_bound(&mut self, _output: Output, _wl_output: WlOutput) {} +} + +impl OutputManagerState { + /// Create new output manager + pub fn new() -> Self { + Self { + xdg_output_manager: None, + } + } + + /// Create new output manager with xdg output support + pub fn new_with_xdg_output(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: GlobalDispatch, + D: 'static, + { + let xdg_output_manager = display.create_global::(3, GlobalData); + + Self { + xdg_output_manager: Some(xdg_output_manager), + } + } + + /// Get global id of xdg output manager + pub fn xdg_output_manager_global(&self) -> Option { + self.xdg_output_manager.clone() + } +} + +/// User data for WlOutput +#[derive(Debug)] +pub struct OutputUserData { + pub(crate) output: WeakOutput, + last_client_scale: AtomicF64, + client_scale: Arc, +} + +impl Clone for OutputUserData { + fn clone(&self) -> Self { + OutputUserData { + output: self.output.clone(), + last_client_scale: AtomicF64::new(self.last_client_scale.load(Ordering::Acquire)), + client_scale: self.client_scale.clone(), + } + } +} + +impl Inner { + fn send_geometry_to(&self, output: &WlOutput) { + output.geometry( + self.location.x, + self.location.y, + self.physical.size.w, + self.physical.size.h, + self.physical.subpixel.into(), + self.physical.make.clone(), + self.physical.model.clone(), + self.transform.into(), + ); + } +} + +impl From for WlSubpixel { + #[inline] + fn from(s: Subpixel) -> Self { + match s { + Subpixel::HorizontalBgr => WlSubpixel::HorizontalBgr, + Subpixel::HorizontalRgb => WlSubpixel::HorizontalRgb, + Subpixel::None => WlSubpixel::None, + Subpixel::Unknown => WlSubpixel::Unknown, + Subpixel::VerticalBgr => WlSubpixel::VerticalBgr, + Subpixel::VerticalRgb => WlSubpixel::VerticalRgb, + } + } +} + +impl Output { + /// Create a new output global. + /// + /// The global is directly registered into the event loop, and this function + /// returns the global handle in case you wish to remove this global in the future. + /// + /// Calling this function multiple times without destroying the global in between, + /// will result in multiple globals, meaning the output will be advertised to clients + /// multiple times. + pub fn create_global(&self, display: &DisplayHandle) -> GlobalId + where + D: GlobalDispatch, + D: 'static, + { + info!(output = self.name(), "Creating new wl_output"); + self.inner.0.lock().unwrap().handle = Some(display.backend_handle().downgrade()); + display.create_global::(4, WlOutputData { output: self.clone() }) + } + + /// Attempt to retrieve a [`Output`] from an existing resource + pub fn from_resource(output: &WlOutput) -> Option { + output.data::().and_then(|ud| ud.output.upgrade()) + } + + pub(crate) fn wl_change_current_state( + &self, + new_mode: Option, + new_transform: Option, + new_scale: Option, + new_location: Option>, + ) { + let inner = self.inner.0.lock().unwrap(); + // XdgOutput has to be updated before WlOutput + // Because WlOutput::done() has to always be called last + if let Some(xdg_output) = inner.xdg_output.as_ref() { + xdg_output.change_current_state(new_mode, new_scale, new_location, new_transform); + } + + let mut flags = WMode::Current; + if inner.preferred_mode == new_mode { + flags |= WMode::Preferred; + } + + for output in &inner.instances { + let Ok(output) = output.upgrade() else { + continue; + }; + + let data = output.data::().unwrap(); + let client_scale = data.client_scale.load(Ordering::Acquire); + let scale_changed = client_scale != data.last_client_scale.swap(client_scale, Ordering::AcqRel); + + if let Some(mode) = new_mode { + output.mode(flags, mode.size.w, mode.size.h, mode.refresh); + } + if new_transform.is_some() || new_location.is_some() { + inner.send_geometry_to(&output); + } + if (new_scale.is_some() || scale_changed) && output.version() >= 2 { + let scale = (inner.scale.integer_scale() as f64 / client_scale).max(1.).ceil() as i32; + output.scale(scale); + } + if output.version() >= 2 { + output.done(); + } + } + } + + /// Check is given [`wl_output`](WlOutput) instance is managed by this [`Output`]. + pub fn owns(&self, output: &WlOutput) -> bool { + self.inner + .0 + .lock() + .unwrap() + .instances + .iter() + .any(|o| o.id() == output.id()) + } + + /// Check if given [`xdg_output`](ZxdgOutputV1) instance is managed by this [`Output`]. + pub fn owns_xdg_output(&self, output: &ZxdgOutputV1) -> bool { + self.inner + .0 + .lock() + .unwrap() + .xdg_output + .as_ref() + .map(|xdg_output| { + xdg_output + .inner + .lock() + .unwrap() + .instances + .iter() + .any(|o| o.id() == output.id()) + }) + .unwrap_or(false) + } + + /// This function returns all managed [WlOutput] matching the provided [Client] + pub fn client_outputs<'a>(&'a self, client: &Client) -> impl Iterator + 'a { + self.client_outputs_internal(client.id()) + } + + fn client_outputs_internal(&self, client: ClientId) -> impl Iterator + '_ { + let guard = self.inner.0.lock().unwrap(); + + new_locked_obj_iter(guard, client, |inner| inner.instances.iter()) + } + + /// Sends `wl_surface.enter` for the provided surface + /// with the matching client output + #[profiling::function] + pub fn enter(&self, surface: &wl_surface::WlSurface) { + let mut inner = self.inner.0.lock().unwrap(); + if inner.surfaces.insert(surface.downgrade()) { + let client = inner + .handle + .as_ref() + .and_then(|handle| handle.upgrade()) + .and_then(|handle| handle.get_client(surface.id()).ok()); + drop(inner); + + if let Some(client) = client { + for output in self.client_outputs_internal(client) { + surface.enter(&output); + } + } + } + } + + /// Sends `wl_surface.leave` for the provided surface + /// with the matching client output + #[profiling::function] + pub fn leave(&self, surface: &wl_surface::WlSurface) { + let mut inner = self.inner.0.lock().unwrap(); + if inner.surfaces.remove(&surface.downgrade()) { + let client = inner + .handle + .as_ref() + .and_then(|handle| handle.upgrade()) + .and_then(|handle| handle.get_client(surface.id()).ok()); + drop(inner); + + if let Some(client) = client { + for output in self.client_outputs_internal(client) { + surface.leave(&output); + } + } + } + } + + /// Sends `wl_surface.leave` for all surfaces on output + pub fn leave_all(&self) { + let mut inner = self.inner.0.lock().unwrap(); + #[allow(clippy::mutable_key_type)] + let surfaces = mem::take(&mut inner.surfaces); + let Some(handle) = inner.handle.as_ref().and_then(|handle| handle.upgrade()) else { + return; + }; + drop(inner); + + for surface in surfaces { + let Ok(surface) = surface.upgrade() else { + continue; + }; + + if let Ok(client) = handle.get_client(surface.id()) { + for output in self.client_outputs_internal(client) { + surface.leave(&output); + } + } + } + } + + pub(crate) fn cleanup_surfaces(&self) { + let mut inner = self.inner.0.lock().unwrap(); + inner.surfaces.retain(|s| s.is_alive()); + } +} diff --git a/vendor/smithay/src/wayland/output/xdg.rs b/vendor/smithay/src/wayland/output/xdg.rs new file mode 100644 index 0000000..84e2096 --- /dev/null +++ b/vendor/smithay/src/wayland/output/xdg.rs @@ -0,0 +1,149 @@ +//! XDG Output advertising capabilities +//! +//! This protocol is meant for describing outputs in a way +//! which is more in line with the concept of an output on desktop oriented systems. + +use std::sync::{Arc, Mutex, atomic::Ordering}; + +use tracing::trace; +use wayland_protocols::xdg::xdg_output::zv1::server::zxdg_output_v1::ZxdgOutputV1; +use wayland_server::{Resource, Weak, protocol::wl_output::WlOutput}; + +use crate::utils::{Logical, Physical, Point, Size, Transform}; + +use super::{Mode, OutputUserData, Scale, XdgOutputUserData}; + +#[derive(Debug)] +pub(crate) struct Inner { + name: String, + description: String, + pub(super) logical_position: Point, + + pub(super) physical_size: Option>, + pub(super) scale: Scale, + transform: Transform, + + pub instances: Vec>, +} + +#[derive(Debug, Clone)] +pub(crate) struct XdgOutput { + pub(crate) inner: Arc>, +} + +impl XdgOutput { + pub(super) fn new(output: &super::Inner) -> Self { + trace!(name = output.name, "Creating new xdg_output"); + + let physical_size = output.current_mode.map(|mode| mode.size); + + Self { + inner: Arc::new(Mutex::new(Inner { + name: output.name.clone(), + description: output.description.clone(), + logical_position: output.location, + + physical_size, + scale: output.scale, + transform: output.transform, + + instances: Vec::new(), + })), + } + } + + pub(super) fn add_instance(&self, xdg_output: &ZxdgOutputV1, wl_output: &WlOutput) { + let mut inner = self.inner.lock().unwrap(); + let client_scale = wl_output + .data::() + .unwrap() + .client_scale + .load(Ordering::Acquire); + + let logical_position = inner.logical_position.to_client_precise_round(client_scale); + xdg_output.logical_position(logical_position.x, logical_position.y); + + if let Some(size) = inner.physical_size { + let logical_size = size + .to_f64() + .to_logical(inner.scale.fractional_scale()) + .to_client(client_scale) + .to_i32_round(); + let transformed_size = inner.transform.transform_size(logical_size); + xdg_output.logical_size(transformed_size.w, transformed_size.h); + } + + if xdg_output.version() >= 2 { + xdg_output.name(inner.name.clone()); + xdg_output.description(inner.description.clone()); + } + + // xdg_output.done() is deprecated since version 3 + if xdg_output.version() < 3 { + xdg_output.done(); + } + + wl_output.done(); + + inner.instances.push(xdg_output.downgrade()); + } + + pub(super) fn change_current_state( + &self, + new_mode: Option, + new_scale: Option, + new_location: Option>, + new_transform: Option>, + ) { + let mut output = self.inner.lock().unwrap(); + + let new_transform = new_transform.map(|x| x.into()); + + if let Some(new_mode) = new_mode { + output.physical_size = Some(new_mode.size); + } + if let Some(new_scale) = new_scale { + output.scale = new_scale; + } + if let Some(new_location) = new_location { + output.logical_position = new_location; + } + if let Some(new_transform) = new_transform { + output.transform = new_transform; + } + + for instance in output.instances.iter() { + let Ok(instance) = instance.upgrade() else { + continue; + }; + + let data = instance.data::().unwrap(); + let client_scale = data.client_scale.load(Ordering::Acquire); + let scale_changed = client_scale != data.last_client_scale.swap(client_scale, Ordering::AcqRel); + + if new_mode.is_some() || new_scale.is_some() || new_transform.is_some() || scale_changed { + if let Some(size) = output.physical_size { + let logical_size = size + .to_f64() + .to_logical(output.scale.fractional_scale()) + .to_client(client_scale) + .to_i32_round(); + let transformed_size = output.transform.transform_size(logical_size); + instance.logical_size(transformed_size.w, transformed_size.h); + } + } + + if new_location.is_some() || scale_changed { + let logical_position = output.logical_position.to_client_precise_round(client_scale); + instance.logical_position(logical_position.x, logical_position.y); + } + + // xdg_output.done() is deprecated since version 3 + if instance.version() < 3 { + instance.done(); + } + + // No need for wl_output.done() here, it will be called by caller (super::Output::change_current_state) + } + } +} diff --git a/vendor/smithay/src/wayland/pointer_constraints.rs b/vendor/smithay/src/wayland/pointer_constraints.rs new file mode 100644 index 0000000..5526445 --- /dev/null +++ b/vendor/smithay/src/wayland/pointer_constraints.rs @@ -0,0 +1,597 @@ +//! Protocol for confining the pointer. +//! +//! This provides a way for the client to request that the pointer is confined to a region or +//! locked in place. +use std::{ + collections::{HashMap, hash_map}, + ops, + sync::{ + Mutex, Weak, + atomic::{AtomicBool, Ordering}, + }, +}; + +use wayland_protocols::wp::pointer_constraints::zv1::server::{ + zwp_confined_pointer_v1::{self, ZwpConfinedPointerV1}, + zwp_locked_pointer_v1::{self, ZwpLockedPointerV1}, + zwp_pointer_constraints_v1::{self, Lifetime, ZwpPointerConstraintsV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, backend::GlobalId, + protocol::wl_surface::WlSurface, +}; + +use super::compositor::{self, RegionAttributes}; +use crate::{ + input::{ + SeatHandler, + pointer::{PointerHandle, PointerRc}, + }, + utils::{Logical, Point}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2, seat::PointerUserData}, +}; + +const VERSION: u32 = 1; + +/// Reason for Constraint remove +#[derive(Debug)] +pub enum ConstraintRemove { + /// Client call destroy + Destroyed(PointerConstraint), + /// Compositor pointer leave surface + PointerLeave(Option), +} + +/// Handler for pointer constraints +pub trait PointerConstraintsHandler: SeatHandler { + /// Pointer lock or confinement constraint created for `pointer` on `surface` + /// + /// Use [`with_pointer_constraint`] to access the constraint. + fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle) {} + + /// Pointer constraint removed for `pointer` on `surface` + /// + /// Use [`with_pointer_constraint`] to access the constraint. + fn remove_constraint( + &mut self, + _surface: &WlSurface, + _pointer: &PointerHandle, + _constraint_remove: ConstraintRemove, + ) { + } + + /// The client holding a LockedPointer has committed a cursor position hint. + /// + /// This is emitted upon a surface commit if the cursor position hint has been updated. + /// + /// Use [`with_pointer_constraint`] to access the constraint and check if it is active. + fn cursor_position_hint( + &mut self, + _surface: &WlSurface, + _pointer: &PointerHandle, + _location: Point, + ) { + } +} + +/// Constraint confining pointer to a region of the surface +#[derive(Debug)] +pub struct ConfinedPointer { + handle: zwp_confined_pointer_v1::ZwpConfinedPointerV1, + region: Option, + pending_region: Option, + lifetime: WEnum, + active: AtomicBool, +} + +impl ConfinedPointer { + /// Region in which to confine the pointer + pub fn region(&self) -> Option<&RegionAttributes> { + self.region.as_ref() + } +} + +/// Constraint locking pointer in place +#[derive(Debug)] +pub struct LockedPointer { + handle: zwp_locked_pointer_v1::ZwpLockedPointerV1, + region: Option, + pending_region: Option, + lifetime: WEnum, + cursor_position_hint: Option>, + pending_cursor_position_hint: Option>, + active: AtomicBool, +} + +impl LockedPointer { + /// Region in which to activate the lock + pub fn region(&self) -> Option<&RegionAttributes> { + self.region.as_ref() + } + + /// Position the client is rendering a cursor, if any + pub fn cursor_position_hint(&self) -> Option> { + self.cursor_position_hint + } +} + +/// A constraint imposed on the pointer instance +#[derive(Debug)] +pub enum PointerConstraint { + /// Pointer is confined to a region of the surface + Confined(ConfinedPointer), + /// Pointer is locked in place + Locked(LockedPointer), +} + +/// A reference to a pointer constraint that can be activated or deactivated. +/// +/// The derefs to `[PointerConstraint]`. +#[derive(Debug)] +pub struct PointerConstraintRef<'a, D: SeatHandler + 'static> { + entry: hash_map::OccupiedEntry<'a, PointerHandle, PointerConstraint>, +} + +impl ops::Deref for PointerConstraintRef<'_, D> { + type Target = PointerConstraint; + + fn deref(&self) -> &Self::Target { + self.entry.get() + } +} + +impl PointerConstraintRef<'_, D> { + /// Send `locked`/`unlocked` + /// + /// This is not sent automatically since compositors may have different + /// policies about when to allow and activate constraints. + pub fn activate(&self) { + match self.entry.get() { + PointerConstraint::Confined(confined) => { + if !confined.active.swap(true, Ordering::SeqCst) { + confined.handle.confined(); + } + } + PointerConstraint::Locked(locked) => { + if !locked.active.swap(true, Ordering::SeqCst) { + locked.handle.locked(); + } + } + } + } + + /// Send `unlocked`/`unconfined` + /// + /// For oneshot constraints, will destroy the constraint. + /// + /// This is sent automatically when the surface loses pointer focus, but + /// may also be invoked while the surface is focused. + pub fn deactivate(self) { + let deactivated = match self.entry.get() { + PointerConstraint::Confined(confined) => { + if confined.active.swap(false, Ordering::SeqCst) { + confined.handle.unconfined(); + true + } else { + false + } + } + PointerConstraint::Locked(locked) => { + if locked.active.swap(false, Ordering::SeqCst) { + locked.handle.unlocked(); + true + } else { + false + } + } + }; + + if deactivated && self.lifetime() == WEnum::Value(Lifetime::Oneshot) { + self.entry.remove_entry(); + } + } +} + +impl PointerConstraint { + /// Constraint is active + pub fn is_active(&self) -> bool { + match self { + PointerConstraint::Confined(confined) => &confined.active, + PointerConstraint::Locked(locked) => &locked.active, + } + .load(Ordering::SeqCst) + } + + /// Region in which to lock or confine the pointer + pub fn region(&self) -> Option<&RegionAttributes> { + match self { + PointerConstraint::Confined(confined) => confined.region(), + PointerConstraint::Locked(locked) => locked.region(), + } + } + + fn lifetime(&self) -> WEnum { + match self { + PointerConstraint::Confined(confined) => confined.lifetime, + PointerConstraint::Locked(locked) => locked.lifetime, + } + } + + /// Commits the pending state of the constraint, and returns the cursor position hint if it has changed. + fn commit(&mut self) -> Option> { + match self { + Self::Confined(confined) => { + confined.region.clone_from(&confined.pending_region); + None + } + Self::Locked(locked) => { + locked.region.clone_from(&locked.pending_region); + locked.pending_cursor_position_hint.take().inspect(|hint| { + locked.cursor_position_hint = Some(*hint); + }) + } + } + } +} + +/// Pointer constraints state. +#[derive(Debug)] +pub struct PointerConstraintsState { + global: GlobalId, +} + +impl PointerConstraintsState { + /// Create a new pointer constraints global + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch>, + D: Dispatch>, + D: SeatHandler, + D: 'static, + { + let global = display.create_global::(VERSION, GlobalData); + + Self { global } + } + + /// Get the id of ZwpPointerConstraintsV1 global + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct PointerConstraintUserData { + surface: WlSurface, + arc: Weak>, +} + +struct PointerConstraintData { + constraints: HashMap, PointerConstraint>, +} + +// TODO Public method to get current constraints for surface/seat +/// Get constraint for surface and pointer, if any +pub fn with_pointer_constraint< + D: SeatHandler + 'static, + T, + F: FnOnce(Option>) -> T, +>( + surface: &WlSurface, + pointer: &PointerHandle, + f: F, +) -> T { + with_constraint_data::(surface, |data| { + let constraint = data.and_then(|data| match data.constraints.entry(pointer.clone()) { + hash_map::Entry::Occupied(entry) => Some(PointerConstraintRef { entry }), + hash_map::Entry::Vacant(_) => None, + }); + f(constraint) + }) +} + +fn commit_hook( + state: &mut D, + _dh: &DisplayHandle, + surface: &WlSurface, +) { + // `with_constraint_data` locks the pointer constraints, + // so we collect the hints first into a Vec, then release the mutex + // and only once the mutex is released, we call the handler method. + // + // This is to avoid deadlocks when the handler method might try to access the constraints again. + // It's not a hypothetical, it bit me while implementing the position hint functionality. + let position_hints = with_constraint_data::(surface, |data| { + let data = data.unwrap(); + data.constraints + .iter_mut() + .filter_map(|(pointer, constraint)| constraint.commit().map(|hint| (pointer.clone(), hint))) + .collect::>() + }); + + for (pointer, hint) in position_hints { + state.cursor_position_hint(surface, &pointer, hint); + } +} + +/// Get `PointerConstraintData` associated with a surface, if any. +fn with_constraint_data< + D: SeatHandler + 'static, + T, + F: FnOnce(Option<&mut PointerConstraintData>) -> T, +>( + surface: &WlSurface, + f: F, +) -> T { + compositor::with_states(surface, |states| { + let data = states.data_map.get::>>(); + if let Some(data) = data { + f(Some(&mut data.lock().unwrap())) + } else { + f(None) + } + }) +} + +/// Add constraint for surface, or raise protocol error if one exists +fn add_constraint( + pointer_constraints: &ZwpPointerConstraintsV1, + surface: &WlSurface, + pointer: &PointerHandle, + constraint: PointerConstraint, +) { + let mut added = false; + compositor::with_states(surface, |states| { + added = states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(PointerConstraintData:: { + constraints: HashMap::new(), + }) + }); + let data = states.data_map.get::>>().unwrap(); + let mut data = data.lock().unwrap(); + + if data.constraints.contains_key(pointer) { + pointer_constraints.post_error( + zwp_pointer_constraints_v1::Error::AlreadyConstrained, + "pointer constraint already exists for surface and seat", + ); + } else { + data.constraints.insert(pointer.clone(), constraint); + } + }); + + if added { + compositor::add_post_commit_hook(surface, commit_hook::); + } +} + +fn remove_constraint( + state: &mut D, + surface: &WlSurface, + pointer: &PointerHandle, +) { + let is_removed = with_constraint_data::(surface, |data| { + if let Some(data) = data { + if let Some(constraint) = data.constraints.remove(pointer) { + return Some(constraint); + } + } + None + }); + + if let Some(constraint) = is_removed { + state.remove_constraint(surface, pointer, ConstraintRemove::Destroyed(constraint)); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch>, + D: Dispatch>, + D: SeatHandler, + D: PointerConstraintsHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + pointer_constraints: &ZwpPointerConstraintsV1, + request: zwp_pointer_constraints_v1::Request, + _dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_pointer_constraints_v1::Request::LockPointer { + id, + surface, + pointer, + region, + lifetime, + } => { + let region = region.as_ref().map(compositor::get_region_attributes); + let arc = pointer.data::>().unwrap().arc.clone(); + let handle = data_init.init( + id, + PointerConstraintUserData { + surface: surface.clone(), + arc: arc.clone(), + }, + ); + if let Some(arc) = arc.upgrade() { + let pointer = PointerHandle { arc }; + add_constraint( + pointer_constraints, + &surface, + &pointer, + PointerConstraint::Locked(LockedPointer { + handle, + region: region.clone(), + pending_region: region, + lifetime, + cursor_position_hint: None, + pending_cursor_position_hint: None, + active: AtomicBool::new(false), + }), + ); + state.new_constraint(&surface, &pointer); + } + } + zwp_pointer_constraints_v1::Request::ConfinePointer { + id, + surface, + pointer, + region, + lifetime, + } => { + let region = region.as_ref().map(compositor::get_region_attributes); + let arc = pointer.data::>().unwrap().arc.clone(); + let handle = data_init.init( + id, + PointerConstraintUserData { + surface: surface.clone(), + arc: arc.clone(), + }, + ); + if let Some(arc) = arc.upgrade() { + let pointer = PointerHandle { arc }; + add_constraint( + pointer_constraints, + &surface, + &pointer, + PointerConstraint::Confined(ConfinedPointer { + handle, + region: region.clone(), + pending_region: region, + lifetime, + active: AtomicBool::new(false), + }), + ); + state.new_constraint(&surface, &pointer); + } + } + zwp_pointer_constraints_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch + SeatHandler + 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for PointerConstraintUserData +where + D: SeatHandler, + D: PointerConstraintsHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _confined_pointer: &ZwpConfinedPointerV1, + request: zwp_confined_pointer_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let Some(pointer) = &self.arc.upgrade().map(|arc| PointerHandle { arc }) else { + return; + }; + + match request { + zwp_confined_pointer_v1::Request::SetRegion { region } => { + with_pointer_constraint(&self.surface, pointer, |constraint| { + if let Some(PointerConstraint::Confined(confined)) = + constraint.map(|x| x.entry.into_mut()) + { + confined.pending_region = region.as_ref().map(compositor::get_region_attributes); + } + }); + } + zwp_confined_pointer_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + _resource: &ZwpConfinedPointerV1, + ) { + let Some(pointer) = &self.arc.upgrade().map(|arc| PointerHandle { arc }) else { + return; + }; + + remove_constraint(state, &self.surface, pointer); + } +} + +impl Dispatch2 for PointerConstraintUserData +where + D: SeatHandler, + D: PointerConstraintsHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _locked_pointer: &ZwpLockedPointerV1, + request: zwp_locked_pointer_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let Some(pointer) = &self.arc.upgrade().map(|arc| PointerHandle { arc }) else { + return; + }; + + match request { + zwp_locked_pointer_v1::Request::SetCursorPositionHint { surface_x, surface_y } => { + with_pointer_constraint(&self.surface, pointer, |constraint| { + if let Some(PointerConstraint::Locked(locked)) = constraint.map(|x| x.entry.into_mut()) { + locked.pending_cursor_position_hint = Some((surface_x, surface_y).into()); + } + }); + } + zwp_locked_pointer_v1::Request::SetRegion { region } => { + with_pointer_constraint(&self.surface, pointer, |constraint| { + if let Some(PointerConstraint::Locked(locked)) = constraint.map(|x| x.entry.into_mut()) { + locked.pending_region = region.as_ref().map(compositor::get_region_attributes); + } + }); + } + zwp_locked_pointer_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: wayland_server::backend::ClientId, + _resource: &ZwpLockedPointerV1, + ) { + let Some(pointer) = &self.arc.upgrade().map(|arc| PointerHandle { arc }) else { + return; + }; + + remove_constraint(state, &self.surface, pointer); + } +} diff --git a/vendor/smithay/src/wayland/pointer_gestures.rs b/vendor/smithay/src/wayland/pointer_gestures.rs new file mode 100644 index 0000000..8ad6e40 --- /dev/null +++ b/vendor/smithay/src/wayland/pointer_gestures.rs @@ -0,0 +1,576 @@ +//! Utilities for pointer gestures support +//! +//! This protocol allows clients to receive touchpad gestures. Use the following methods to send +//! gesture events to any respective objects created by the client: +//! +//! 1. Swipe gestures +//! - [`PointerHandle::gesture_swipe_begin`] +//! - [`PointerHandle::gesture_swipe_update`] +//! - [`PointerHandle::gesture_swipe_end`] +//! 2. Pinch gesture: +//! - [`PointerHandle::gesture_pinch_begin`] +//! - [`PointerHandle::gesture_pinch_update`] +//! - [`PointerHandle::gesture_pinch_end`] +//! 3. Hold gesture: +//! - [`PointerHandle::gesture_hold_begin`] +//! - [`PointerHandle::gesture_hold_end`] +//! +//! ``` +//! extern crate wayland_server; +//! extern crate smithay; +//! +//! use smithay::wayland::pointer_gestures::PointerGesturesState; +//! # use smithay::backend::input::{InputTime, KeyState}; +//! # use smithay::input::{ +//! # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, +//! # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, +//! # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, +//! # GestureHoldBeginEvent, GestureHoldEndEvent}, +//! # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, +//! # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, +//! # Seat, SeatHandler, SeatState, +//! # }; +//! # use smithay::utils::{IsAlive, Serial}; +//! +//! # #[derive(Debug, Clone, PartialEq)] +//! # struct Target; +//! # impl IsAlive for Target { +//! # fn alive(&self) -> bool { true } +//! # } +//! # impl PointerTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} +//! # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} +//! # fn frame(&self, seat: &Seat, data: &mut State) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} +//! # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} +//! # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} +//! # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} +//! # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} +//! # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} +//! # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} +//! # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} +//! # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} +//! # } +//! # impl KeyboardTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} +//! # fn key( +//! # &self, +//! # seat: &Seat, +//! # data: &mut State, +//! # key: KeysymHandle<'_>, +//! # state: KeyState, +//! # serial: Serial, +//! # time: InputTime, +//! # ) {} +//! # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} +//! # } +//! # impl TouchTarget for Target { +//! # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} +//! # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} +//! # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } +//! # } +//! # struct State { +//! # seat_state: SeatState, +//! # }; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # impl SeatHandler for State { +//! # type KeyboardFocus = Target; +//! # type PointerFocus = Target; +//! # type TouchFocus = Target; +//! # +//! # fn seat_state(&mut self) -> &mut SeatState { +//! # &mut self.seat_state +//! # } +//! # } +//! let state = PointerGesturesState::new::(&display.handle()); +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::sync::{Arc, Mutex, Weak, atomic::Ordering}; + +use portable_atomic::AtomicF64; +use wayland_protocols::wp::pointer_gestures::zv1::server::{ + zwp_pointer_gesture_hold_v1::{self, ZwpPointerGestureHoldV1}, + zwp_pointer_gesture_pinch_v1::{self, ZwpPointerGesturePinchV1}, + zwp_pointer_gesture_swipe_v1::{self, ZwpPointerGestureSwipeV1}, + zwp_pointer_gestures_v1::{self, ZwpPointerGesturesV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId}, + protocol::wl_surface::WlSurface, +}; + +#[cfg(doc)] +use crate::input::pointer::PointerHandle; +use crate::{ + backend::input::InputTime, + input::{ + SeatHandler, + pointer::{ + GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, + GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + PointerRc, + }, + }, + utils::{SERIAL_COUNTER, Serial}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2, seat::PointerUserData}, +}; + +const MANAGER_VERSION: u32 = 3; + +#[derive(Debug, Default)] +pub(crate) struct WpPointerGesturePointerHandle { + known_swipe_gestures: Mutex>, + known_pinch_gestures: Mutex>, + known_hold_gestures: Mutex>, +} + +impl WpPointerGesturePointerHandle { + fn new_swipe_gesture(&self, gesture: ZwpPointerGestureSwipeV1) { + self.known_swipe_gestures.lock().unwrap().push(gesture); + } + + fn new_pinch_gesture(&self, gesture: ZwpPointerGesturePinchV1) { + self.known_pinch_gestures.lock().unwrap().push(gesture); + } + + fn new_hold_gesture(&self, gesture: ZwpPointerGestureHoldV1) { + self.known_hold_gestures.lock().unwrap().push(gesture); + } + + pub(super) fn leave( + &self, + surface: &WlSurface, + serial: Serial, + time: InputTime, + ) { + self.for_each_focused_swipe_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().take(); + if ongoing.is_some() { + // Cancel the ongoing gesture. + gesture.end(serial.into(), time.millis(), 1); + } + }); + self.for_each_focused_pinch_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().take(); + if ongoing.is_some() { + // Cancel the ongoing gesture. + gesture.end(serial.into(), time.millis(), 1); + } + }); + self.for_each_focused_hold_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().take(); + if ongoing.is_some() { + // Cancel the ongoing gesture. + gesture.end(serial.into(), time.millis(), 1); + } + }); + } + + pub(super) fn gesture_swipe_begin( + &self, + surface: &WlSurface, + event: &GestureSwipeBeginEvent, + ) { + self.for_each_focused_swipe_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().replace(surface.clone()); + if ongoing.is_some() { + // Cancel an ongoing gesture for a different surface. + gesture.end(event.serial.into(), event.time.millis(), 1); + } + gesture.begin(event.serial.into(), event.time.millis(), surface, event.fingers); + }); + } + + pub(super) fn gesture_swipe_update( + &self, + surface: &WlSurface, + event: &GestureSwipeUpdateEvent, + ) { + self.for_each_focused_swipe_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let mut ongoing = data.in_progress_on.lock().unwrap(); + // Check that the ongoing gesture is for this surface. + if ongoing.as_ref() == Some(surface) { + let client_scale = data.client_scale.load(Ordering::Acquire); + let delta = event.delta.to_client(client_scale); + gesture.update(event.time.millis(), delta.x, delta.y); + } else if ongoing.take().is_some() { + // If it was for a different surface, cancel it. + gesture.end(SERIAL_COUNTER.next_serial().into(), event.time.millis(), 1); + } + }); + } + + pub(super) fn gesture_swipe_end( + &self, + surface: &WlSurface, + event: &GestureSwipeEndEvent, + ) { + self.for_each_focused_swipe_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().take(); + // Check if the gesture was ongoing. + if ongoing.is_some() { + let cancelled = if ongoing.as_ref() == Some(surface) { + event.cancelled + } else { + // If the gesture was ongoing for any other surface then cancel it. + true + }; + gesture.end(event.serial.into(), event.time.millis(), cancelled.into()); + } + }); + } + + pub(super) fn gesture_pinch_begin( + &self, + surface: &WlSurface, + event: &GesturePinchBeginEvent, + ) { + self.for_each_focused_pinch_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().replace(surface.clone()); + if ongoing.is_some() { + // Cancel an ongoing gesture for a different surface. + gesture.end(event.serial.into(), event.time.millis(), 1); + } + gesture.begin(event.serial.into(), event.time.millis(), surface, event.fingers); + }); + } + + pub(super) fn gesture_pinch_update( + &self, + surface: &WlSurface, + event: &GesturePinchUpdateEvent, + ) { + self.for_each_focused_pinch_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let mut ongoing = data.in_progress_on.lock().unwrap(); + // Check that the ongoing gesture is for this surface. + if ongoing.as_ref() == Some(surface) { + let client_scale = data.client_scale.load(Ordering::Acquire); + let delta = event.delta.to_client(client_scale); + gesture.update(event.time.millis(), delta.x, delta.y, event.scale, event.rotation); + } else if ongoing.take().is_some() { + // If it was for a different surface, cancel it. + gesture.end(SERIAL_COUNTER.next_serial().into(), event.time.millis(), 1); + } + }); + } + + pub(super) fn gesture_pinch_end( + &self, + surface: &WlSurface, + event: &GesturePinchEndEvent, + ) { + self.for_each_focused_pinch_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().take(); + // Check if the gesture was ongoing. + if ongoing.is_some() { + let cancelled = if ongoing.as_ref() == Some(surface) { + event.cancelled + } else { + // If the gesture was ongoing for any other surface then cancel it. + true + }; + gesture.end(event.serial.into(), event.time.millis(), cancelled.into()); + } + }); + } + + pub(super) fn gesture_hold_begin( + &self, + surface: &WlSurface, + event: &GestureHoldBeginEvent, + ) { + self.for_each_focused_hold_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().replace(surface.clone()); + if ongoing.is_some() { + // Cancel an ongoing gesture for a different surface. + gesture.end(event.serial.into(), event.time.millis(), 1); + } + gesture.begin(event.serial.into(), event.time.millis(), surface, event.fingers); + }); + } + + pub(super) fn gesture_hold_end( + &self, + surface: &WlSurface, + event: &GestureHoldEndEvent, + ) { + self.for_each_focused_hold_gesture(surface, |gesture| { + let data = gesture.data::>().unwrap(); + let ongoing = data.in_progress_on.lock().unwrap().take(); + // Check if the gesture was ongoing. + if ongoing.is_some() { + let cancelled = if ongoing.as_ref() == Some(surface) { + event.cancelled + } else { + // If the gesture was ongoing for any other surface then cancel it. + true + }; + gesture.end(event.serial.into(), event.time.millis(), cancelled.into()); + } + }); + } + + fn for_each_focused_swipe_gesture( + &self, + surface: &WlSurface, + mut f: impl FnMut(ZwpPointerGestureSwipeV1), + ) { + let inner = self.known_swipe_gestures.lock().unwrap(); + for ptr in &*inner { + if ptr.id().same_client_as(&surface.id()) { + f(ptr.clone()) + } + } + } + + fn for_each_focused_pinch_gesture( + &self, + surface: &WlSurface, + mut f: impl FnMut(ZwpPointerGesturePinchV1), + ) { + let inner = self.known_pinch_gestures.lock().unwrap(); + for ptr in &*inner { + if ptr.id().same_client_as(&surface.id()) { + f(ptr.clone()) + } + } + } + + fn for_each_focused_hold_gesture(&self, surface: &WlSurface, mut f: impl FnMut(ZwpPointerGestureHoldV1)) { + let inner = self.known_hold_gestures.lock().unwrap(); + for ptr in &*inner { + if ptr.id().same_client_as(&surface.id()) { + f(ptr.clone()) + } + } + } +} + +/// User data of ZwpPointerGesture*V1 objects +#[derive(Debug)] +pub struct PointerGestureUserData { + arc: Weak>, + /// This gesture is in the middle between its begin() and end() on this surface. + pub(crate) in_progress_on: Mutex>, + client_scale: Arc, +} + +/// State of the pointer gestures +#[derive(Debug)] +pub struct PointerGesturesState { + global: GlobalId, +} + +impl PointerGesturesState { + /// Register new [ZwpPointerGesturesV1] global + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch>, + D: Dispatch>, + D: Dispatch>, + D: SeatHandler, + D: 'static, + { + let global = display.create_global::(MANAGER_VERSION, GlobalData); + + Self { global } + } + + /// [ZwpPointerGesturesV1] GlobalId getter + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch>, + D: Dispatch>, + D: Dispatch>, + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _pointer_gestures: &ZwpPointerGesturesV1, + request: zwp_pointer_gestures_v1::Request, + _dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_pointer_gestures_v1::Request::GetSwipeGesture { id, pointer } => { + let data = pointer.data::>().unwrap(); + let user_data = PointerGestureUserData { + arc: data.arc.clone(), + in_progress_on: Mutex::new(None), + client_scale: data.client_scale.clone(), + }; + let gesture = data_init.init(id, user_data); + if let Some(arc) = data.arc.upgrade() { + arc.wp_pointer_gestures.new_swipe_gesture(gesture); + } + } + zwp_pointer_gestures_v1::Request::GetPinchGesture { id, pointer } => { + let data = pointer.data::>().unwrap(); + let user_data = PointerGestureUserData { + arc: data.arc.clone(), + in_progress_on: Mutex::new(None), + client_scale: data.client_scale.clone(), + }; + let gesture = data_init.init(id, user_data); + if let Some(arc) = data.arc.upgrade() { + arc.wp_pointer_gestures.new_pinch_gesture(gesture); + } + } + zwp_pointer_gestures_v1::Request::GetHoldGesture { id, pointer } => { + let data = pointer.data::>().unwrap(); + let user_data = PointerGestureUserData { + arc: data.arc.clone(), + in_progress_on: Mutex::new(None), + client_scale: data.client_scale.clone(), + }; + let gesture = data_init.init(id, user_data); + if let Some(arc) = data.arc.upgrade() { + arc.wp_pointer_gestures.new_hold_gesture(gesture); + } + } + zwp_pointer_gestures_v1::Request::Release => {} + _ => unreachable!(), + } + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch + SeatHandler + 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for PointerGestureUserData +where + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _gesture: &ZwpPointerGestureSwipeV1, + request: zwp_pointer_gesture_swipe_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_pointer_gesture_swipe_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _: ClientId, object: &ZwpPointerGestureSwipeV1) { + if let Some(arc) = self.arc.upgrade() { + arc.wp_pointer_gestures + .known_swipe_gestures + .lock() + .unwrap() + .retain(|p| p.id() != object.id()); + } + } +} + +impl Dispatch2 for PointerGestureUserData +where + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _gesture: &ZwpPointerGesturePinchV1, + request: zwp_pointer_gesture_pinch_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_pointer_gesture_pinch_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _: ClientId, object: &ZwpPointerGesturePinchV1) { + if let Some(arc) = self.arc.upgrade() { + arc.wp_pointer_gestures + .known_pinch_gestures + .lock() + .unwrap() + .retain(|p| p.id() != object.id()); + } + } +} + +impl Dispatch2 for PointerGestureUserData +where + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _gesture: &ZwpPointerGestureHoldV1, + request: zwp_pointer_gesture_hold_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_pointer_gesture_hold_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _: ClientId, object: &ZwpPointerGestureHoldV1) { + if let Some(arc) = self.arc.upgrade() { + arc.wp_pointer_gestures + .known_hold_gestures + .lock() + .unwrap() + .retain(|p| p.id() != object.id()); + } + } +} diff --git a/vendor/smithay/src/wayland/pointer_warp.rs b/vendor/smithay/src/wayland/pointer_warp.rs new file mode 100644 index 0000000..d3271b8 --- /dev/null +++ b/vendor/smithay/src/wayland/pointer_warp.rs @@ -0,0 +1,158 @@ +//! Wp Pointer Warp +//! +//! This global interface allows applications to request the pointer to be moved to a position +//! relative to a wl_surface. +//! +//! In order to advertise pointer warp global call [PointerWarpManager::new]. +//! +//! ``` +//! use smithay::wayland::pointer_warp::{PointerWarpManager, PointerWarpHandler}; +//! use wayland_server::protocol::wl_surface::WlSurface; +//! use wayland_server::protocol::wl_pointer::WlPointer; +//! use smithay::utils::{Serial, Logical, Point}; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! # use smithay::reexports::wayland_server::Client; +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &WlSurface) {} +//! # } +//! # +//! # impl smithay::input::SeatHandler for State { +//! # type KeyboardFocus = WlSurface; +//! # type PointerFocus = WlSurface; +//! # type TouchFocus = WlSurface; +//! # fn seat_state(&mut self) -> &mut smithay::input::SeatState { +//! # todo!() +//! # } +//! # } +//! # impl PointerConstraintsHandler for State {} +//! +//! PointerWarpManager::new::( +//! &display.handle(), +//! ); +//! +//! impl PointerWarpHandler for State { +//! fn warp_pointer(&mut self, surface: WlSurface, pointer: WlPointer, pos: Point, serial: Serial) { +//! // Pointer warp was requested by the client +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::sync::atomic::Ordering; + +use wayland_protocols::wp::pointer_warp::v1::server::wp_pointer_warp_v1::{self, WpPointerWarpV1}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::GlobalId, + protocol::{wl_pointer::WlPointer, wl_surface::WlSurface}, +}; + +use crate::{ + input::SeatHandler, + utils::{Client as ClientCords, Logical, Point, Serial}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2, seat::PointerUserData}, +}; + +/// Handler trait for pointer warp events. +pub trait PointerWarpHandler: SeatHandler + 'static { + /// Request the compositor to move the pointer to a surface-local position. + /// Whether or not the compositor honors the request is implementation defined, but it should + /// - honor it if the surface has pointer focus, including when it has an implicit pointer grab + /// - reject it if the enter serial is incorrect + /// - reject it if the requested position is outside of the surface + /// + /// Note that the enter serial is valid for any surface of the client, and does not have to be from the surface the pointer is warped to. + /// + /// * `serial` - serial number of the surface enter event + #[allow(unused)] + fn warp_pointer( + &mut self, + surface: WlSurface, + pointer: WlPointer, + pos: Point, + serial: Serial, + ) { + } +} + +/// Delegate type for handling pointer warp events. +#[derive(Debug)] +pub struct PointerWarpManager { + global: GlobalId, +} + +impl PointerWarpManager { + /// Creates a new delegate type for handling [WpPointerWarpV1] events. + pub fn new(display: &DisplayHandle) -> Self + where + D: PointerWarpHandler + GlobalDispatch, + { + let global = display.create_global::(1, GlobalData); + Self { global } + } + + /// Returns the [WpPointerWarpV1] global id. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData { + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &WpPointerWarpV1, + request: wp_pointer_warp_v1::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + use wp_pointer_warp_v1::Request; + + match request { + Request::WarpPointer { + surface, + pointer, + x, + y, + serial, + } => { + let client_scale = pointer + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + let location: Point = Point::new(x, y); + let location = location.to_logical(client_scale); + + state.warp_pointer(surface, pointer, location, Serial::from(serial)); + } + Request::Destroy => {} + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/presentation/mod.rs b/vendor/smithay/src/wayland/presentation/mod.rs new file mode 100644 index 0000000..54b5ce6 --- /dev/null +++ b/vendor/smithay/src/wayland/presentation/mod.rs @@ -0,0 +1,348 @@ +//! Utilities for handling the `wp_presentation` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation, create [`PresentationState`], store it in your `State` struct and +//! implement the required traits, as shown in this example: +//! +//! ``` +//! use smithay::wayland::presentation::PresentationState; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the presentation state: +//! let presentation_state = PresentationState::new::( +//! &display.handle(), // the display +//! 1 // the id of the clock +//! ); +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Use the presentation state +//! +//! Before sending the frame callbacks you should drain all committed presentation feedback callbacks. +//! After the associated frame has been presented the callbacks can be marked presented as shown in +//! the example. +//! +//! The [`presentation state`](PresentationFeedbackCachedState) is double-buffered and +//! can be accessed by using the [`with_states`] function +//! +//! ```no_run +//! # use smithay::{output::{Output, PhysicalProperties, Subpixel}, wayland::compositor::with_states}; +//! # use wayland_server::{backend::ObjectId, protocol::wl_surface::WlSurface, Resource}; +//! # use std::time::Duration; +//! use smithay::wayland::presentation::{PresentationFeedbackCachedState, Refresh}; +//! use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let dh = display.handle(); +//! # let surface = WlSurface::from_id(&dh, ObjectId::null()).unwrap(); +//! # let output = Output::new( +//! # "output-0".into(), // the name of this output, +//! # PhysicalProperties { +//! # size: (200, 150).into(), // dimensions (width, height) in mm +//! # subpixel: Subpixel::HorizontalRgb, // subpixel information +//! # make: "Screens Inc".into(), // make of the monitor +//! # model: "Monitor Ultra".into(), // model of the monitor +//! # serial_number: "KL11C28J04XJ".into(), // serial number of the monitor +//! # }, +//! # ); +//! // ... render frame ... +//! +//! let presentation_feedbacks = with_states(&surface, |states| { +//! std::mem::take(&mut states.cached_state.get::().current().callbacks) +//! }); +//! +//! // ... send frame callbacks and present frame +//! +//! # let time = Duration::ZERO; +//! # let refresh = Refresh::fixed(Duration::from_secs_f64(1_000f64 / 60_000f64)); +//! # let seq = 0; +//! for feedback in presentation_feedbacks { +//! feedback.presented(&output, time, refresh, seq, wp_presentation_feedback::Kind::Vsync); +//! } +//! ``` + +use std::time::Duration; + +use wayland_protocols::wp::presentation_time::server::{wp_presentation, wp_presentation_feedback}; +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, Resource, Weak, backend::GlobalId, protocol::wl_surface, +}; + +use crate::{ + output::Output, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +use super::compositor::{Cacheable, with_states}; + +const EVT_PRESENTED_VARIABLE_SINCE: u32 = 2; + +/// State of the wp_presentation global +#[derive(Debug)] +pub struct PresentationState { + global: GlobalId, +} + +impl PresentationState { + /// Create new [`WpPresentation`](wp_presentation::WpPresentation) global. + /// + /// It returns the presentation state, which you can drop to remove these global from + /// the event loop in the future. + pub fn new(display: &DisplayHandle, clk_id: u32) -> Self + where + D: GlobalDispatch + + Dispatch + + Dispatch + + 'static, + { + PresentationState { + global: display + .create_global::(2, PresentationData { clk_id }), + } + } + + /// Returns the presentation global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug)] +pub struct PresentationData { + clk_id: u32, +} + +impl GlobalDispatch2 for PresentationData +where + D: Dispatch, + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let interface = data_init.init(resource, *self); + interface.clock_id(self.clk_id); + } +} + +impl Dispatch2 for PresentationData +where + D: Dispatch, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &wp_presentation::WpPresentation, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wp_presentation::Request::Feedback { surface, callback } => { + let callback = data_init.init(callback, GlobalData); + + // TODO: Is there a better way to store the surface? + with_states(&surface, |states| { + states + .cached_state + .get::() + .pending() + .add_callback(&surface, self.clk_id, callback); + }); + } + wp_presentation::Request::Destroy => { + // All is already handled by our destructor + } + _ => unreachable!(), + } + } +} + +impl Dispatch2 for GlobalData { + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &wp_presentation_feedback::WpPresentationFeedback, + _request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + // wp_presentation_feedback currently has no requests + } +} + +/// State for a single presentation feedback callback +#[derive(Debug)] +pub struct PresentationFeedbackState; + +/// Holds a single presentation feedback +#[derive(Debug)] +pub struct PresentationFeedbackCallback { + surface: Weak, + clk_id: u32, + callback: wp_presentation_feedback::WpPresentationFeedback, +} + +/// Refresh of the output on which the surface was presented +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Refresh { + /// The output does not have a known refresh rate + Unknown, + /// The output has a variable refresh rate + /// + /// The passed refresh rate has to correspond to the minimum (fastest) rate + Variable(Duration), + /// The output has a fixed rate + Fixed(Duration), +} + +impl Refresh { + /// Construct a [`Refresh::Variable`] from a [`Duration`] + pub fn variable(min: impl Into) -> Self { + Self::Variable(min.into()) + } + + /// Construct a [`Refresh::Fixed`] from a [`Duration`] + pub fn fixed(rate: impl Into) -> Self { + Self::Fixed(rate.into()) + } +} + +impl PresentationFeedbackCallback { + /// Get the id of the clock that was used at bind + pub fn clk_id(&self) -> u32 { + self.clk_id + } + + /// Mark this callback as presented + pub fn presented( + self, + output: &Output, + time: impl Into, + refresh: Refresh, + seq: u64, + flags: wp_presentation_feedback::Kind, + ) { + // If the surface has been destroyed but the callback + // has already been taken it won't be automatically + // discarded, do that now + let surface = match self.surface.upgrade() { + Ok(surface) => surface, + Err(_) => { + self.discarded(); + return; + } + }; + + // If the client disconnected there is no need to + // send any feedback + let client = match surface.client() { + Some(client) => client, + None => return, + }; + + for output in output.client_outputs(&client) { + self.callback.sync_output(&output); + } + + // Since version 2 wp_presentation supports variable refresh rates, + // in which case the minimum (fastest rate) refresh is sent. + // Clients binding an earlier version shall receive a zero refresh in this case. + let refresh = match refresh { + Refresh::Fixed(duration) => duration, + Refresh::Variable(duration) if self.callback.version() >= EVT_PRESENTED_VARIABLE_SINCE => { + duration + } + _ => Duration::ZERO, + }; + + let time = time.into(); + let tv_sec_hi = (time.as_secs() >> 32) as u32; + let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32; + let tv_nsec = time.subsec_nanos(); + let refresh = refresh.as_nanos() as u32; + let seq_hi = (seq >> 32) as u32; + let seq_lo = (seq & 0xFFFFFFFF) as u32; + + self.callback + .presented(tv_sec_hi, tv_sec_lo, tv_nsec, refresh, seq_hi, seq_lo, flags); + } + + /// Mark this callback as discarded + pub fn discarded(self) { + self.callback.discarded() + } +} + +/// State of a single presentation feedback requested +/// for a surface +#[derive(Debug, Default)] +pub struct PresentationFeedbackCachedState { + /// Holds the registered presentation feedbacks + pub callbacks: Vec, +} + +impl PresentationFeedbackCachedState { + fn add_callback( + &mut self, + surface: &wl_surface::WlSurface, + clk_id: u32, + callback: wp_presentation_feedback::WpPresentationFeedback, + ) { + self.callbacks.push(PresentationFeedbackCallback { + surface: surface.downgrade(), + clk_id, + callback, + }); + } +} + +impl Cacheable for PresentationFeedbackCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + PresentationFeedbackCachedState { + callbacks: std::mem::take(&mut self.callbacks), + } + } + + fn merge_into(mut self, into: &mut Self, _dh: &DisplayHandle) { + // In case our cached state does not contain any callbacks we can + // exit early. This is important for commits on a subsurface tree + // where we might end up merging the state multiple times which + // would override the callbacks without this check. + if self.callbacks.is_empty() { + return; + } + + // discard unprocessed callbacks as defined by the spec + // ...the user did not see the content update because it was superseded... + for callback in std::mem::replace(&mut into.callbacks, std::mem::take(&mut self.callbacks)) { + callback.discarded(); + } + } +} + +impl Drop for PresentationFeedbackCachedState { + fn drop(&mut self) { + // discard unprocessed callbacks as defined by the spec + // ...the user did not see the content update because it was superseded or its surface destroyed... + for callback in self.callbacks.drain(..) { + callback.discarded(); + } + } +} diff --git a/vendor/smithay/src/wayland/relative_pointer.rs b/vendor/smithay/src/wayland/relative_pointer.rs new file mode 100644 index 0000000..a58eba8 --- /dev/null +++ b/vendor/smithay/src/wayland/relative_pointer.rs @@ -0,0 +1,270 @@ +//! Utilities for relative pointer support +//! +//! [PointerHandle::relative_motion][crate::input::pointer::PointerHandle::relative_motion] +//! sends relative pointer events to any [ZwpRelativePointerV1] objects created by the client. +//! +//! ``` +//! extern crate wayland_server; +//! extern crate smithay; +//! +//! use smithay::wayland::relative_pointer::RelativePointerManagerState; +//! # use smithay::backend::input::{InputTime, KeyState}; +//! # use smithay::input::{ +//! # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, +//! # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, +//! # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, +//! # GestureHoldBeginEvent, GestureHoldEndEvent}, +//! # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, +//! # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, +//! # Seat, SeatHandler, SeatState, +//! # }; +//! # use smithay::utils::{IsAlive, Serial}; +//! +//! # #[derive(Debug, Clone, PartialEq)] +//! # struct Target; +//! # impl IsAlive for Target { +//! # fn alive(&self) -> bool { true } +//! # } +//! # impl PointerTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} +//! # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} +//! # fn frame(&self, seat: &Seat, data: &mut State) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} +//! # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} +//! # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} +//! # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} +//! # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} +//! # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} +//! # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} +//! # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} +//! # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} +//! # } +//! # impl KeyboardTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} +//! # fn key( +//! # &self, +//! # seat: &Seat, +//! # data: &mut State, +//! # key: KeysymHandle<'_>, +//! # state: KeyState, +//! # serial: Serial, +//! # time: InputTime, +//! # ) {} +//! # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} +//! # } +//! # impl TouchTarget for Target { +//! # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} +//! # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} +//! # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } +//! # } +//! # struct State { +//! # seat_state: SeatState, +//! # }; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # impl SeatHandler for State { +//! # type KeyboardFocus = Target; +//! # type PointerFocus = Target; +//! # type TouchFocus = Target; +//! # +//! # fn seat_state(&mut self) -> &mut SeatState { +//! # &mut self.seat_state +//! # } +//! # } +//! let state = RelativePointerManagerState::new::(&display.handle()); +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::sync::{Arc, Mutex, Weak, atomic::Ordering}; + +use portable_atomic::AtomicF64; +use wayland_protocols::wp::relative_pointer::zv1::server::{ + zwp_relative_pointer_manager_v1::{self, ZwpRelativePointerManagerV1}, + zwp_relative_pointer_v1::{self, ZwpRelativePointerV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId}, + protocol::wl_surface::WlSurface, +}; + +use crate::{ + input::{ + SeatHandler, + pointer::{PointerRc, RelativeMotionEvent}, + }, + wayland::{Dispatch2, GlobalData, GlobalDispatch2, seat::PointerUserData}, +}; + +const MANAGER_VERSION: u32 = 1; + +#[derive(Debug, Default)] +pub(crate) struct WpRelativePointerHandle { + known_relative_pointers: Mutex>, +} + +impl WpRelativePointerHandle { + fn new_relative_pointer(&self, pointer: ZwpRelativePointerV1) { + self.known_relative_pointers.lock().unwrap().push(pointer); + } + + pub(super) fn relative_motion( + &self, + surface: &WlSurface, + event: &RelativeMotionEvent, + ) { + self.for_each_focused_pointer(surface, |ptr| { + let client_scale = ptr + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + let delta = event.delta.to_client(client_scale); + let delta_unaccel = event.delta_unaccel; + + let utime = event.time.micros(); + let utime_hi = (utime >> 32) as u32; + let utime_lo = (utime & 0xffffffff) as u32; + ptr.relative_motion( + utime_hi, + utime_lo, + delta.x, + delta.y, + delta_unaccel.x, + delta_unaccel.y, + ); + }) + } + + fn for_each_focused_pointer(&self, surface: &WlSurface, mut f: impl FnMut(ZwpRelativePointerV1)) { + let inner = self.known_relative_pointers.lock().unwrap(); + for ptr in &*inner { + if ptr.id().same_client_as(&surface.id()) { + f(ptr.clone()) + } + } + } +} + +/// User data of ZwpRelativePointerV1 object +#[derive(Debug)] +pub struct RelativePointerUserData { + arc: Weak>, + client_scale: Arc, +} + +/// State of the relative pointer manager +#[derive(Debug)] +pub struct RelativePointerManagerState { + global: GlobalId, +} + +impl RelativePointerManagerState { + /// Register new [ZwpRelativePointerV1] global + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch>, + D: SeatHandler, + D: 'static, + { + let global = display.create_global::(MANAGER_VERSION, GlobalData); + + Self { global } + } + + /// [ZwpRelativePointerV1] GlobalId getter + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch>, + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _relative_pointer_manager: &ZwpRelativePointerManagerV1, + request: zwp_relative_pointer_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_relative_pointer_manager_v1::Request::GetRelativePointer { id, pointer } => { + let data = pointer.data::>().unwrap(); + let user_data = RelativePointerUserData { + arc: data.arc.clone(), + client_scale: data.client_scale.clone(), + }; + let pointer = data_init.init(id, user_data); + if let Some(arc) = data.arc.upgrade() { + arc.wp_relative.new_relative_pointer(pointer); + } + } + zwp_relative_pointer_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch + SeatHandler + 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for RelativePointerUserData +where + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _relative_pointer: &ZwpRelativePointerV1, + request: zwp_relative_pointer_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_relative_pointer_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _: ClientId, object: &ZwpRelativePointerV1) { + if let Some(arc) = self.arc.upgrade() { + arc.wp_relative + .known_relative_pointers + .lock() + .unwrap() + .retain(|p| p.id() != object.id()); + } + } +} diff --git a/vendor/smithay/src/wayland/seat/keyboard.rs b/vendor/smithay/src/wayland/seat/keyboard.rs new file mode 100644 index 0000000..b4b80a9 --- /dev/null +++ b/vendor/smithay/src/wayland/seat/keyboard.rs @@ -0,0 +1,348 @@ +use std::{cell::RefCell, fmt, sync::Weak}; + +use tracing::{instrument, trace, warn}; +use wayland_server::{ + Client, DisplayHandle, Resource, + backend::{ClientId, ObjectId}, + protocol::{ + wl_keyboard::{self, KeyState as WlKeyState, WlKeyboard}, + wl_surface::WlSurface, + }, +}; + +use super::WaylandFocus; +use crate::{ + backend::input::{InputTime, KeyState, Keycode}, + input::{ + Seat, SeatHandler, WeakSeat, + keyboard::{KbdRc, KeyboardHandle, KeyboardTarget, KeysymHandle, ModifiersState}, + }, + utils::{HookId, Serial, iter::new_locked_obj_iter_from_vec}, + wayland::{ + Dispatch2, + compositor::{add_destruction_hook, remove_destruction_hook, with_states}, + input_method::InputMethodSeat, + text_input::TextInputSeat, + }, +}; + +impl KeyboardHandle +where + D: SeatHandler + 'static, + ::KeyboardFocus: WaylandFocus, +{ + /// Check if client of given resource currently has keyboard focus + pub fn client_of_object_has_focus(&self, id: &ObjectId) -> bool { + self.arc + .internal + .lock() + .unwrap() + .focus + .as_ref() + .map(|f| f.0.same_client_as(id)) + .unwrap_or(false) + } + + /// Return all raw [`WlKeyboard`] instances for a particular [`Client`] + pub fn client_keyboards<'a>(&'a self, client: &Client) -> impl Iterator + 'a { + let guard = self.arc.known_kbds.lock().unwrap(); + + new_locked_obj_iter_from_vec(guard, client.id()) + } + + /// Register a new keyboard to this handler + /// + /// The keymap will automatically be sent to it + /// + /// This should be done first, before anything else is done with this keyboard. + #[instrument(parent = &self.arc.span, skip(self))] + pub(crate) fn new_kbd(&self, kbd: WlKeyboard) { + trace!("Sending keymap to client"); + + // prepare a tempfile with the keymap, to send it to the client + let keymap_file = self.arc.keymap.lock().unwrap(); + let ret = keymap_file.send(&kbd); + + if let Err(e) = ret { + warn!( + err = ?e, + "Failed write keymap to client in a tempfile" + ); + return; + }; + + let guard = self.arc.internal.lock().unwrap(); + if kbd.version() >= 4 { + kbd.repeat_info(guard.repeat_rate, guard.repeat_delay); + } + if let Some((focused, serial)) = guard.focus.as_ref() { + if focused.same_client_as(&kbd.id()) { + let serialized = guard.mods_state.serialized; + let keys = serialize_pressed_keys(guard.pressed_keys.iter().copied()); + kbd.enter((*serial).into(), &focused.wl_surface().unwrap(), keys); + // Modifiers must be send after enter event. + kbd.modifiers( + (*serial).into(), + serialized.depressed, + serialized.latched, + serialized.locked, + serialized.layout_effective, + ); + } + } + self.arc.known_kbds.lock().unwrap().push(kbd.downgrade()); + } +} + +impl KeyboardHandle { + /// Attempt to retrieve a [`KeyboardHandle`] from an existing resource + /// + /// May return `None` for a valid `WlKeyboard` that was created without + /// the keyboard capability, or if capability has since been removed. + pub fn from_resource(seat: &WlKeyboard) -> Option { + Some(KeyboardHandle { + arc: seat.data::>()?.arc.upgrade()?, + }) + } +} + +/// User data for keyboard +pub struct KeyboardUserData { + pub(crate) arc: Weak>, +} + +impl fmt::Debug for KeyboardUserData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KeyboardUserData") + .field("arc", &self.arc) + .finish() + } +} + +impl Dispatch2 for KeyboardUserData +where + D: 'static, + D: SeatHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &WlKeyboard, + _request: wl_keyboard::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + } + + fn destroyed(&self, _state: &mut D, _client_id: ClientId, keyboard: &WlKeyboard) { + if let Some(arc) = self.arc.upgrade() { + arc.known_kbds.lock().unwrap().retain(|k| k.id() != keyboard.id()) + } + } +} + +pub(crate) fn for_each_focused_kbds( + seat: &Seat, + surface: &WlSurface, + mut f: impl FnMut(WlKeyboard), +) { + if let Some(keyboard) = seat.get_keyboard() { + let inner = keyboard.arc.known_kbds.lock().unwrap(); + for kbd in &*inner { + let Ok(kbd) = kbd.upgrade() else { + continue; + }; + + if kbd.id().same_client_as(&surface.id()) { + f(kbd.clone()) + } + } + } +} + +/// Serialize keycodes for the `WlKeyboard` interface +pub fn serialize_pressed_keys(keys: impl Iterator) -> Vec { + keys.flat_map(|key| (key.raw() - 8).to_ne_bytes()).collect() +} + +// WeakSeat doesn't implement `Hash`, but we don't expect a lot of seats anyway, +// so a vector with linear comparison is fine actually. +pub(crate) struct FocusDestroyHook(RefCell, HookId)>>); + +impl FocusDestroyHook { + pub fn insert(&self, seat: &Seat, hook_id: HookId) -> Option { + let mut set = self.0.borrow_mut(); + set.retain(|(seat, _)| seat.upgrade().is_some()); + + if let Some((_, hook)) = set + .iter_mut() + .find(|(s, _)| s.upgrade().is_some_and(|s| &s == seat)) + { + Some(std::mem::replace(hook, hook_id)) + } else { + set.push((seat.downgrade(), hook_id)); + None + } + } + + pub fn remove(&self, seat: &Seat) -> Option { + let mut set = self.0.borrow_mut(); + set.retain(|(seat, _)| seat.upgrade().is_some()); + set.iter() + .position(|(s, _)| s.upgrade().is_some_and(|s| &s == seat)) + .map(|pos| set.remove(pos).1) + } +} + +impl Default for FocusDestroyHook { + fn default() -> Self { + FocusDestroyHook(RefCell::new(Vec::new())) + } +} + +pub(crate) fn enter_internal( + surface: &WlSurface, + seat: &Seat, + state: &mut D, + keys: impl Iterator, + serial: Serial, +) { + *seat.get_keyboard().unwrap().arc.last_enter.lock().unwrap() = Some(serial); + let serialized_keys = serialize_pressed_keys(keys); + for_each_focused_kbds(seat, surface, |kbd| { + kbd.enter(serial.into(), surface, serialized_keys.clone()) + }); + + let seat_clone = seat.clone(); + let hook_id = add_destruction_hook::(surface, move |_, surface| { + if let Some(client) = surface.client() { + let keyboard = seat_clone.get_keyboard().unwrap(); + let inner = keyboard.arc.known_kbds.lock().unwrap(); + for kbd in &*inner { + let Ok(kbd) = kbd.upgrade() else { + continue; + }; + + if kbd.client().is_some_and(|c| c == client) { + kbd.leave(serial.into(), surface); + } + } + } + }); + if let Some(old_hook_id) = with_states(surface, |states| { + states + .data_map + .get_or_insert::, _>(Default::default) + .insert(seat, hook_id) + }) { + remove_destruction_hook(surface, &old_hook_id); + } + + let text_input = seat.text_input(); + let input_method = seat.input_method(); + + if input_method.has_instance() { + input_method.deactivate_input_method(state); + } + + // NOTE: Always set focus regardless whether the client actually has the + // text-input global bound due to clients doing lazy global binding. + text_input.set_focus(Some(surface.clone())); + + // Notify on `enter` once we have an actual IME, or while the compositor is itself acting + // as the input method for this seat. + if input_method.has_instance() || text_input.compositor_input_method() { + text_input.enter(); + } +} + +impl KeyboardTarget for WlSurface { + fn enter(&self, seat: &Seat, state: &mut D, keys: Vec>, serial: Serial) { + enter_internal(self, seat, state, keys.iter().map(|h| h.raw_code()), serial) + } + + fn leave(&self, seat: &Seat, state: &mut D, serial: Serial) { + *seat.get_keyboard().unwrap().arc.last_enter.lock().unwrap() = None; + for_each_focused_kbds(seat, self, |kbd| kbd.leave(serial.into(), self)); + if let Some(hook_id) = with_states(self, |states| { + states + .data_map + .get::>() + .and_then(|hook| hook.remove(seat)) + }) { + remove_destruction_hook(self, &hook_id); + }; + + let text_input = seat.text_input(); + let input_method = seat.input_method(); + + if input_method.has_instance() { + input_method.deactivate_input_method(state); + } + // Send `leave` for a real IME or while the compositor is acting as the input method. + if input_method.has_instance() || text_input.compositor_input_method() { + text_input.leave(); + } + + text_input.set_focus(None); + } + + fn key( + &self, + seat: &Seat, + _data: &mut D, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: InputTime, + ) { + for_each_focused_kbds(seat, self, |kbd| { + kbd.key( + serial.into(), + time.millis(), + key.raw_code().raw() - 8, + state.into(), + ) + }) + } + + fn modifiers(&self, seat: &Seat, _data: &mut D, modifiers: ModifiersState, serial: Serial) { + for_each_focused_kbds(seat, self, |kbd| { + let modifiers = modifiers.serialized; + kbd.modifiers( + serial.into(), + modifiers.depressed, + modifiers.latched, + modifiers.locked, + modifiers.layout_effective, + ); + }) + } +} + +impl From for WlKeyState { + #[inline] + fn from(state: KeyState) -> WlKeyState { + match state { + KeyState::Pressed => WlKeyState::Pressed, + KeyState::Released => WlKeyState::Released, + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Unknown KeyState {0:?}")] +pub struct UnknownKeyState(WlKeyState); + +impl TryFrom for KeyState { + type Error = UnknownKeyState; + #[inline] + fn try_from(state: WlKeyState) -> Result { + match state { + WlKeyState::Pressed => Ok(KeyState::Pressed), + WlKeyState::Released => Ok(KeyState::Released), + x => Err(UnknownKeyState(x)), + } + } +} diff --git a/vendor/smithay/src/wayland/seat/mod.rs b/vendor/smithay/src/wayland/seat/mod.rs new file mode 100644 index 0000000..18e542f --- /dev/null +++ b/vendor/smithay/src/wayland/seat/mod.rs @@ -0,0 +1,412 @@ +//! Seat global utilities +//! +//! This module provides you with utilities for handling the seat globals +//! and the associated input Wayland objects. +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! ``` +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! use smithay::reexports::wayland_server::{Display, protocol::wl_surface::WlSurface}; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! # use smithay::reexports::wayland_server::Client; +//! +//! # struct State { seat_state: SeatState }; +//! # let mut display = Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! let mut seat_state = SeatState::::new(); +//! // add the seat state to your state +//! // ... +//! +//! // create the wl_seat +//! let seat = seat_state.new_wl_seat( +//! &display_handle, // the display +//! "seat-0", // the name of the seat, will be advertized to clients +//! ); +//! +//! // implement the required traits +//! impl SeatHandler for State { +//! type KeyboardFocus = WlSurface; +//! type PointerFocus = WlSurface; +//! type TouchFocus = WlSurface; +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { +//! // ... +//! } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { +//! // ... +//! } +//! } +//! # impl PointerConstraintsHandler for State {} +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &WlSurface) {} +//! # } +//! ``` +//! +//! ### Run usage +//! +//! Once the seat is initialized, you can add capabilities to it. +//! +//! You can add these capabilities via methods of the [`Seat`] struct: +//! [`Seat::add_keyboard`], [`Seat::add_pointer`] and [`Seat::add_touch`]. +//! These methods return handles that can be cloned and sent across thread, so you can keep one around +//! in your event-handling code to forward inputs to your clients. +//! +//! This module further defines the `"cursor_image"` role, that is assigned to surfaces used by clients +//! to change the cursor icon. + +pub(crate) mod keyboard; +pub(crate) mod pointer; +mod touch; + +use std::{ + borrow::Cow, + fmt, + sync::{ + Arc, Weak, + atomic::{AtomicU32, Ordering}, + }, +}; + +use crate::input::{Inner, Seat, SeatHandler, SeatRc, SeatState}; +use crate::wayland::{Dispatch2, GlobalDispatch2}; + +pub use self::{ + keyboard::KeyboardUserData, + pointer::{CURSOR_IMAGE_ROLE, PointerUserData}, + touch::TouchUserData, +}; + +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId, ObjectId}, + protocol::{ + wl_keyboard::WlKeyboard, + wl_pointer::WlPointer, + wl_seat::{self, WlSeat}, + wl_surface, + wl_touch::WlTouch, + }, +}; + +use super::compositor::CompositorHandler; + +/// Focused objects that *might* have an underlying wl_surface. +pub trait WaylandFocus { + /// Returns the underlying wl_surface, if any. + /// + /// *Note*: This has to return `Some`, if `same_client_as` can return true + /// for any provided `ObjectId` + fn wl_surface(&self) -> Option>; + /// Returns true, if the underlying wayland object originates from + /// the same client connection as the provided `ObjectId`. + /// + /// *Must* return false, if there is not underlying wayland object. + fn same_client_as(&self, object_id: &ObjectId) -> bool { + self.wl_surface() + .map(|s| s.id().same_client_as(object_id)) + .unwrap_or(false) + } +} + +impl WaylandFocus for wl_surface::WlSurface { + #[inline] + fn wl_surface(&self) -> Option> { + Some(Cow::Borrowed(self)) + } +} + +impl Inner { + fn compute_caps(&self) -> wl_seat::Capability { + let mut caps = wl_seat::Capability::empty(); + if self.pointer.is_some() { + caps |= wl_seat::Capability::Pointer; + } + if self.keyboard.is_some() { + caps |= wl_seat::Capability::Keyboard; + } + if self.touch.is_some() { + caps |= wl_seat::Capability::Touch; + } + caps + } + + pub(crate) fn send_all_caps(&self) { + let capabilities = self.compute_caps(); + for seat in &self.known_seats { + if let Ok(seat) = seat.upgrade() { + let data = seat.data::>().unwrap(); + data.sent_capabilities + .fetch_or(u32::from(capabilities), Ordering::SeqCst); + + seat.capabilities(capabilities); + } + } + } +} + +/// Global data of WlSeat +pub struct SeatGlobalData { + arc: Arc>, +} + +impl fmt::Debug for SeatGlobalData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SeatGlobalData").field("arc", &self.arc).finish() + } +} + +impl SeatState { + /// Create a new seat global + /// + /// A new seat global is created with given name and inserted + /// into this wayland display. + /// + /// You are provided with the state token to retrieve it (allowing + /// you to add or remove capabilities from it), and the global handle, + /// in case you want to remove it. + pub fn new_wl_seat(&mut self, display: &DisplayHandle, name: N) -> Seat + where + D: GlobalDispatch> + SeatHandler + 'static, + ::PointerFocus: WaylandFocus, + ::KeyboardFocus: WaylandFocus, + N: Into, + { + let Seat { arc } = self.new_seat(name); + + let global_id = display.create_global::(9, SeatGlobalData { arc: arc.clone() }); + arc.inner.lock().unwrap().global = Some(global_id); + + Seat { arc } + } +} + +impl Seat { + /// Checks whether a given [`WlSeat`] is associated with this [`Seat`] + pub fn owns(&self, seat: &wl_seat::WlSeat) -> bool { + let inner = self.arc.inner.lock().unwrap(); + inner.known_seats.iter().any(|s| s == seat) + } + + /// Attempt to retrieve a [`Seat`] from an existing resource + /// + /// May return `None` for a valid `WlSeat` if the `Seat` has since been + /// removed. + pub fn from_resource(seat: &WlSeat) -> Option { + Some(Self { + arc: seat.data::>()?.arc.upgrade()?, + }) + } + + /// Retrieves [`WlSeat`] resources for a given client + pub fn client_seats(&self, client: &Client) -> Vec { + self.arc + .inner + .lock() + .unwrap() + .known_seats + .iter() + .filter_map(|w| w.upgrade().ok()) + .filter(|s| s.client().is_some_and(|c| &c == client)) + .collect() + } + + /// Get the id of WlSeat global + pub fn global(&self) -> Option { + self.arc.inner.lock().unwrap().global.as_ref().cloned() + } +} + +/// User data for seat +pub struct SeatUserData { + arc: Weak>, + // Protocol states that `missing_capability` should be sent if capability is + // not advertised, *or* previously advertised on seat. + sent_capabilities: AtomicU32, +} + +impl fmt::Debug for SeatUserData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SeatUserData").field("arc", &self.arc).finish() + } +} + +impl Dispatch2 for SeatUserData +where + D: Dispatch>, + D: Dispatch>, + D: Dispatch>, + D: SeatHandler, + D: CompositorHandler, + ::PointerFocus: WaylandFocus, + ::KeyboardFocus: WaylandFocus, + ::TouchFocus: WaylandFocus, + D: 'static, +{ + fn request( + &self, + state: &mut D, + client: &wayland_server::Client, + seat: &WlSeat, + request: wl_seat::Request, + _dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let sent_capabilities = + wl_seat::Capability::from_bits_retain(self.sent_capabilities.load(Ordering::SeqCst)); + + match request { + wl_seat::Request::GetPointer { id } => { + if !sent_capabilities.contains(wl_seat::Capability::Pointer) { + seat.post_error(wl_seat::Error::MissingCapability, "missing pointer capability"); + return; + } + + let ptr_handle = self + .arc + .upgrade() + .and_then(|arc| arc.inner.lock().unwrap().pointer.clone()); + + let client_scale = state.client_compositor_state(client).clone_client_scale(); + let pointer = data_init.init( + id, + PointerUserData { + arc: ptr_handle + .as_ref() + .map_or_else(Weak::new, |h| Arc::downgrade(&h.arc)), + client_scale, + }, + ); + + if let Some(ptr_handle) = &ptr_handle { + ptr_handle.arc.wl_pointer.new_pointer::(pointer); + } else { + // we should send a protocol error... but the protocol does not allow + // us, so this pointer will just remain inactive ¯\_(ツ)_/¯ + } + } + wl_seat::Request::GetKeyboard { id } => { + if !sent_capabilities.contains(wl_seat::Capability::Keyboard) { + seat.post_error(wl_seat::Error::MissingCapability, "missing keyboard capability"); + return; + } + + let kbd_handle = self + .arc + .upgrade() + .and_then(|arc| arc.inner.lock().unwrap().keyboard.clone()); + + let keyboard = data_init.init( + id, + KeyboardUserData { + arc: kbd_handle + .as_ref() + .map_or_else(Weak::new, |h| Arc::downgrade(&h.arc)), + }, + ); + + if let Some(h) = &kbd_handle { + h.new_kbd(keyboard); + } else { + // same as pointer, should error but cannot + + // Protocol spec says this should be sent immediately on creation, so send + // for inert object. + if keyboard.version() >= 4 { + keyboard.repeat_info(0, 0); + } + } + } + wl_seat::Request::GetTouch { id } => { + if !sent_capabilities.contains(wl_seat::Capability::Touch) { + seat.post_error(wl_seat::Error::MissingCapability, "missing touch capability"); + return; + } + + let touch_handle = self + .arc + .upgrade() + .and_then(|arc| arc.inner.lock().unwrap().touch.clone()); + + let client_scale = state.client_compositor_state(client).clone_client_scale(); + let touch = data_init.init( + id, + TouchUserData { + arc: touch_handle + .as_ref() + .map_or_else(Weak::new, |h| Arc::downgrade(&h.arc)), + client_scale, + }, + ); + + if let Some(h) = &touch_handle { + h.new_touch(touch); + } else { + // same as pointer, should error but cannot + } + } + wl_seat::Request::Release => { + // Our destructors already handle it + } + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _: ClientId, seat: &WlSeat) { + if let Some(arc) = self.arc.upgrade() { + arc.inner + .lock() + .unwrap() + .known_seats + .retain(|s| s.id() != seat.id()); + } + } +} + +impl GlobalDispatch2 for SeatGlobalData +where + D: Dispatch>, + D: Dispatch>, + D: Dispatch>, + D: Dispatch>, + D: SeatHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let mut inner = self.arc.inner.lock().unwrap(); + + let capabilities = inner.compute_caps(); + + let data = SeatUserData { + arc: Arc::downgrade(&self.arc), + sent_capabilities: AtomicU32::new(u32::from(capabilities)), + }; + + let resource = data_init.init(resource, data); + + if resource.version() >= 2 { + resource.name(self.arc.name.clone()); + } + + resource.capabilities(inner.compute_caps()); + inner.known_seats.push(resource.downgrade()); + } +} diff --git a/vendor/smithay/src/wayland/seat/pointer.rs b/vendor/smithay/src/wayland/seat/pointer.rs new file mode 100644 index 0000000..eac6fc0 --- /dev/null +++ b/vendor/smithay/src/wayland/seat/pointer.rs @@ -0,0 +1,614 @@ +use std::sync::{Arc, Mutex, atomic::Ordering}; + +use portable_atomic::AtomicF64; +use wayland_server::{ + Client, DisplayHandle, Resource, Weak, + backend::{ClientId, ObjectId}, + protocol::{ + wl_pointer::{ + self, Axis as WlAxis, AxisSource as WlAxisSource, ButtonState as WlButtonState, Request, + WlPointer, + }, + wl_surface::WlSurface, + }, +}; + +use crate::{ + backend::input::{Axis, AxisSource, ButtonState, InputTime}, + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, CursorImageAttributes, CursorImageStatus, GestureHoldBeginEvent, + GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, + PointerHandle, PointerRc, PointerTarget, RelativeMotionEvent, + }, + }, + utils::{Client as ClientCoords, Point, Serial, iter::new_locked_obj_iter_from_vec}, + wayland::{ + Dispatch2, compositor, + pointer_constraints::{ConstraintRemove, PointerConstraintsHandler, with_pointer_constraint}, + }, +}; + +use super::{SeatHandler, WaylandFocus}; + +// Use to accumulate discrete values for `wl_pointer` < 8 +#[derive(Default)] +struct V120UserData { + x: i32, + y: i32, +} + +/// WlSurface role of a cursor image icon +pub const CURSOR_IMAGE_ROLE: &str = "cursor_image"; + +impl PointerHandle { + /// Attempt to retrieve a [`PointerHandle`] from an existing resource + /// + /// May return `None` for a valid `WlPointer` that was created without + /// the pointer capability, or if capability has since been removed. + pub fn from_resource(pointer: &WlPointer) -> Option { + Some(Self { + arc: pointer.data::>()?.arc.upgrade()?, + }) + } + + /// Return all raw [`WlPointer`] instances for a particular [`Client`] + pub fn client_pointers<'a>(&'a self, client: &Client) -> impl Iterator + 'a { + let guard = self.arc.wl_pointer.known_pointers.lock().unwrap(); + new_locked_obj_iter_from_vec(guard, client.id()) + } +} + +#[derive(Debug, Default)] +pub(crate) struct WlPointerHandle { + pub(crate) last_enter: Mutex>, + known_pointers: Mutex>>, +} + +impl WlPointerHandle { + pub(super) fn new_pointer(&self, pointer: WlPointer) + where + ::PointerFocus: WaylandFocus, + { + self.known_pointers.lock().unwrap().push(pointer.downgrade()); + + let data = pointer.data::>().unwrap(); + if let Some(arc) = data.arc.upgrade() { + let guard = arc.inner.lock().unwrap(); + if let Some((focus, location)) = &guard.focus { + if focus.same_client_as(&pointer.id()) { + if let Some(surface) = focus.wl_surface() { + let serial = self.last_enter.lock().unwrap().unwrap(); + let client_scale = data.client_scale.load(Ordering::Acquire); + let location = (guard.location - *location).to_client(client_scale); + pointer.enter(serial.into(), &surface, location.x, location.y); + if pointer.version() >= 5 { + pointer.frame(); + } + } + } + } + } + } + + fn enter(&self, surface: &WlSurface, event: &MotionEvent) { + *self.last_enter.lock().unwrap() = Some(event.serial); + + self.for_each_focused_pointer(surface, |ptr| { + let client_scale = ptr + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + let location = event.location.to_client(client_scale); + ptr.enter(event.serial.into(), surface, location.x, location.y); + }) + } + + fn leave(&self, surface: &WlSurface, serial: Serial, _time: InputTime) { + self.for_each_focused_pointer(surface, |ptr| { + ptr.leave(serial.into(), surface); + if ptr.version() >= 5 { + ptr.frame(); + } + }); + + *self.last_enter.lock().unwrap() = None; + } + + fn motion(&self, surface: &WlSurface, event: &MotionEvent) { + self.for_each_focused_pointer(surface, |ptr| { + let client_scale = ptr + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + let location = event.location.to_client(client_scale); + ptr.motion(event.time.millis(), location.x, location.y); + }) + } + + fn button(&self, surface: &WlSurface, event: &ButtonEvent) { + self.for_each_focused_pointer(surface, |ptr| { + ptr.button( + event.serial.into(), + event.time.millis(), + event.button, + event.state.into(), + ); + }) + } + + fn axis(&self, surface: &WlSurface, details: AxisFrame) { + self.for_each_focused_pointer(surface, |ptr| { + if ptr.version() >= 5 { + // axis source + if let Some(source) = details.source { + // WheelTilt was not supported before version 6 + // The best we can do is replace it with Wheel + let source = if ptr.version() < 6 { + match source { + AxisSource::WheelTilt => AxisSource::Wheel, + other => other, + } + } else { + source + } + .into(); + ptr.axis_source(source); + } + // axis discrete + if let Some((x, y)) = details.v120 { + if ptr.version() >= 8 { + if x != 0 { + ptr.axis_value120(WlAxis::HorizontalScroll, x); + } + if y != 0 { + ptr.axis_value120(WlAxis::VerticalScroll, y); + } + } else { + compositor::with_states(surface, |states| { + let mut data = states + .data_map + .get_or_insert_threadsafe(Mutex::::default) + .lock() + .unwrap(); + + data.x += x; + if data.x.abs() >= 120 { + ptr.axis_discrete(WlAxis::HorizontalScroll, data.x / 120); + data.x %= 120; + } + + data.y += y; + if data.y.abs() >= 120 { + ptr.axis_discrete(WlAxis::VerticalScroll, data.y / 120); + data.y %= 120; + } + }); + } + } + // stop + if details.stop.0 { + ptr.axis_stop(details.time.millis(), WlAxis::HorizontalScroll); + + compositor::with_states(surface, |states| { + if let Some(data) = states.data_map.get::>() { + data.lock().unwrap().x = 0; + } + }); + } + if details.stop.1 { + ptr.axis_stop(details.time.millis(), WlAxis::VerticalScroll); + + compositor::with_states(surface, |states| { + if let Some(data) = states.data_map.get::>() { + data.lock().unwrap().y = 0; + } + }); + } + } + // axis + let client_scale = ptr + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + if details.axis.0 != 0.0 { + if ptr.version() >= 9 { + ptr.axis_relative_direction( + WlAxis::HorizontalScroll, + details.relative_direction.0.into(), + ); + } + ptr.axis( + details.time.millis(), + WlAxis::HorizontalScroll, + details.axis.0 * client_scale, + ); + } + if details.axis.1 != 0.0 { + if ptr.version() >= 9 { + ptr.axis_relative_direction(WlAxis::VerticalScroll, details.relative_direction.1.into()); + } + ptr.axis( + details.time.millis(), + WlAxis::VerticalScroll, + details.axis.1 * client_scale, + ); + } + }) + } + + fn frame(&self, surface: &WlSurface) { + self.for_each_focused_pointer(surface, |ptr| { + if ptr.version() >= 5 { + ptr.frame(); + } + }); + } + + fn for_each_focused_pointer(&self, surface: &WlSurface, mut f: impl FnMut(WlPointer)) { + let inner = self.known_pointers.lock().unwrap(); + for ptr in &*inner { + let Ok(ptr) = ptr.upgrade() else { + continue; + }; + + if ptr.id().same_client_as(&surface.id()) { + f(ptr.clone()) + } + } + } +} + +impl PointerTarget for WlSurface +where + D: SeatHandler, + D: PointerConstraintsHandler, + D: 'static, +{ + fn enter(&self, seat: &Seat, _data: &mut D, event: &MotionEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wl_pointer.enter::(self, event); + } + } + + fn leave(&self, seat: &Seat, data: &mut D, serial: Serial, time: InputTime) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wp_pointer_gestures.leave::(self, serial, time); + pointer.arc.wl_pointer.leave(self, serial, time); + + if let Some(region) = with_pointer_constraint(self, &pointer, |constraint| { + if let Some(constraint) = constraint { + if constraint.is_active() { + let region = constraint.region().cloned(); + constraint.deactivate(); + Some(region) + } else { + None + } + } else { + None + } + }) { + data.remove_constraint(self, &pointer, ConstraintRemove::PointerLeave(region)); + } + } + + compositor::with_states(self, |states| { + if let Some(data) = states.data_map.get::>() { + *data.lock().unwrap() = Default::default(); + } + }); + } + + fn motion(&self, seat: &Seat, _data: &mut D, event: &MotionEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wl_pointer.motion::(self, event); + } + } + + fn relative_motion(&self, seat: &Seat, _data: &mut D, event: &RelativeMotionEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wp_relative.relative_motion::(self, event); + } + } + + fn button(&self, seat: &Seat, _data: &mut D, event: &ButtonEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wl_pointer.button(self, event); + } + } + + fn axis(&self, seat: &Seat, _data: &mut D, details: AxisFrame) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wl_pointer.axis::(self, details); + } + } + + fn frame(&self, seat: &Seat, _data: &mut D) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wl_pointer.frame(self); + } + } + + fn gesture_swipe_begin(&self, seat: &Seat, _data: &mut D, event: &GestureSwipeBeginEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer + .arc + .wp_pointer_gestures + .gesture_swipe_begin::(self, event); + } + } + + fn gesture_swipe_update(&self, seat: &Seat, _data: &mut D, event: &GestureSwipeUpdateEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer + .arc + .wp_pointer_gestures + .gesture_swipe_update::(self, event); + } + } + + fn gesture_swipe_end(&self, seat: &Seat, _data: &mut D, event: &GestureSwipeEndEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer + .arc + .wp_pointer_gestures + .gesture_swipe_end::(self, event); + } + } + + fn gesture_pinch_begin(&self, seat: &Seat, _data: &mut D, event: &GesturePinchBeginEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer + .arc + .wp_pointer_gestures + .gesture_pinch_begin::(self, event); + } + } + + fn gesture_pinch_update(&self, seat: &Seat, _data: &mut D, event: &GesturePinchUpdateEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer + .arc + .wp_pointer_gestures + .gesture_pinch_update::(self, event); + } + } + + fn gesture_pinch_end(&self, seat: &Seat, _data: &mut D, event: &GesturePinchEndEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer + .arc + .wp_pointer_gestures + .gesture_pinch_end::(self, event); + } + } + + fn gesture_hold_begin(&self, seat: &Seat, _data: &mut D, event: &GestureHoldBeginEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer + .arc + .wp_pointer_gestures + .gesture_hold_begin::(self, event); + } + } + + fn gesture_hold_end(&self, seat: &Seat, _data: &mut D, event: &GestureHoldEndEvent) { + if let Some(pointer) = seat.get_pointer() { + pointer.arc.wp_pointer_gestures.gesture_hold_end::(self, event); + } + } +} + +/// User data for pointer +#[derive(Debug)] +pub struct PointerUserData { + pub(crate) arc: std::sync::Weak>, + pub(crate) client_scale: Arc, +} + +impl Dispatch2 for PointerUserData +where + D: SeatHandler, + ::PointerFocus: WaylandFocus, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + pointer: &WlPointer, + request: wl_pointer::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + Request::SetCursor { + serial, + surface, + hotspot_x, + hotspot_y, + } => { + let handle = match self.arc.upgrade() { + Some(arc) => PointerHandle { arc }, + None => return, + }; + + if !allow_setting_cursor(&handle, Serial(serial), &pointer.id()) { + return; + } + + let cursor_image = match surface { + Some(surface) => { + // tolerate re-using the same surface + if compositor::give_role(&surface, CURSOR_IMAGE_ROLE).is_err() + && compositor::get_role(&surface) != Some(CURSOR_IMAGE_ROLE) + { + pointer.post_error(wl_pointer::Error::Role, "Given wl_surface has another role."); + return; + } + + compositor::with_states(&surface, |states| { + states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(CursorImageAttributes { + hotspot: (0, 0).into(), + }) + }); + let client_scale = pointer + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + let hotspot = Point::::from((hotspot_x, hotspot_y)) + .to_f64() + .to_logical(client_scale) + .to_i32_round(); + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot = hotspot; + }); + + CursorImageStatus::Surface(surface) + } + None => CursorImageStatus::Hidden, + }; + + let seat = state + .seat_state() + .seats + .iter() + .find(|seat| seat.get_pointer().map(|h| h == handle).unwrap_or(false)) + .cloned(); + + if let Some(seat) = seat { + state.cursor_image(&seat, cursor_image) + } + } + Request::Release => { + // Our destructors already handle it + } + _ => unreachable!(), + }; + } + + fn destroyed(&self, _state: &mut D, _: ClientId, pointer: &WlPointer) { + if let Some(arc) = self.arc.upgrade() { + arc.wl_pointer + .known_pointers + .lock() + .unwrap() + .retain(|p| p.id() != pointer.id()); + } + } +} + +impl From for WlAxis { + #[inline] + fn from(axis: Axis) -> WlAxis { + match axis { + Axis::Horizontal => WlAxis::HorizontalScroll, + Axis::Vertical => WlAxis::VerticalScroll, + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Unknown Axis {0:?}")] +pub struct UnknownAxis(WlAxis); + +impl TryFrom for Axis { + type Error = UnknownAxis; + #[inline] + fn try_from(value: WlAxis) -> Result { + match value { + WlAxis::HorizontalScroll => Ok(Axis::Horizontal), + WlAxis::VerticalScroll => Ok(Axis::Vertical), + x => Err(UnknownAxis(x)), + } + } +} + +impl From for WlAxisSource { + #[inline] + fn from(axis: AxisSource) -> WlAxisSource { + match axis { + AxisSource::Wheel => WlAxisSource::Wheel, + AxisSource::Finger => WlAxisSource::Finger, + AxisSource::Continuous => WlAxisSource::Continuous, + AxisSource::WheelTilt => WlAxisSource::WheelTilt, + } + } +} + +impl From for WlButtonState { + #[inline] + fn from(state: ButtonState) -> WlButtonState { + match state { + ButtonState::Pressed => WlButtonState::Pressed, + ButtonState::Released => WlButtonState::Released, + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Unknown ButtonState {0:?}")] +pub struct UnknownButtonState(WlButtonState); + +impl TryFrom for ButtonState { + type Error = UnknownButtonState; + #[inline] + fn try_from(value: WlButtonState) -> Result { + match value { + WlButtonState::Pressed => Ok(ButtonState::Pressed), + WlButtonState::Released => Ok(ButtonState::Released), + x => Err(UnknownButtonState(x)), + } + } +} + +pub(crate) fn allow_setting_cursor(handle: &PointerHandle, serial: Serial, object_id: &ObjectId) -> bool +where + D: SeatHandler + 'static, + ::PointerFocus: WaylandFocus, +{ + // Allow client if there is a pointer grab for that client. Like drag and drop. + if handle + .grab_start_data() + .and_then(|data| data.focus) + .is_some_and(|focus| focus.0.same_client_as(object_id)) + { + return true; + } + + if !handle + .arc + .wl_pointer + .last_enter + .lock() + .unwrap() + .as_ref() + .is_some_and(|last_serial| *last_serial == serial) + { + return false; // Ignore mismatches in serial + } + + // Only allow setting the cursor icon if the current pointer focus is of the same + // client. + handle + .arc + .inner + .lock() + .unwrap() + .focus + .as_ref() + .is_some_and(|(focus, _)| focus.same_client_as(object_id)) +} diff --git a/vendor/smithay/src/wayland/seat/touch.rs b/vendor/smithay/src/wayland/seat/touch.rs new file mode 100644 index 0000000..ae36cd8 --- /dev/null +++ b/vendor/smithay/src/wayland/seat/touch.rs @@ -0,0 +1,248 @@ +use std::sync::{Arc, Weak, atomic::Ordering}; + +use portable_atomic::AtomicF64; +use wayland_server::{ + Client, DisplayHandle, Resource, + backend::ClientId, + protocol::wl_touch::{self, WlTouch}, +}; + +use super::SeatHandler; +use crate::wayland::Dispatch2; +use crate::wayland::compositor::CompositorHandler; +use crate::wayland::seat::wl_surface::WlSurface; +use crate::{input::touch::TouchHandle, wayland::seat::WaylandFocus}; +use crate::{ + input::touch::{TouchRc, TouchTarget}, + utils::{Clock, Monotonic}, +}; +use crate::{ + input::{ + Seat, + touch::{DownEvent, FrameMarker, MotionEvent, OrientationEvent, ShapeEvent, UpEvent}, + }, + utils::iter::new_locked_obj_iter_from_vec, +}; + +impl TouchHandle +where + ::TouchFocus: WaylandFocus, +{ + pub(crate) fn new_touch(&self, touch: WlTouch) { + let mut guard = self.arc.known_instances.lock().unwrap(); + guard.push(touch.downgrade()); + + let mut time = None; + let data = touch.data::>().unwrap(); + let guard = self.arc.inner.lock().unwrap(); + let mut sent = false; + for (slot, state) in &guard.focus { + if let Some((focus, location)) = &state.focus { + if focus.same_client_as(&touch.id()) { + if let Some(surface) = focus.wl_surface() { + if let Some(arc) = data.arc.upgrade() { + let serial = arc.last_down.lock().unwrap()[slot]; + let time = + *time.get_or_insert_with(|| Clock::::new().now().as_millis()); + let client_scale = data.client_scale.load(Ordering::Acquire); + let location = (state.location - *location).to_client(client_scale); + + touch.down( + serial.into(), + time, + &surface, + (*slot).into(), + location.x, + location.y, + ); + sent = true; + + // TODO: send shape, orientation. + } + } + } + } + } + + if sent { + touch.frame(); + } + } +} + +impl TouchHandle { + /// Attempt to retrieve a [`TouchHandle`] from an existing resource + /// + /// May return `None` for a valid `WlTouch` that was created without + /// the touch capability, or if capability has since been removed. + pub fn from_resource(seat: &WlTouch) -> Option { + Some(Self { + arc: seat.data::>()?.arc.upgrade()?, + }) + } + + /// Return all raw [`WlTouch`] instances for a particular [`Client`] + pub fn client_touch<'a>(&'a self, client: &Client) -> impl Iterator + 'a { + let guard = self.arc.known_instances.lock().unwrap(); + new_locked_obj_iter_from_vec(guard, client.id()) + } +} + +fn for_each_focused_touch( + seat: &Seat, + surface: &WlSurface, + mut f: impl FnMut(WlTouch), +) { + if let Some(touch) = seat.get_touch() { + let mut inner = touch.arc.known_instances.lock().unwrap(); + for ptr in &mut *inner { + let Ok(ptr) = ptr.upgrade() else { + continue; + }; + + if ptr.id().same_client_as(&surface.id()) { + f(ptr.clone()); + } + } + } +} + +fn set_touch_frame_marker( + data: &D, + surface: &WlSurface, + marker: FrameMarker, +) { + if let Some(client) = surface.client().as_ref() { + data.client_compositor_state(client).set_last_touch_frame(marker); + } +} + +#[cfg(feature = "wayland_frontend")] +impl TouchTarget for WlSurface +where + D: SeatHandler + 'static, + D: CompositorHandler, +{ + fn down(&self, seat: &Seat, _data: &mut D, event: &DownEvent) { + let serial = event.serial; + let slot = event.slot; + + if let Some(touch) = seat.get_touch() { + touch.arc.last_down.lock().unwrap().insert(slot, serial); + } + + for_each_focused_touch(seat, self, |touch| { + let client_scale = touch + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + let location = event.location.to_client(client_scale); + touch.down( + serial.into(), + event.time.millis(), + self, + slot.into(), + location.x, + location.y, + ); + }) + } + + fn up(&self, seat: &Seat, _data: &mut D, event: &UpEvent) { + let serial = event.serial; + let slot = event.slot; + for_each_focused_touch(seat, self, |touch| { + touch.up(serial.into(), event.time.millis(), slot.into()); + }); + + if let Some(touch) = seat.get_touch() { + touch.arc.last_down.lock().unwrap().remove(&slot); + } + } + + fn motion(&self, seat: &Seat, _data: &mut D, event: &MotionEvent) { + let slot = event.slot; + for_each_focused_touch(seat, self, |touch| { + let client_scale = touch + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + let location = event.location.to_client(client_scale); + touch.motion(event.time.millis(), slot.into(), location.x, location.y); + }) + } + + fn frame(&self, seat: &Seat, data: &mut D, marker: FrameMarker) { + set_touch_frame_marker(data, self, marker); + + for_each_focused_touch(seat, self, |touch| { + touch.frame(); + }) + } + + fn cancel(&self, seat: &Seat, data: &mut D, marker: FrameMarker) { + set_touch_frame_marker(data, self, marker); + + for_each_focused_touch(seat, self, |touch| { + touch.cancel(); + }); + + if let Some(touch) = seat.get_touch() { + touch.arc.last_down.lock().unwrap().clear(); + } + } + + fn shape(&self, seat: &Seat, _data: &mut D, event: &ShapeEvent) { + let slot = event.slot; + for_each_focused_touch(seat, self, |touch| { + touch.shape(slot.into(), event.major, event.minor); + }) + } + + fn orientation(&self, seat: &Seat, _data: &mut D, event: &OrientationEvent) { + let slot = event.slot; + for_each_focused_touch(seat, self, |touch| { + touch.orientation(slot.into(), event.orientation); + }) + } + + fn last_frame(&self, _seat: &Seat, data: &mut D) -> Option { + self.client() + .and_then(|c| data.client_compositor_state(&c).last_touch_frame()) + } +} + +/// User data for touch +#[derive(Debug)] +pub struct TouchUserData { + pub(crate) arc: Weak>, + pub(crate) client_scale: Arc, +} + +impl Dispatch2 for TouchUserData +where + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &WlTouch, + _request: wl_touch::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + } + + fn destroyed(&self, _state: &mut D, _client_id: ClientId, touch: &WlTouch) { + if let Some(arc) = self.arc.upgrade() { + arc.known_instances + .lock() + .unwrap() + .retain(|p| p.id() != touch.id()); + } + } +} diff --git a/vendor/smithay/src/wayland/security_context/listener_source.rs b/vendor/smithay/src/wayland/security_context/listener_source.rs new file mode 100644 index 0000000..33c2198 --- /dev/null +++ b/vendor/smithay/src/wayland/security_context/listener_source.rs @@ -0,0 +1,81 @@ +// TODO calloop source +// - poll POLLHUP/POLLIN on close_fd +// - poll for accept + +use calloop::{ + EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory, generic::Generic, +}; +use std::{ + io, + os::unix::{ + io::OwnedFd, + net::{UnixListener, UnixStream}, + }, +}; + +/// Security context listener event source. +/// +/// This implements [`EventSource`] and may be inserted into an event loop. +#[derive(Debug)] +pub struct SecurityContextListenerSource { + listen_fd: Generic, + close_fd: Generic, +} + +impl SecurityContextListenerSource { + pub(super) fn new(listen_fd: UnixListener, close_fd: OwnedFd) -> io::Result { + listen_fd.set_nonblocking(true)?; + let listen_fd = Generic::new(listen_fd, Interest::READ, Mode::Level); + // close_fd.set_nonblocking(true); + // XXX POLLHUP + let close_fd = Generic::new(close_fd, Interest::READ, Mode::Level); + Ok(Self { listen_fd, close_fd }) + } +} + +impl EventSource for SecurityContextListenerSource { + type Event = UnixStream; + type Metadata = (); + type Ret = (); + type Error = io::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> io::Result { + self.listen_fd.process_events(readiness, token, |_, socket| { + loop { + match socket.accept() { + Ok((stream, _)) => callback(stream, &mut ()), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + break; + } + Err(e) => { + return Err(e); + } + } + } + Ok(PostAction::Continue) + })?; + + self.close_fd + .process_events(readiness, token, |_, _fd| Ok(PostAction::Remove)) + } + + fn register(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.listen_fd.register(poll, token_factory)?; + self.close_fd.register(poll, token_factory) + } + + fn reregister(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.listen_fd.reregister(poll, token_factory)?; + self.close_fd.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.close_fd.unregister(poll)?; + self.listen_fd.unregister(poll) + } +} diff --git a/vendor/smithay/src/wayland/security_context/mod.rs b/vendor/smithay/src/wayland/security_context/mod.rs new file mode 100644 index 0000000..dd99377 --- /dev/null +++ b/vendor/smithay/src/wayland/security_context/mod.rs @@ -0,0 +1,228 @@ +//! Utilities for handling the security context protocol + +use std::{ + os::unix::{io::OwnedFd, net::UnixListener}, + sync::Mutex, +}; +use tracing::error; +use wayland_protocols::wp::security_context::v1::server::{ + wp_security_context_manager_v1::{self, WpSecurityContextManagerV1}, + wp_security_context_v1::{self, WpSecurityContextV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId}, +}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +mod listener_source; +pub use listener_source::SecurityContextListenerSource; + +const MANAGER_VERSION: u32 = 1; + +/// Handler for security context protocol +pub trait SecurityContextHandler { + /// A client has created a security context. `source` is a callop `EventSource` that listens on + /// the socket and produces streams when clients connect. `context` has the metadata associated + /// with the security context. + fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext); +} + +/// State of the [WpSecurityContextManagerV1] global +#[derive(Debug)] +pub struct SecurityContextState { + global: GlobalId, +} + +/// User data for a `WpSecurityContextV1` +#[derive(Debug)] +pub struct SecurityContextUserData(Mutex>); + +#[derive(Debug)] +struct SecurityContextBuilder { + listen_fd: UnixListener, + close_fd: OwnedFd, + sandbox_engine: Option, + app_id: Option, + instance_id: Option, +} + +/// A security context associated with a listener created through security +/// context protocol, and with clients connected through that listener. +#[derive(Clone, Debug)] +pub struct SecurityContext { + /// Name of the sandbox engine associated with the security context + pub sandbox_engine: Option, + /// Opaque sandbox-specific ID for an application + pub app_id: Option, + /// Opaque sandbox-specific ID for an instance of an application + pub instance_id: Option, + /// Client that created the security context + pub creator_client_id: ClientId, +} + +impl SecurityContextState { + /// Register new [WpSecurityContextManagerV1] global + /// + /// Filter determines if what clients see the global. It *must* exclude clients + /// created through a security context for the protocol to be correct and secure. + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let global_data = SecurityContextGlobalData { + filter: Box::new(filter), + }; + let global = display.create_global::(MANAGER_VERSION, global_data); + + Self { global } + } + + /// [WpSecurityContextManagerV1] GlobalId getter + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +/// User data for a `WpSecurityContextManagerV1` global +#[allow(missing_debug_implementations)] +pub struct SecurityContextGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +impl GlobalDispatch2 for SecurityContextGlobalData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _manager: &WpSecurityContextManagerV1, + request: wp_security_context_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wp_security_context_manager_v1::Request::CreateListener { + id, + listen_fd, + close_fd, + } => { + let data = SecurityContextUserData(Mutex::new(Some(SecurityContextBuilder { + listen_fd: UnixListener::from(listen_fd), + close_fd, + sandbox_engine: None, + app_id: None, + instance_id: None, + }))); + data_init.init(id, data); + } + wp_security_context_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for SecurityContextUserData +where + D: SecurityContextHandler + 'static, +{ + fn request( + &self, + state: &mut D, + client: &wayland_server::Client, + context: &WpSecurityContextV1, + request: wp_security_context_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let mut data = self.0.lock().unwrap(); + + if matches!(request, wp_security_context_v1::Request::Destroy) { + return; + } + + let Some(builder) = &mut *data else { + context.post_error( + wp_security_context_v1::Error::AlreadyUsed, + "Security context already used", + ); + return; + }; + + match request { + wp_security_context_v1::Request::SetSandboxEngine { name } => { + if builder.sandbox_engine.is_some() { + context.post_error( + wp_security_context_v1::Error::AlreadySet, + "Security context already has a sandbox engine", + ); + } + builder.sandbox_engine = Some(name); + } + wp_security_context_v1::Request::SetAppId { app_id } => { + if builder.app_id.is_some() { + context.post_error( + wp_security_context_v1::Error::AlreadySet, + "Security context already has an app id", + ); + } + builder.app_id = Some(app_id); + } + wp_security_context_v1::Request::SetInstanceId { instance_id } => { + if builder.instance_id.is_some() { + context.post_error( + wp_security_context_v1::Error::AlreadySet, + "Security context already has an instance id", + ); + } + builder.instance_id = Some(instance_id); + } + wp_security_context_v1::Request::Commit => { + let builder = data.take().unwrap(); + let listener_source = SecurityContextListenerSource::new(builder.listen_fd, builder.close_fd); + let security_context = SecurityContext { + sandbox_engine: builder.sandbox_engine, + app_id: builder.app_id, + instance_id: builder.instance_id, + creator_client_id: client.id(), + }; + match listener_source { + Ok(listener_source) => state.context_created(listener_source, security_context), + Err(e) => { + error!(error = ?e, "Failed to create security context listener source"); + } + } + } + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/selection/data_device/device.rs b/vendor/smithay/src/wayland/selection/data_device/device.rs new file mode 100644 index 0000000..c9542e0 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/data_device/device.rs @@ -0,0 +1,209 @@ +use std::cell::RefCell; + +use tracing::debug; +use wayland_server::{ + Client, DataInit, DisplayHandle, Resource, + protocol::{ + wl_data_device::{self, WlDataDevice}, + wl_seat::WlSeat, + }, +}; + +use crate::{ + input::{Seat, SeatHandler, dnd::DndFocus}, + utils::Serial, + wayland::{ + Dispatch2, compositor, + seat::WaylandFocus, + selection::{ + SelectionTarget, + device::SelectionDevice, + offer::OfferReplySource, + seat_data::SeatData, + source::{SelectionSource, SelectionSourceProvider}, + }, + }, +}; + +use super::{DataDeviceHandler, GrabType}; + +/// WlSurface role of drag and drop icon +pub const DND_ICON_ROLE: &str = "dnd_icon"; + +#[doc(hidden)] +#[derive(Debug)] +pub struct DataDeviceUserData { + pub(crate) wl_seat: WlSeat, +} + +impl Dispatch2 for DataDeviceUserData +where + D: DataDeviceHandler, + D: SeatHandler, + ::PointerFocus: DndFocus, + ::TouchFocus: DndFocus, + ::KeyboardFocus: WaylandFocus, + D: 'static, +{ + fn request( + &self, + handler: &mut D, + client: &Client, + resource: &WlDataDevice, + request: wl_data_device::Request, + dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let seat = match Seat::::from_resource(&self.wl_seat) { + Some(seat) => seat, + None => return, + }; + + match request { + wl_data_device::Request::StartDrag { + source, + origin, + icon, + serial, + } => { + // NOTE: While protocol states that selection shouldn't be used more than once, + // no-one enforces it, thus we have clients around that do so and crashing them + // doesn't worth it at this point. + if let Some(source) = source.as_ref() { + handler + .data_device_state() + .used_sources + .insert(source.clone(), self.wl_seat.clone()); + } + + let serial = Serial::from(serial); + if let Some(pointer) = seat.get_pointer() { + if pointer.has_grab(serial) { + if let Some(ref icon) = icon { + if compositor::give_role(icon, DND_ICON_ROLE).is_err() { + resource.post_error( + wl_data_device::Error::Role, + "Given surface already has an other role", + ); + return; + } + } + // The StartDrag is in response to a pointer implicit grab, all is good + if let Some(source) = source { + handler.dnd_requested( + source, + icon.clone(), + seat.clone(), + serial, + GrabType::Pointer, + ); + } else { + handler.dnd_requested( + origin, + icon.clone(), + seat.clone(), + serial, + GrabType::Pointer, + ); + } + + return; + } + } + if let Some(touch) = seat.get_touch() { + if touch.has_grab(serial) { + if let Some(ref icon) = icon { + if compositor::give_role(icon, DND_ICON_ROLE).is_err() { + resource.post_error( + wl_data_device::Error::Role, + "Given surface already has an other role", + ); + return; + } + } + // The StartDrag is in response to a touch implicit grab, all is good + if let Some(source) = source { + handler.dnd_requested( + source, + icon.clone(), + seat.clone(), + serial, + GrabType::Touch, + ); + } else { + handler.dnd_requested( + origin, + icon.clone(), + seat.clone(), + serial, + GrabType::Touch, + ); + } + return; + } + } + debug!(serial = ?serial, client = ?client, "denying drag from client without implicit grab"); + } + wl_data_device::Request::SetSelection { source, .. } => { + let seat_data = match seat.get_keyboard() { + Some(keyboard) if keyboard.client_of_object_has_focus(&resource.id()) => seat + .user_data() + .get::>>() + .unwrap(), + _ => { + debug!( + client = ?client, + "denying setting selection by a non-focused client" + ); + return; + } + }; + + // NOTE: While protocol states that selection shouldn't be used more than once, + // no-one enforces it, thus we have clients around that do so and crashing them + // doesn't worth it at this point. + if let Some(source) = source.as_ref() { + handler + .data_device_state() + .used_sources + .insert(source.clone(), self.wl_seat.clone()); + } + + let source = source.map(SelectionSourceProvider::DataDevice); + + handler.new_selection( + SelectionTarget::Clipboard, + source.clone().map(|provider| SelectionSource { provider }), + seat.clone(), + ); + + // The client has kbd focus, it can set the selection + seat_data + .borrow_mut() + .set_clipboard_selection::(dh, source.map(OfferReplySource::Client)); + } + wl_data_device::Request::Release => seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut() + .retain_devices(|ndd| match ndd { + SelectionDevice::DataDevice(ndd) => ndd != resource, + _ => true, + }), + + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: wayland_server::backend::ClientId, resource: &WlDataDevice) { + if let Some(seat) = Seat::::from_resource(&self.wl_seat) { + if let Some(seat_data) = seat.user_data().get::>>() { + seat_data.borrow_mut().retain_devices(|ndd| match ndd { + SelectionDevice::DataDevice(ndd) => ndd != resource, + _ => true, + }); + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/data_device/mod.rs b/vendor/smithay/src/wayland/selection/data_device/mod.rs new file mode 100644 index 0000000..3eaa998 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/data_device/mod.rs @@ -0,0 +1,846 @@ +//! Utilities for manipulating the data devices +//! +//! The data device is wayland's abstraction to represent both selection (copy/paste) and +//! drag'n'drop actions. This module provides logic to handle this part of the protocol. +//! Selection and drag'n'drop are per-seat notions. +//! +//! This module provides the freestanding [`set_data_device_focus`] function: +//! This function sets the data device focus for a given seat; you'd typically call it +//! whenever the keyboard focus changes, to follow it (for example in the focus hook of your keyboards). +//! +//! Using these two functions is enough for your clients to be able to interact with each other using +//! the data devices. +//! +//! The module also provides additional mechanisms allowing your compositor to see and interact with +//! the contents of the data device: +//! +//! - the freestanding function [`set_data_device_selection`] +//! allows you to set the contents of the selection for your clients +//! +//! The module defines the role `"dnd_icon"` that is assigned to surfaces used as drag'n'drop icons. +//! +//! ## Initialization +//! +//! To initialize this implementation, create the [`DataDeviceState`], store it inside your `State` struct +//! and implement the [`DataDeviceHandler`] and [`SelectionHandler`], as shown in this example: +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::selection::SelectionHandler; +//! use smithay::wayland::selection::data_device::{WaylandDndGrabHandler, DataDeviceState, DataDeviceHandler}; +//! # use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! # struct State { data_device_state: DataDeviceState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the data_device state +//! let data_device_state = DataDeviceState::new::( +//! &display.handle(), +//! ); +//! +//! // insert the DataDeviceState into your state +//! // .. +//! +//! // implement the necessary traits +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! # impl SeatHandler for State { +//! # type KeyboardFocus = WlSurface; +//! # type PointerFocus = WlSurface; +//! # type TouchFocus = WlSurface; +//! # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! # } +//! # impl PointerConstraintsHandler for State {} +//! impl WaylandDndGrabHandler for State { +//! // ... implement `dnd_requested` to handle drag&drop operations +//! } +//! impl SelectionHandler for State { +//! type SelectionUserData = (); +//! } +//! impl DataDeviceHandler for State { +//! fn data_device_state(&mut self) -> &mut DataDeviceState { &mut self.data_device_state } +//! // ... override default implementations here to customize handling ... +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` + +use std::{ + cell::{Ref, RefCell}, + collections::HashMap, + os::unix::io::OwnedFd, + sync::{Arc, Mutex}, +}; + +use smallvec::SmallVec; +use tracing::instrument; +use wayland_server::{ + Client, DisplayHandle, GlobalDispatch, Resource, + backend::{ClientId, GlobalId, Handle, ObjectData, ObjectId, protocol::Message}, + protocol::{ + wl_data_device_manager::{DndAction as WlDndAction, WlDataDeviceManager}, + wl_data_offer::{self, WlDataOffer}, + wl_data_source::WlDataSource, + wl_seat::WlSeat, + wl_surface::WlSurface, + }, +}; + +use crate::{ + backend::input::InputTime, + input::{ + Seat, SeatHandler, + dnd::{DndAction, DndFocus, GrabType, OfferData, Source}, + }, + utils::{Logical, Point, Serial}, + wayland::GlobalData, +}; + +mod device; +mod source; + +pub use device::{DND_ICON_ROLE, DataDeviceUserData}; +pub use source::DataSourceUserData; + +use super::{ + SelectionHandler, SelectionTarget, offer::OfferReplySource, seat_data::SeatData, + source::CompositorSelectionProvider, +}; + +/// Events that are generated by interactions of the clients with the data device +#[allow(unused_variables)] +pub trait DataDeviceHandler: Sized + SelectionHandler + WaylandDndGrabHandler { + /// [DataDeviceState] getter + fn data_device_state(&mut self) -> &mut DataDeviceState; + + /// Action chooser for DnD negotiation + fn action_choice(&mut self, available: WlDndAction, preferred: WlDndAction) -> WlDndAction { + default_action_chooser(available, preferred) + } +} + +/// Handler for wayland client initiated drag'n'drop +#[allow(unused_variables)] +pub trait WaylandDndGrabHandler: SeatHandler + Sized { + /// A client requested a drag'n'drop as response to a user pointer action + /// + /// * `source` - The data source provided by the client. + /// * `icon` - The icon the client requested to be used to be associated with the cursor icon + /// during the drag'n'drop. + /// * `seat` - The seat on which the DnD operation was started + /// * `serial` - Serial passed and validated for the grab + /// * `type_` - If the grab was validated for pointer or touch input + fn dnd_requested( + &mut self, + source: S, + icon: Option, + seat: Seat, + serial: Serial, + type_: GrabType, + ) { + source.cancel(); + let _ = (icon, seat, type_); + } +} + +impl DndAction { + fn unwrap_single(slice: &[Self]) -> Option { + if slice.is_empty() { + Some(Self::None) + } else if slice.len() == 1 { + Some(slice[0]) + } else { + None + } + } + + fn vec_from_wl(value: WlDndAction) -> SmallVec<[Self; 3]> { + let mut vec = SmallVec::new(); + if value.contains(WlDndAction::Ask) { + vec.push(Self::Ask); + } + if value.contains(WlDndAction::Copy) { + vec.push(Self::Copy); + } + if value.contains(WlDndAction::Move) { + vec.push(Self::Move); + } + vec + } + + fn convert_slice(value: &[Self]) -> WlDndAction { + let mut result = WlDndAction::None; + for action in value.iter().copied() { + result |= action.into(); + } + result + } +} + +impl From for WlDndAction { + fn from(value: DndAction) -> Self { + match value { + DndAction::Ask => WlDndAction::Ask, + DndAction::Copy => WlDndAction::Copy, + DndAction::Move => WlDndAction::Move, + _ => WlDndAction::None, + } + } +} + +#[derive(Debug)] +struct WlOfferState { + active: bool, + dropped: bool, + accepted: bool, + finished: bool, + chosen_action: WlDndAction, +} + +struct WlDndDataOffer { + state: Arc>, + source: Mutex>>, +} + +impl ObjectData for WlDndDataOffer +where + D: DataDeviceHandler, + D: 'static, + S: Source, +{ + fn request( + self: Arc, + dh: &Handle, + handler: &mut D, + _client_id: ClientId, + msg: Message, + ) -> Option>> { + let dh = DisplayHandle::from(dh.clone()); + if let Ok((resource, request)) = WlDataOffer::parse_request(&dh, msg) { + handle_dnd(handler, &resource, request, &self); + } + + None + } + + fn destroyed( + self: Arc, + _handle: &Handle, + _data: &mut D, + _client_id: ClientId, + _object_id: ObjectId, + ) { + } +} + +fn handle_dnd( + handler: &mut D, + offer: &WlDataOffer, + request: wl_data_offer::Request, + data: &WlDndDataOffer, +) where + D: DataDeviceHandler, + D: 'static, + S: Source, +{ + use self::wl_data_offer::Request; + let mut source = data.source.lock().unwrap(); + let mut data = data.state.lock().unwrap(); + match request { + Request::Accept { mime_type, .. } => { + if let Some(source) = source.as_ref() { + data.accepted = match &mime_type { + Some(mtype) => source + .metadata() + .is_some_and(|meta| meta.mime_types.contains(mtype)), + None => false, + }; + source.accepted(mime_type); + } else if data.finished { + offer.post_error( + wl_data_offer::Error::InvalidFinish, + "Accept request after Finish.", + ); + } + } + Request::Receive { mime_type, fd } => { + if let Some(source) = source.as_ref() { + // check if the source and associated mime type is still valid + let valid = source + .metadata() + .is_some_and(|meta| meta.mime_types.contains(&mime_type)) + && source.alive() + && data.active; + if valid { + source.send(&mime_type, fd); + } + } else if data.finished { + offer.post_error( + wl_data_offer::Error::InvalidFinish, + "Receive request after Finish.", + ); + } + } + Request::Destroy => { + if data.dropped && !data.finished { + if let Some(source) = source.as_ref() { + source.cancel(); + } + } + } + Request::Finish => { + if data.chosen_action.is_empty() { + offer.post_error( + wl_data_offer::Error::InvalidFinish, + "Cannot finish a data offer with no valid action.", + ); + return; + } + if let Some(source) = source.take() { + if data.accepted { + source.finished(); + } + } else if data.finished { + offer.post_error( + wl_data_offer::Error::InvalidFinish, + "Cannot finish a data offer, which was finished already.", + ); + } + data.active = false; + data.finished = true; + } + Request::SetActions { + dnd_actions, + preferred_action, + } => { + if let Some(source) = source.as_ref() { + let dnd_actions = dnd_actions.into_result().unwrap_or(WlDndAction::None); + let preferred_action = preferred_action.into_result().unwrap_or(WlDndAction::None); + + // preferred_action must only contain one bitflag at the same time + if ![ + WlDndAction::None, + WlDndAction::Move, + WlDndAction::Copy, + WlDndAction::Ask, + ] + .contains(&preferred_action) + { + offer.post_error(wl_data_offer::Error::InvalidAction, "Invalid preferred action."); + return; + } + + let source_actions = source + .metadata() + .map(|meta| DndAction::convert_slice(&meta.dnd_actions)) + .unwrap_or_else(WlDndAction::empty); + let possible_actions = source_actions & dnd_actions; + let chosen_action = handler.action_choice(possible_actions, preferred_action); + // check that the user provided callback respects that one precise action should be chosen + debug_assert!( + [ + WlDndAction::None, + WlDndAction::Move, + WlDndAction::Copy, + WlDndAction::Ask + ] + .contains(&chosen_action), + "Only one precise action should be chosen" + ); + if chosen_action != data.chosen_action { + data.chosen_action = chosen_action; + if offer.version() >= wl_data_offer::EVT_ACTION_SINCE { + offer.action(chosen_action); + } + source.choose_action( + DndAction::unwrap_single(&DndAction::vec_from_wl(chosen_action)) + .expect("We have selected a single value at this point."), + ); + } + } else if data.finished { + offer.post_error( + wl_data_offer::Error::InvalidFinish, + "SetActions request after Finish.", + ); + } + } + _ => unreachable!(), + } +} + +/// Type implementing [`OfferData`] for wl_data_offer based DnD sources +#[derive(Debug)] +pub struct WlOfferData { + state: Arc>, + source: Arc, + last_source_actions: SmallVec<[DndAction; 3]>, + #[allow(dead_code)] + wl_offers: Vec, +} + +impl OfferData for WlOfferData { + fn disable(&self) { + self.state.lock().unwrap().active = false; + self.source.choose_action(DndAction::None); + for offer in &self.wl_offers { + if let Some(data) = offer + .object_data() + .and_then(|arc| arc.downcast_ref::>()) + { + data.source.lock().unwrap().take(); + } + } + } + + fn drop(&self) { + self.state.lock().unwrap().dropped = true; + } + + fn validated(&self) -> bool { + let data = self.state.lock().unwrap(); + let requires_action = self + .wl_offers + .iter() + .any(|o| o.version() >= wl_data_offer::REQ_SET_ACTIONS_SINCE); + data.accepted && (!requires_action || !data.chosen_action.is_empty()) + } +} + +impl DndFocus for WlSurface { + type OfferData = WlOfferData; + + fn enter( + &self, + _data: &mut D, + dh: &DisplayHandle, + source: Arc, + seat: &Seat, + location: Point, + serial: &Serial, + ) -> Option> { + let seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow(); + let mut offers = Vec::with_capacity(1); + let offer_state = Arc::new(Mutex::new(WlOfferState { + active: true, + dropped: false, + accepted: true, + finished: false, + chosen_action: WlDndAction::empty(), + })); + + if source.is_client_local(self) { + for device in seat_data + .known_data_devices() + .filter(|d| d.id().same_client_as(&self.id())) + { + device.enter((*serial).into(), self, location.x, location.y, None); + } + None + } else if let Some(metadata) = source.metadata() { + let handle = dh.backend_handle(); + let client = handle.get_client(self.id()).ok()?; + + for device in seat_data + .known_data_devices() + .filter(|d| d.id().same_client_as(&self.id())) + { + // create a data offer + let offer = handle + .create_object::( + client.clone(), + WlDataOffer::interface(), + device.version(), + Arc::new(WlDndDataOffer { + state: offer_state.clone(), + source: Mutex::new(Some(source.clone())), + }), + ) + .unwrap(); + let offer = WlDataOffer::from_id(dh, offer).unwrap(); + + // advertize the offer to the client + device.data_offer(&offer); + + for mime_type in metadata.mime_types.iter().cloned() { + offer.offer(mime_type); + } + if offer.version() >= wl_data_offer::EVT_SOURCE_ACTIONS_SINCE { + offer.source_actions(DndAction::convert_slice(&metadata.dnd_actions)); + } + + device.enter((*serial).into(), self, location.x, location.y, Some(&offer)); + + offers.push(offer); + } + + Some(WlOfferData { + state: offer_state, + source, + wl_offers: offers, + last_source_actions: metadata.dnd_actions, + }) + } else { + None + } + } + + fn motion( + &self, + _data: &mut D, + offer: Option<&mut WlOfferData>, + seat: &Seat, + location: Point, + time: InputTime, + ) { + let seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow(); + + if let Some(offer) = offer { + if let Some(new_metadata) = offer.source.metadata() { + if offer.last_source_actions != new_metadata.dnd_actions { + for wl_offer in &offer.wl_offers { + if wl_offer.version() >= wl_data_offer::EVT_SOURCE_ACTIONS_SINCE { + wl_offer.source_actions(DndAction::convert_slice(&new_metadata.dnd_actions)); + } + } + + offer.last_source_actions = new_metadata.dnd_actions; + } + }; + } + + for device in seat_data.known_data_devices() { + if device.id().same_client_as(&self.id()) { + device.motion(time.millis(), location.x, location.y); + } + } + } + + fn leave(&self, _data: &mut D, _offer: Option<&mut WlOfferData>, seat: &Seat) { + let seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow(); + for device in seat_data.known_data_devices() { + if device.id().same_client_as(&self.id()) { + device.leave(); + } + } + } + + fn drop(&self, _data: &mut D, offer: Option<&mut WlOfferData>, seat: &Seat) { + let seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow(); + for device in seat_data.known_data_devices() { + if let Some(ref offer) = offer { + if device.version() >= wl_data_offer::REQ_SET_ACTIONS_SINCE + && offer.state.lock().unwrap().chosen_action.is_empty() + { + continue; + } + } + if device.id().same_client_as(&self.id()) { + device.drop(); + } + } + } +} + +/// State of data device +#[derive(Debug)] +pub struct DataDeviceState { + manager_global: GlobalId, + /// Used sources. + /// + /// Protocol states that each source can only be used once. We + /// also use it during destruction to get seat data. + pub(crate) used_sources: HashMap, +} + +impl DataDeviceState { + /// Regiseter new [WlDataDeviceManager] global + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch + 'static, + D: DataDeviceHandler, + { + let manager_global = display.create_global::(3, GlobalData); + + Self { + manager_global, + used_sources: Default::default(), + } + } + + /// [WlDataDeviceManager] GlobalId getter + pub fn global(&self) -> GlobalId { + self.manager_global.clone() + } +} + +/// A simple action chooser for DnD negotiation +/// +/// If the preferred action is available, it'll pick it. Otherwise, it'll pick the first +/// available in the following order: Ask, Copy, Move. +pub fn default_action_chooser(available: WlDndAction, preferred: WlDndAction) -> WlDndAction { + // if the preferred action is valid (a single action) and in the available actions, use it + // otherwise, follow a fallback strategy + if [WlDndAction::Move, WlDndAction::Copy, WlDndAction::Ask].contains(&preferred) + && available.contains(preferred) + { + preferred + } else if available.contains(WlDndAction::Ask) { + WlDndAction::Ask + } else if available.contains(WlDndAction::Copy) { + WlDndAction::Copy + } else if available.contains(WlDndAction::Move) { + WlDndAction::Move + } else { + WlDndAction::empty() + } +} + +/// Set the data device focus to a certain client for a given seat +/// +/// If the focus is different from the existing focus, the current selection will be offered to the client. +#[instrument(name = "wayland_data_device", level = "debug", skip(dh, seat, client), fields(seat = seat.name(), client = ?client.as_ref().map(|c| c.id())))] +pub fn set_data_device_focus(dh: &DisplayHandle, seat: &Seat, client: Option) +where + D: SeatHandler + DataDeviceHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + seat_data.borrow_mut().set_clipboard_focus::(dh, client); +} + +/// Set a compositor-provided selection for this seat +/// +/// You need to provide the available mime types for this selection. +/// +/// Whenever a client requests to read the selection, your callback will +/// receive a [`SelectionHandler::send_selection`] event. +#[instrument(name = "wayland_data_device", level = "debug", skip(dh, seat, user_data), fields(seat = seat.name()))] +pub fn set_data_device_selection( + dh: &DisplayHandle, + seat: &Seat, + mime_types: Vec, + user_data: D::SelectionUserData, +) where + D: SeatHandler + DataDeviceHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + + let selection = OfferReplySource::Compositor(CompositorSelectionProvider { + ty: SelectionTarget::Clipboard, + mime_types, + user_data, + }); + + seat_data + .borrow_mut() + .set_clipboard_selection::(dh, Some(selection)); +} + +/// Errors happening when requesting selection contents +#[derive(Debug, thiserror::Error)] +pub enum SelectionRequestError { + /// Requested mime type is not available + #[error("Requested mime type is not available")] + InvalidMimetype, + /// Requesting server side selection contents is not supported + #[error("Current selection is server-side")] + ServerSideSelection, + /// There is no active selection + #[error("No active selection to query")] + NoSelection, +} + +/// Request the current data_device selection of the given seat +/// to be written to the provided file descriptor in the given mime type. +pub fn request_data_device_client_selection( + seat: &Seat, + mime_type: String, + fd: OwnedFd, +) -> Result<(), SelectionRequestError> +where + D: SeatHandler + DataDeviceHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + match seat_data.borrow().get_clipboard_selection() { + None => Err(SelectionRequestError::NoSelection), + Some(OfferReplySource::Client(source)) => { + if !source.contains_mime_type(&mime_type) { + Err(SelectionRequestError::InvalidMimetype) + } else { + source.send(mime_type, fd); + Ok(()) + } + } + Some(OfferReplySource::Compositor(selection)) => { + if !selection.mime_types.contains(&mime_type) { + Err(SelectionRequestError::InvalidMimetype) + } else { + Err(SelectionRequestError::ServerSideSelection) + } + } + } +} + +/// Gets the user_data for the currently active selection, if set by the compositor +#[instrument(name = "wayland_data_device", level = "debug", skip_all, fields(seat = seat.name()))] +pub fn current_data_device_selection_userdata(seat: &Seat) -> Option> +where + D: SeatHandler + DataDeviceHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + Ref::filter_map(seat_data.borrow(), |data| match data.get_clipboard_selection() { + Some(OfferReplySource::Compositor(CompositorSelectionProvider { user_data, .. })) => Some(user_data), + _ => None, + }) + .ok() +} + +/// Clear the current selection for this seat +#[instrument(name = "wayland_data_device", level = "debug", skip_all, fields(seat = seat.name()))] +pub fn clear_data_device_selection(dh: &DisplayHandle, seat: &Seat) +where + D: SeatHandler + DataDeviceHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + seat_data.borrow_mut().set_clipboard_selection::(dh, None); +} + +mod handlers { + use std::cell::RefCell; + + use tracing::error; + use wayland_server::{ + Dispatch, DisplayHandle, + protocol::{ + wl_data_device::WlDataDevice, + wl_data_device_manager::{self, WlDataDeviceManager}, + wl_data_source::WlDataSource, + }, + }; + + use crate::{ + input::Seat, + wayland::selection::{device::SelectionDevice, seat_data::SeatData}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, + }; + + use super::DataDeviceHandler; + use super::{device::DataDeviceUserData, source::DataSourceUserData}; + + impl GlobalDispatch2 for GlobalData + where + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: DataDeviceHandler, + D: 'static, + { + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + } + + impl Dispatch2 for GlobalData + where + D: Dispatch, + D: Dispatch, + D: DataDeviceHandler, + D: 'static, + { + fn request( + &self, + _state: &mut D, + client: &wayland_server::Client, + _resource: &WlDataDeviceManager, + request: wl_data_device_manager::Request, + dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wl_data_device_manager::Request::CreateDataSource { id } => { + data_init.init(id, DataSourceUserData::new(dhandle.clone())); + } + wl_data_device_manager::Request::GetDataDevice { id, seat: wl_seat } => { + match Seat::::from_resource(&wl_seat) { + Some(seat) => { + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let device = SelectionDevice::DataDevice( + data_init.init(id, DataDeviceUserData { wl_seat }), + ); + + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + seat_data.borrow_mut().add_device(device); + } + None => { + error!(client = ?client, data_device = ?id, "Unmanaged seat given to a data device."); + } + } + } + _ => unreachable!(), + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/data_device/source.rs b/vendor/smithay/src/wayland/selection/data_device/source.rs new file mode 100644 index 0000000..09ac7eb --- /dev/null +++ b/vendor/smithay/src/wayland/selection/data_device/source.rs @@ -0,0 +1,176 @@ +use std::{ + any::Any, + cell::RefCell, + os::fd::{AsFd, OwnedFd}, + sync::Mutex, +}; +use tracing::{debug, error}; + +use wayland_server::{ + DisplayHandle, Resource, + backend::ClientId, + protocol::{ + wl_data_source::{self, WlDataSource}, + wl_surface::WlSurface, + }, +}; + +use crate::input::{ + Seat, + dnd::{DndAction, Source, SourceMetadata}, +}; +use crate::utils::{IsAlive, alive_tracker::AliveTracker}; +use crate::wayland::Dispatch2; +use crate::wayland::selection::offer::OfferReplySource; +use crate::wayland::selection::seat_data::SeatData; +use crate::wayland::selection::source::SelectionSourceProvider; + +use super::DataDeviceHandler; + +#[doc(hidden)] +#[derive(Debug)] +pub struct DataSourceUserData { + pub(crate) inner: Mutex, + alive_tracker: AliveTracker, + display_handle: DisplayHandle, +} + +impl DataSourceUserData { + pub(super) fn new(display_handle: DisplayHandle) -> Self { + Self { + inner: Default::default(), + alive_tracker: Default::default(), + display_handle, + } + } +} + +impl Dispatch2 for DataSourceUserData +where + D: DataDeviceHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &WlDataSource, + request: wl_data_source::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let mut data = self.inner.lock().unwrap(); + + match request { + wl_data_source::Request::Offer { mime_type } => { + data.mime_types.push(mime_type); + } + wl_data_source::Request::SetActions { dnd_actions } => match dnd_actions { + wayland_server::WEnum::Value(dnd_actions) => { + data.dnd_actions = DndAction::vec_from_wl(dnd_actions); + } + wayland_server::WEnum::Unknown(action) => { + error!("Unknown dnd_action: {:?}", action); + } + }, + wl_data_source::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, source: &WlDataSource) { + self.alive_tracker.destroy_notify(); + + // Remove the source from the used ones. + let seat = match state + .data_device_state() + .used_sources + .remove(source) + .as_ref() + .and_then(Seat::::from_resource) + { + Some(seat) => seat, + None => return, + }; + + let mut seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut(); + + match seat_data.get_clipboard_selection() { + Some(OfferReplySource::Client(SelectionSourceProvider::DataDevice(set_source))) + if set_source == source => + { + seat_data.set_clipboard_selection::(&self.display_handle, None) + } + _ => (), + } + } +} + +impl IsAlive for WlDataSource { + #[inline] + fn alive(&self) -> bool { + let data: &DataSourceUserData = self.data().unwrap(); + data.alive_tracker.alive() + } +} + +impl Source for WlDataSource { + fn metadata(&self) -> Option { + self.data::() + .map(|data| data.inner.lock().unwrap().clone()) + } + + fn choose_action(&self, action: DndAction) { + self.action(action.into()); + } + + fn send(&self, mime_type: &str, fd: OwnedFd) { + debug!(?mime_type, "DnD transfer request"); + self.send(mime_type.to_owned(), fd.as_fd()); + } + + fn drop_performed(&self) { + if self.version() >= wl_data_source::EVT_DND_DROP_PERFORMED_SINCE { + self.dnd_drop_performed(); + } + } + + fn cancel(&self) { + self.cancelled(); + } + + fn finished(&self) { + if self.version() >= wl_data_source::EVT_DND_FINISHED_SINCE { + self.dnd_finished(); + } + } +} + +impl Source for WlSurface { + fn is_client_local(&self, target: &dyn Any) -> bool { + target + .downcast_ref::() + .is_some_and(|target| target.id().same_client_as(&self.id())) + } + + fn metadata(&self) -> Option { + None + } + + fn choose_action(&self, action: DndAction) { + let _ = action; + } + + fn send(&self, mime_type: &str, fd: OwnedFd) { + let _ = (mime_type, fd); + unreachable!("Local dnd drops can't send"); + } + + fn drop_performed(&self) {} + fn cancel(&self) {} + fn finished(&self) {} +} diff --git a/vendor/smithay/src/wayland/selection/device.rs b/vendor/smithay/src/wayland/selection/device.rs new file mode 100644 index 0000000..088a64a --- /dev/null +++ b/vendor/smithay/src/wayland/selection/device.rs @@ -0,0 +1,120 @@ +use wayland_protocols::ext::data_control::v1::server::ext_data_control_device_v1::ExtDataControlDeviceV1; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1 as PrimaryDevice; +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; +use wayland_server::Resource; +use wayland_server::backend::ObjectId; +use wayland_server::protocol::wl_data_device::WlDataDevice; +use wayland_server::protocol::wl_seat::WlSeat; + +use super::data_device::DataDeviceUserData; +use super::ext_data_control::ExtDataControlDeviceUserData; +use super::offer::SelectionOffer; +use super::primary_selection::PrimaryDeviceUserData; +use super::private::selection_dispatch; +use super::wlr_data_control::DataControlDeviceUserData; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum DataDeviceKind { + Core, + Primary, + WlrDataControl, + ExtDataControl, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SelectionDevice { + DataDevice(WlDataDevice), + Primary(PrimaryDevice), + WlrDataControl(ZwlrDataControlDeviceV1), + ExtDataControl(ExtDataControlDeviceV1), +} + +impl SelectionDevice { + pub fn offer(&self, offer: &SelectionOffer) { + selection_dispatch!(self, offer; Self(device), SelectionOffer(offer) => device.data_offer(offer)) + } + + pub fn version(&self) -> u32 { + selection_dispatch!(self; Self(device) => device.version()) + } + + pub fn id(&self) -> ObjectId { + selection_dispatch!(self; Self(device) => device.id()) + } + + pub fn device_kind(&self) -> DataDeviceKind { + match self { + Self::DataDevice(_) => DataDeviceKind::Core, + Self::Primary(_) => DataDeviceKind::Primary, + Self::WlrDataControl(_) => DataDeviceKind::WlrDataControl, + Self::ExtDataControl(_) => DataDeviceKind::ExtDataControl, + } + } + + /// [`WlSeat`] associated with this device. + pub fn seat(&self) -> WlSeat { + match self { + SelectionDevice::DataDevice(device) => { + let data: &DataDeviceUserData = device.data().unwrap(); + data.wl_seat.clone() + } + SelectionDevice::Primary(device) => { + let data: &PrimaryDeviceUserData = device.data().unwrap(); + data.wl_seat.clone() + } + SelectionDevice::WlrDataControl(device) => { + let data: &DataControlDeviceUserData = device.data().unwrap(); + data.wl_seat.clone() + } + SelectionDevice::ExtDataControl(device) => { + let data: &ExtDataControlDeviceUserData = device.data().unwrap(); + data.wl_seat.clone() + } + } + } + + /// Send regular selection. + pub fn selection(&self, offer: &SelectionOffer) { + match (self, offer) { + (Self::DataDevice(device), SelectionOffer::DataDevice(offer)) => { + device.selection(Some(offer)); + } + (Self::WlrDataControl(obj), SelectionOffer::WlrDataControl(offer)) => obj.selection(Some(offer)), + (Self::ExtDataControl(obj), SelectionOffer::ExtDataControl(offer)) => obj.selection(Some(offer)), + _ => unreachable!("non-supported configuration for setting clipboard selection."), + } + } + + pub fn unset_selection(&self) { + match self { + Self::DataDevice(device) => device.selection(None), + Self::WlrDataControl(device) => device.selection(None), + Self::ExtDataControl(device) => device.selection(None), + Self::Primary(_) => unreachable!("primary clipboard has no clipboard selection"), + } + } + + pub fn primary_selection(&self, offer: &SelectionOffer) { + match (self, offer) { + (Self::Primary(device), SelectionOffer::Primary(offer)) => { + device.selection(Some(offer)); + } + (Self::WlrDataControl(obj), SelectionOffer::WlrDataControl(offer)) => { + obj.primary_selection(Some(offer)) + } + (Self::ExtDataControl(obj), SelectionOffer::ExtDataControl(offer)) => { + obj.primary_selection(Some(offer)) + } + _ => unreachable!("non-supported configuration for setting clipboard selection."), + } + } + + pub fn unset_primary_selection(&self) { + match self { + Self::Primary(device) => device.selection(None), + Self::WlrDataControl(device) => device.primary_selection(None), + Self::ExtDataControl(device) => device.primary_selection(None), + Self::DataDevice(_) => unreachable!("data control has primary selection"), + } + } +} diff --git a/vendor/smithay/src/wayland/selection/ext_data_control/device.rs b/vendor/smithay/src/wayland/selection/ext_data_control/device.rs new file mode 100644 index 0000000..a58f7c1 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/ext_data_control/device.rs @@ -0,0 +1,149 @@ +use std::cell::RefCell; + +use wayland_protocols::ext::data_control::v1::server::ext_data_control_device_v1::{ + self, ExtDataControlDeviceV1, +}; +use wayland_server::Resource; +use wayland_server::protocol::wl_seat::WlSeat; +use wayland_server::{Client, DisplayHandle}; + +use crate::input::Seat; +use crate::wayland::Dispatch2; +use crate::wayland::selection::device::SelectionDevice; +use crate::wayland::selection::offer::OfferReplySource; +use crate::wayland::selection::seat_data::SeatData; +use crate::wayland::selection::source::SelectionSourceProvider; +use crate::wayland::selection::{SelectionSource, SelectionTarget}; + +use super::DataControlHandler; + +#[doc(hidden)] +#[derive(Debug)] +pub struct ExtDataControlDeviceUserData { + pub(crate) primary: bool, + pub(crate) wl_seat: WlSeat, +} + +impl Dispatch2 for ExtDataControlDeviceUserData +where + D: DataControlHandler, + D: 'static, +{ + fn request( + &self, + handler: &mut D, + _client: &Client, + resource: &ExtDataControlDeviceV1, + request: ::Request, + dh: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, D>, + ) { + let seat = match Seat::::from_resource(&self.wl_seat) { + Some(seat) => seat, + None => return, + }; + + match request { + ext_data_control_device_v1::Request::SetSelection { source, .. } => { + // Each source can only be used once. + if let Some(source) = source.as_ref() { + if handler + .data_control_state() + .used_sources + .insert(source.clone(), self.wl_seat.clone()) + .is_some() + { + resource.post_error( + ext_data_control_device_v1::Error::UsedSource, + "selection source can be used only once.", + ); + return; + } + } + + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let source = source.map(SelectionSourceProvider::ExtDataControl); + + handler.new_selection( + SelectionTarget::Clipboard, + source.clone().map(|provider| SelectionSource { provider }), + seat.clone(), + ); + + seat.user_data() + .get::>>() + .unwrap() + .borrow_mut() + .set_clipboard_selection::(dh, source.map(OfferReplySource::Client)); + } + ext_data_control_device_v1::Request::SetPrimarySelection { source, .. } => { + // When the primary selection is disabled, we should simply ignore the requests. + if !self.primary { + return; + } + + // Each source can only be used once. + if let Some(source) = source.as_ref() { + if handler + .data_control_state() + .used_sources + .insert(source.clone(), self.wl_seat.clone()) + .is_some() + { + resource.post_error( + ext_data_control_device_v1::Error::UsedSource, + "selection source can be used only once.", + ); + return; + } + } + + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let source = source.map(SelectionSourceProvider::ExtDataControl); + + handler.new_selection( + SelectionTarget::Primary, + source.clone().map(|provider| SelectionSource { provider }), + seat.clone(), + ); + + seat.user_data() + .get::>>() + .unwrap() + .borrow_mut() + .set_primary_selection::(dh, source.map(OfferReplySource::Client)); + } + ext_data_control_device_v1::Request::Destroy => seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut() + .retain_devices(|ndd| match ndd { + SelectionDevice::ExtDataControl(ndd) => ndd != resource, + _ => true, + }), + + _ => unreachable!(), + } + } + + fn destroyed( + &self, + _state: &mut D, + _client: wayland_server::backend::ClientId, + resource: &ExtDataControlDeviceV1, + ) { + if let Some(seat) = Seat::::from_resource(&self.wl_seat) { + if let Some(seat_data) = seat.user_data().get::>>() { + seat_data.borrow_mut().retain_devices(|ndd| match ndd { + SelectionDevice::ExtDataControl(ndd) => ndd != resource, + _ => true, + }); + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/ext_data_control/mod.rs b/vendor/smithay/src/wayland/selection/ext_data_control/mod.rs new file mode 100644 index 0000000..2d4905e --- /dev/null +++ b/vendor/smithay/src/wayland/selection/ext_data_control/mod.rs @@ -0,0 +1,253 @@ +//! Automatic handling of the `ext_data_control` protocol +//! +//! ## Initialization +//! +//! To initialize this implementation, create [`DataControlState`], store it in your `State` +//! struct, and implement the required trait, as shown in the example: +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::selection::SelectionHandler; +//! use smithay::wayland::selection::ext_data_control::{DataControlState, DataControlHandler}; +//! # use smithay::input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}; +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! # struct State { data_control_state: DataControlState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the data_control state +//! let data_control_state = DataControlState::new::( +//! &display.handle(), None, |_| true +//! ); +//! +//! // insert the DataControlState into your state +//! // .. +//! +//! // implement the necessary traits +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! # impl SeatHandler for State { +//! # type KeyboardFocus = WlSurface; +//! # type PointerFocus = WlSurface; +//! # type TouchFocus = WlSurface; +//! # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! # } +//! # impl PointerConstraintsHandler for State {} +//! impl SelectionHandler for State { +//! type SelectionUserData = (); +//! } +//! impl DataControlHandler for State { +//! fn data_control_state(&mut self) -> &mut DataControlState { &mut self.data_control_state } +//! // ... override default implementations here to customize handling ... +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! Be aware that data control clients rely on other selection providers to be implemneted, like +//! wl_data_device or zwp_primary_selection. + +use std::collections::HashMap; + +use wayland_protocols::ext::data_control::v1::server::ext_data_control_manager_v1::ExtDataControlManagerV1; +use wayland_protocols::ext::data_control::v1::server::ext_data_control_source_v1::ExtDataControlSourceV1; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_seat::WlSeat; +use wayland_server::{Client, DisplayHandle, GlobalDispatch}; + +mod device; +mod source; + +pub use device::ExtDataControlDeviceUserData; +pub use source::ExtDataControlSourceUserData; + +use super::SelectionHandler; +use super::primary_selection::PrimarySelectionState; + +/// Access the data control state. +pub trait DataControlHandler: Sized + SelectionHandler { + /// [`DataControlState`] getter. + fn data_control_state(&mut self) -> &mut DataControlState; +} + +/// State of the data control. +#[derive(Debug)] +pub struct DataControlState { + manager_global: GlobalId, + /// Used sources. + /// + /// Protocol states that each source can only be used once. We + /// also use it during destruction to get seat data. + pub(crate) used_sources: HashMap, +} + +impl DataControlState { + /// Register new [ExtDataControlManagerV1] global. + /// + /// Passing `primary_selection` will enable support for primary selection as well. + pub fn new( + display: &DisplayHandle, + primary_selection: Option<&PrimarySelectionState>, + filter: F, + ) -> Self + where + D: GlobalDispatch + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = ExtDataControlManagerGlobalData { + primary: primary_selection.is_some(), + filter: Box::new(filter), + }; + let manager_global = display.create_global::(1, data); + Self { + manager_global, + used_sources: Default::default(), + } + } + + /// [ExtDataControlManagerV1] GlobalId getter. + pub fn global(&self) -> GlobalId { + self.manager_global.clone() + } +} + +#[allow(missing_debug_implementations)] +#[doc(hidden)] +pub struct ExtDataControlManagerGlobalData { + /// Whether to allow primary selection. + primary: bool, + + /// Filter whether the clients can view global. + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +#[doc(hidden)] +#[derive(Debug, Clone, Copy)] +pub struct ExtDataControlManagerUserData { + /// Whether to allow primary selection. + primary: bool, +} + +mod handlers { + use std::cell::RefCell; + + use tracing::error; + use wayland_protocols::ext::data_control::v1::server::{ + ext_data_control_device_v1::ExtDataControlDeviceV1, + ext_data_control_manager_v1::{self, ExtDataControlManagerV1}, + ext_data_control_source_v1::ExtDataControlSourceV1, + }; + use wayland_server::{Client, Dispatch, DisplayHandle}; + + use crate::input::Seat; + use crate::wayland::selection::SelectionTarget; + use crate::wayland::selection::device::SelectionDevice; + use crate::wayland::selection::seat_data::SeatData; + use crate::wayland::{Dispatch2, GlobalDispatch2}; + + use super::DataControlHandler; + use super::ExtDataControlDeviceUserData; + use super::ExtDataControlManagerGlobalData; + use super::ExtDataControlManagerUserData; + use super::ExtDataControlSourceUserData; + + impl GlobalDispatch2 for ExtDataControlManagerGlobalData + where + D: Dispatch, + D: DataControlHandler, + D: 'static, + { + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init( + resource, + ExtDataControlManagerUserData { + primary: self.primary, + }, + ); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } + } + + impl Dispatch2 for ExtDataControlManagerUserData + where + D: Dispatch, + D: Dispatch, + D: DataControlHandler, + D: 'static, + { + fn request( + &self, + _handler: &mut D, + client: &wayland_server::Client, + _resource: &ExtDataControlManagerV1, + request: ::Request, + dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + ext_data_control_manager_v1::Request::CreateDataSource { id } => { + data_init.init(id, ExtDataControlSourceUserData::new(dh.clone())); + } + ext_data_control_manager_v1::Request::GetDataDevice { id, seat: wl_seat } => { + match Seat::::from_resource(&wl_seat) { + Some(seat) => { + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let device = SelectionDevice::ExtDataControl(data_init.init( + id, + ExtDataControlDeviceUserData { + wl_seat, + primary: self.primary, + }, + )); + + let mut seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut(); + + seat_data.add_device(device.clone()); + + // NOTE: broadcast selection only to the newly created device. + let device = Some(&device); + seat_data.send_selection::(dh, SelectionTarget::Clipboard, device, true); + if self.primary { + seat_data.send_selection::(dh, SelectionTarget::Primary, device, true); + } + } + None => { + error!( + data_control_device = ?id, + client = ?client, + "Unmanaged seat given to a primary selection device." + ); + } + } + } + ext_data_control_manager_v1::Request::Destroy => (), + _ => unreachable!(), + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/ext_data_control/source.rs b/vendor/smithay/src/wayland/selection/ext_data_control/source.rs new file mode 100644 index 0000000..4c05b11 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/ext_data_control/source.rs @@ -0,0 +1,109 @@ +use std::cell::RefCell; +use std::sync::Mutex; + +use wayland_server::DisplayHandle; +use wayland_server::backend::ClientId; + +use crate::input::Seat; +use crate::wayland::Dispatch2; +use crate::wayland::selection::SelectionTarget; +use crate::wayland::selection::offer::OfferReplySource; +use crate::wayland::selection::seat_data::SeatData; +use crate::wayland::selection::source::SelectionSourceProvider; + +use wayland_protocols::ext::data_control::v1::server::ext_data_control_source_v1::{ + self, ExtDataControlSourceV1, +}; + +use super::DataControlHandler; + +#[doc(hidden)] +#[derive(Debug)] +pub struct ExtDataControlSourceUserData { + pub(crate) inner: Mutex, + display_handle: DisplayHandle, +} + +impl ExtDataControlSourceUserData { + pub(crate) fn new(display_handle: DisplayHandle) -> Self { + Self { + inner: Default::default(), + display_handle, + } + } +} + +/// The metadata describing a data source +#[derive(Debug, Default, Clone)] +pub struct SourceMetadata { + /// The MIME types supported by this source + pub mime_types: Vec, +} + +impl Dispatch2 for ExtDataControlSourceUserData +where + D: DataControlHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &ExtDataControlSourceV1, + request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + ext_data_control_source_v1::Request::Offer { mime_type } => { + let mut data = self.inner.lock().unwrap(); + data.mime_types.push(mime_type); + } + ext_data_control_source_v1::Request::Destroy => (), + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, source: &ExtDataControlSourceV1) { + // Remove the source from the used ones. + let seat = match state + .data_control_state() + .used_sources + .remove(source) + .as_ref() + .and_then(Seat::::from_resource) + { + Some(seat) => seat, + None => return, + }; + + let mut seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut(); + + for target in [SelectionTarget::Primary, SelectionTarget::Clipboard] { + let selection = match target { + SelectionTarget::Primary => seat_data.get_primary_selection(), + SelectionTarget::Clipboard => seat_data.get_clipboard_selection(), + }; + + match selection { + Some(OfferReplySource::Client(SelectionSourceProvider::ExtDataControl(set_source))) + if set_source == source => + { + match target { + SelectionTarget::Primary => { + seat_data.set_primary_selection::(&self.display_handle, None) + } + SelectionTarget::Clipboard => { + seat_data.set_clipboard_selection::(&self.display_handle, None) + } + } + } + _ => (), + }; + } + } +} diff --git a/vendor/smithay/src/wayland/selection/mod.rs b/vendor/smithay/src/wayland/selection/mod.rs new file mode 100644 index 0000000..7391a89 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/mod.rs @@ -0,0 +1,110 @@ +//! Handler utilities for the various selection protocols. +//! +//! Wayland originally only had a singe way to deal with the user clipboard with the help of +//! `wl_data_device` protocol. However the demand for *primary* selection as well as writing +//! clipboard managers made it into developing protocol extension for the said cases. +//! +//! The following selection providers are supported: +//! +//! - The [`data_device`](data_device/index.html) module to work with the traditional clipboard selection. +//! - The [`primary_selection`](primary_selection/index.html) module to work with the primary selection. +//! - The [`wlr_data_control`](wlr_data_control/index.html) module to hook data control into +//! clipboard and primary selection + +use std::os::unix::io::OwnedFd; + +use crate::input::{Seat, SeatHandler}; + +pub mod data_device; +pub mod ext_data_control; +pub mod primary_selection; +pub mod wlr_data_control; + +mod device; +mod offer; +mod seat_data; +mod source; + +pub use source::SelectionSource; + +/// Events that are generated by interactions of the clients with the data device. +pub trait SelectionHandler: Sized + SeatHandler { + /// UserData attached to server-side selections. + type SelectionUserData: Clone + Send + Sync + 'static; + + /// A client has set the selection + #[allow(unused_variables)] + fn new_selection(&mut self, ty: SelectionTarget, source: Option, seat: Seat) {} + + /// A client requested to read the server-set selection. + /// + /// * `mime_type` - the requested mime type + /// * `fd` - the fd to write into + #[allow(unused_variables)] + fn send_selection( + &mut self, + ty: SelectionTarget, + mime_type: String, + fd: OwnedFd, + seat: Seat, + user_data: &Self::SelectionUserData, + ) { + } +} + +/// The target for the selection request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SelectionTarget { + /// The target for the request is primary selection. + Primary, + /// The target for the request is clipboard selection. + Clipboard, +} + +pub(crate) mod private { + /// `selection_dispatch!(self; Enum(foo) => foo.something())` + /// expands to the equivalent of + /// ```ignore + /// match self { + /// Enum::DataDevice(foo) => foo.something(), + /// Enum::Primary(foo) => foo.something(), + /// Enum::WlrDataControl(foo) => foo.something(), + /// Enum::ExtDataControl(foo) => foo.something(), + /// } + /// ``` + /// + /// And `selection_dispatch!(self, other; Enum(foo), EnumNext(zoo) => foo.something(zoo))` expands + /// to the equivalent of + /// ```ignore + /// match (self, other) { + /// (Enum::DataDevice(foo), EnumNext::DataDevice(zoo)) => foo.something(zoo), + /// (Enum::Primary(foo), EnumNext::Primary(zoo)) => foo.something(zoo), + /// (Enum::WlrDataControl(foo), EnumNext::WlrDataControl(zoo)) => foo.something(zoo), + /// (Enum::ExtDataControl(foo), EnumNext::ExtDataControl(zoo)) => foo.something(zoo), + /// _ => unreachable!(), + /// } + /// ``` + /// + /// The result can be converted to another enum by adding `; as AnotherEnum` + macro_rules! selection_dispatch { + ($what:ident; $enum:ident ( $($c1:tt)* ) => $x:expr) => { + match $what { + $enum::DataDevice($($c1)*) => $x, + $enum::Primary($($c1)*) => $x, + $enum::WlrDataControl($($c1)*) => $x, + $enum::ExtDataControl($($c1)*) => $x, + } + }; + ($what:ident$(, $what_next:ident)+; $enum:ident ( $($c1:tt)*) $(, $enum_next:ident ( $($c2:tt)* ) )+ => $x:expr) => { + match ($what$(, $what_next)*) { + ($enum::DataDevice($($c1)*)$(, $enum_next::DataDevice($($c2)*))*) => $x, + ($enum::Primary($($c1)*)$(, $enum_next::Primary($($c2)*))*) => $x, + ($enum::WlrDataControl($($c1)*)$(, $enum_next::WlrDataControl($($c2)*))*) => $x, + ($enum::ExtDataControl($($c1)*)$(, $enum_next::ExtDataControl($($c2)*))*) => $x, + _ => unreachable!(), + } + }; + } + + pub(crate) use selection_dispatch; +} diff --git a/vendor/smithay/src/wayland/selection/offer.rs b/vendor/smithay/src/wayland/selection/offer.rs new file mode 100644 index 0000000..86f16ae --- /dev/null +++ b/vendor/smithay/src/wayland/selection/offer.rs @@ -0,0 +1,196 @@ +use std::os::unix::io::OwnedFd; +use std::sync::Arc; + +use tracing::debug; +use wayland_protocols::ext::data_control::v1::server::ext_data_control_offer_v1::{ + self, ExtDataControlOfferV1, +}; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_offer_v1::{ + self, ZwpPrimarySelectionOfferV1 as PrimaryOffer, +}; +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_offer_v1::{ + self, ZwlrDataControlOfferV1, +}; +use wayland_server::DisplayHandle; +use wayland_server::backend::ObjectId; +use wayland_server::backend::protocol::Message; +use wayland_server::backend::{ClientId, Handle, ObjectData}; +use wayland_server::protocol::wl_data_offer; +use wayland_server::protocol::wl_seat::WlSeat; +use wayland_server::{Resource, protocol::wl_data_offer::WlDataOffer}; +use wl_data_offer::Request as DataOfferRequest; +use zwlr_data_control_offer_v1::Request as DataControlRequest; +use zwp_primary_selection_offer_v1::Request as PrimaryRequest; + +use crate::input::Seat; + +use super::SelectionHandler; +use super::device::{DataDeviceKind, SelectionDevice}; +use super::private::selection_dispatch; +use super::source::{CompositorSelectionProvider, SelectionSourceProvider}; + +#[derive(Debug, Clone)] +pub enum OfferReplySource { + /// The selection is backend by client source. + Client(SelectionSourceProvider), + /// The selection is backed by the compositor. + Compositor(CompositorSelectionProvider), +} + +impl OfferReplySource { + /// Get the copy of the underlying mime types. + pub fn mime_types(&self) -> Vec { + match self { + OfferReplySource::Client(source) => source.mime_types(), + OfferReplySource::Compositor(source) => source.mime_types.clone(), + } + } + + /// Check whether the source contains the given `mime_type`. + pub fn contains_mime_type(&self, mime_type: &String) -> bool { + match self { + OfferReplySource::Client(source) => source.contains_mime_type(mime_type), + OfferReplySource::Compositor(source) => source.mime_types.contains(mime_type), + } + } +} + +/// Offer representing various selection offers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SelectionOffer { + DataDevice(WlDataOffer), + Primary(PrimaryOffer), + WlrDataControl(ZwlrDataControlOfferV1), + ExtDataControl(ExtDataControlOfferV1), +} + +impl SelectionOffer { + pub fn new( + dh: &DisplayHandle, + device: &SelectionDevice, + client_id: ClientId, + data: OfferReplySource, + ) -> Self + where + D: SelectionHandler + 'static, + { + // NOTE: the types are tied to the `SelectionDevice`, so every + // RTTI like checking is safe and reliable. + + let device_kind = device.device_kind(); + let data = Arc::new(OfferReplyData { + device_kind, + seat: device.seat(), + source: data, + }); + let backend = dh.backend_handle(); + + let interface = match device_kind { + DataDeviceKind::Core => WlDataOffer::interface(), + DataDeviceKind::Primary => PrimaryOffer::interface(), + DataDeviceKind::WlrDataControl => ZwlrDataControlOfferV1::interface(), + DataDeviceKind::ExtDataControl => ExtDataControlOfferV1::interface(), + }; + + let offer = backend + .create_object::(client_id, interface, device.version(), data) + .unwrap(); + + match device_kind { + DataDeviceKind::Core => Self::DataDevice(WlDataOffer::from_id(dh, offer).unwrap()), + DataDeviceKind::Primary => Self::Primary(PrimaryOffer::from_id(dh, offer).unwrap()), + DataDeviceKind::WlrDataControl => { + Self::WlrDataControl(ZwlrDataControlOfferV1::from_id(dh, offer).unwrap()) + } + DataDeviceKind::ExtDataControl => { + Self::ExtDataControl(ExtDataControlOfferV1::from_id(dh, offer).unwrap()) + } + } + } + + pub fn offer(&self, mime_type: String) { + selection_dispatch!(self; Self(offer) => offer.offer(mime_type)) + } +} + +struct OfferReplyData { + device_kind: DataDeviceKind, + source: OfferReplySource, + seat: WlSeat, +} + +impl ObjectData for OfferReplyData +where + D: SelectionHandler + 'static, +{ + fn request( + self: Arc, + dh: &Handle, + handle: &mut D, + _: ClientId, + msg: Message, + ) -> Option>> { + let dh = DisplayHandle::from(dh.clone()); + + // NOTE: we can't parse message more than once, since it expects the `OwnedFd` which + // we can't clone. To achieve that, we use RTTI passed along the selection data, to + // make the parsing work only once. + let (mime_type, fd, object_name) = match self.device_kind { + DataDeviceKind::Core => { + if let Ok((_resource, DataOfferRequest::Receive { mime_type, fd })) = + WlDataOffer::parse_request(&dh, msg) + { + (mime_type, fd, "wl_data_offer") + } else { + return None; + } + } + DataDeviceKind::Primary => { + if let Ok((_resource, PrimaryRequest::Receive { mime_type, fd })) = + PrimaryOffer::parse_request(&dh, msg) + { + (mime_type, fd, "primary_selection_offer") + } else { + return None; + } + } + DataDeviceKind::WlrDataControl => { + if let Ok((_resource, DataControlRequest::Receive { mime_type, fd })) = + ZwlrDataControlOfferV1::parse_request(&dh, msg) + { + (mime_type, fd, "wlr_data_control_offer") + } else { + return None; + } + } + DataDeviceKind::ExtDataControl => { + if let Ok((_resource, ext_data_control_offer_v1::Request::Receive { mime_type, fd })) = + ExtDataControlOfferV1::parse_request(&dh, msg) + { + (mime_type, fd, "ext_data_control_offer") + } else { + return None; + } + } + }; + + if !self.source.contains_mime_type(&mime_type) { + debug!("Denying a {object_name}.receive with invalid mime type"); + } else { + match &self.source { + OfferReplySource::Client(source) => { + source.send(mime_type, fd); + } + OfferReplySource::Compositor(source) => { + if let Some(seat) = Seat::::from_resource(&self.seat) { + handle.send_selection(source.ty, mime_type, fd, seat, &source.user_data); + } + } + } + } + + None + } + + fn destroyed(self: Arc, _: &Handle, _: &mut D, _: ClientId, _: ObjectId) {} +} diff --git a/vendor/smithay/src/wayland/selection/primary_selection/device.rs b/vendor/smithay/src/wayland/selection/primary_selection/device.rs new file mode 100644 index 0000000..5d10d16 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/primary_selection/device.rs @@ -0,0 +1,121 @@ +use std::cell::RefCell; + +use tracing::debug; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::{ + self as primary_device, ZwpPrimarySelectionDeviceV1 as PrimaryDevice, +}; +use wayland_server::{Client, DataInit, DisplayHandle, Resource, protocol::wl_seat::WlSeat}; + +use crate::{ + input::{Seat, SeatHandler}, + wayland::{ + Dispatch2, + seat::WaylandFocus, + selection::{ + SelectionHandler, SelectionTarget, + device::SelectionDevice, + offer::OfferReplySource, + seat_data::SeatData, + source::{SelectionSource, SelectionSourceProvider}, + }, + }, +}; + +use super::PrimarySelectionHandler; + +#[doc(hidden)] +#[derive(Debug)] +pub struct PrimaryDeviceUserData { + pub(crate) wl_seat: WlSeat, +} + +impl Dispatch2 for PrimaryDeviceUserData +where + D: PrimarySelectionHandler, + D: SelectionHandler, + D: SeatHandler, + ::KeyboardFocus: WaylandFocus, + D: 'static, +{ + fn request( + &self, + handler: &mut D, + client: &Client, + resource: &PrimaryDevice, + request: primary_device::Request, + dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let seat = match Seat::::from_resource(&self.wl_seat) { + Some(seat) => seat, + None => return, + }; + + match request { + primary_device::Request::SetSelection { source, .. } => { + let seat_data = match seat.get_keyboard() { + Some(keyboard) if keyboard.client_of_object_has_focus(&resource.id()) => seat + .user_data() + .get::>>() + .unwrap(), + _ => { + debug!( + client = ?client, + "denying setting selection by a non-focused client" + ); + return; + } + }; + + // NOTE: While protocol states that selection shouldn't be used more than once, + // no-one enforces it, thus we have clients around that do so and crashing them + // doesn't worth it at this point. + if let Some(source) = source.as_ref() { + handler + .primary_selection_state() + .used_sources + .insert(source.clone(), self.wl_seat.clone()); + } + + let source = source.map(SelectionSourceProvider::Primary); + + handler.new_selection( + SelectionTarget::Primary, + source.clone().map(|provider| SelectionSource { provider }), + seat.clone(), + ); + + // The client has kbd focus, it can set the selection + seat_data + .borrow_mut() + .set_primary_selection::(dh, source.map(OfferReplySource::Client)); + } + primary_device::Request::Destroy => seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut() + .retain_devices(|ndd| match ndd { + SelectionDevice::Primary(ndd) => ndd != resource, + _ => true, + }), + _ => unreachable!(), + } + } + + fn destroyed( + &self, + _state: &mut D, + _client: wayland_server::backend::ClientId, + resource: &PrimaryDevice, + ) { + if let Some(seat) = Seat::::from_resource(&self.wl_seat) { + if let Some(seat_data) = seat.user_data().get::>>() { + seat_data.borrow_mut().retain_devices(|ndd| match ndd { + SelectionDevice::Primary(ndd) => ndd != resource, + _ => true, + }); + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/primary_selection/mod.rs b/vendor/smithay/src/wayland/selection/primary_selection/mod.rs new file mode 100644 index 0000000..73a3b03 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/primary_selection/mod.rs @@ -0,0 +1,408 @@ +//! Utilities for manipulating the primary selection +//! +//! The primary selection is an additional protocol modeled after the data device to represent +//! and additional selection (copy/paste), a concept taken from the X11 Server. +//! This primary selection is a shortcut to the common clipboard selection, +//! where text just needs to be selected in order to allow copying it elsewhere +//! The de facto way to perform this action is the middle mouse button, although it is not limited to this one. +//! +//! This module provides the freestanding [`set_primary_focus`] function: +//! This function sets the data device focus for a given seat; you'd typically call it +//! whenever the keyboard focus changes, to follow it (for example in the focus hook of your keyboards). +//! +//! The module also provides an additional mechanism allowing your compositor to see and interact with +//! the contents of the primary selection: +//! +//! - the freestanding function [`set_primary_selection`] +//! allows you to set the contents of the selection for your clients +//! - the `PrimarySelectionHandle` gives you the option to inspect new selections +//! by overriding [`SelectionHandler::new_selection`]. +//! +//! ## Initialization +//! +//! To initialize this implementation, create the [`PrimarySelectionState`], store it inside your `State` struct +//! and implement the [`PrimarySelectionHandler`] and [`SelectionHandler`], as shown in this example: +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::selection::SelectionHandler; +//! use smithay::wayland::selection::primary_selection::{PrimarySelectionState, PrimarySelectionHandler}; +//! # use smithay::input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}; +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! # struct State { primary_selection_state: PrimarySelectionState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the primary_selection state +//! let primary_selection_state = PrimarySelectionState::new::( +//! &display.handle(), +//! ); +//! +//! // insert the PrimarySelectionState into your state +//! // .. +//! +//! // implement the necessary traits +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! # impl SeatHandler for State { +//! # type KeyboardFocus = WlSurface; +//! # type PointerFocus = WlSurface; +//! # type TouchFocus = WlSurface; +//! # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! # } +//! # impl PointerConstraintsHandler for State {} +//! impl SelectionHandler for State { +//! type SelectionUserData = (); +//! } +//! impl PrimarySelectionHandler for State { +//! fn primary_selection_state(&mut self) -> &mut PrimarySelectionState { &mut self.primary_selection_state } +//! // ... override default implementations here to customize handling ... +//! } +//! +//! delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` + +use std::cell::{Ref, RefCell}; +use std::collections::HashMap; +use std::os::unix::io::OwnedFd; +use std::sync::Arc; + +use tracing::instrument; +use wayland_protocols::wp::primary_selection::zv1::{ + server::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1 as PrimaryDeviceManager, + server::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, +}; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_seat::WlSeat; +use wayland_server::{Client, DisplayHandle, GlobalDispatch}; + +use crate::{ + input::{Seat, SeatHandler}, + wayland::selection::SelectionTarget, +}; + +mod device; +mod source; + +pub use device::PrimaryDeviceUserData; +pub use source::{PrimarySourceUserData, SourceMetadata}; + +use super::SelectionHandler; +use super::source::CompositorSelectionProvider; +use super::{offer::OfferReplySource, seat_data::SeatData}; + +/// Access the primary selection state. +pub trait PrimarySelectionHandler: Sized + SeatHandler + SelectionHandler { + /// [PrimarySelectionState] getter. + fn primary_selection_state(&mut self) -> &mut PrimarySelectionState; +} + +/// State of the primary selection. +pub struct PrimarySelectionState { + manager_global: GlobalId, + pub(super) filter: Arc Fn(&'c Client) -> bool + Send + Sync>>, + /// Used sources. + /// + /// Protocol states that each source can only be used once. We + /// also use it during destruction to get seat data. + pub(crate) used_sources: HashMap, +} + +impl std::fmt::Debug for PrimarySelectionState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PrimarySelectionState") + .field("manager_global", &self.manager_global) + .finish() + } +} + +impl PrimarySelectionState { + /// Register new [`PrimaryDeviceManager`] global + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch + 'static, + D: PrimarySelectionHandler, + { + Self::new_with_filter::(display, |_| true) + } + + /// Register new [`PrimaryDeviceManager`] global with a filter. + /// + /// Filters can be used to limit visibility of a global to certain clients. + pub fn new_with_filter(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch + 'static, + D: PrimarySelectionHandler, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = PrimaryDeviceManagerGlobalData { + filter: Arc::new(Box::new(filter)), + }; + let filter = Arc::clone(&data.filter); + let manager_global = display.create_global::(1, data); + + Self { + manager_global, + filter, + used_sources: Default::default(), + } + } + + /// [`PrimaryDeviceManager`] GlobalId getter + pub fn global(&self) -> GlobalId { + self.manager_global.clone() + } +} + +#[allow(missing_debug_implementations)] +#[doc(hidden)] +pub struct PrimaryDeviceManagerGlobalData { + filter: Arc Fn(&'c Client) -> bool + Send + Sync>>, +} + +/// Set the primary selection focus to a certain client for a given seat +/// +/// If the focus is different from the existing focus, the current selection will be offered to the client. +#[instrument(name = "wayland_primary_selection", level = "debug", skip(dh, seat, client), fields(seat = seat.name(), client = ?client.as_ref().map(|c| c.id())))] +pub fn set_primary_focus(dh: &DisplayHandle, seat: &Seat, client: Option) +where + D: SeatHandler + PrimarySelectionHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + seat_data.borrow_mut().set_primary_focus::(dh, client); +} + +/// Set a compositor-provided primary selection for this seat +/// +/// You need to provide the available mime types for this selection. +/// +/// Whenever a client requests to read the selection, your callback will +/// receive a [`SelectionHandler::send_selection`] event. +#[instrument(name = "wayland_primary_selection", level = "debug", skip(dh, seat, user_data), fields(seat = seat.name()))] +pub fn set_primary_selection( + dh: &DisplayHandle, + seat: &Seat, + mime_types: Vec, + user_data: D::SelectionUserData, +) where + D: SeatHandler + PrimarySelectionHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + + let selection = OfferReplySource::Compositor(CompositorSelectionProvider { + ty: SelectionTarget::Primary, + mime_types, + user_data, + }); + + seat_data + .borrow_mut() + .set_primary_selection::(dh, Some(selection)); +} + +/// Errors happening when requesting selection contents +#[derive(Debug, thiserror::Error)] +pub enum SelectionRequestError { + /// Requested mime type is not available + #[error("Requested mime type is not available")] + InvalidMimetype, + /// Requesting server side selection contents is not supported + #[error("Current selection is server-side")] + ServerSideSelection, + /// There is no active selection + #[error("No active selection to query")] + NoSelection, +} + +/// Request the current primary selection of the given seat +/// to be written to the provided file descriptor with the given mime type. +pub fn request_primary_client_selection( + seat: &Seat, + mime_type: String, + fd: OwnedFd, +) -> Result<(), SelectionRequestError> +where + D: SeatHandler + PrimarySelectionHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + match seat_data.borrow().get_primary_selection() { + None => Err(SelectionRequestError::NoSelection), + Some(OfferReplySource::Client(source)) => { + if !source.contains_mime_type(&mime_type) { + Err(SelectionRequestError::InvalidMimetype) + } else { + source.send(mime_type, fd); + Ok(()) + } + } + Some(OfferReplySource::Compositor(selection)) => { + if !selection.mime_types.contains(&mime_type) { + Err(SelectionRequestError::InvalidMimetype) + } else { + Err(SelectionRequestError::ServerSideSelection) + } + } + } +} + +/// Gets the user_data for the currently active selection, if set by the compositor +#[instrument(name = "wayland_primary_selection", level = "debug", skip_all, fields(seat = seat.name()))] +pub fn current_primary_selection_userdata(seat: &Seat) -> Option> +where + D: SeatHandler + PrimarySelectionHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + Ref::filter_map(seat_data.borrow(), |data| match data.get_primary_selection() { + Some(OfferReplySource::Compositor(CompositorSelectionProvider { user_data, .. })) => Some(user_data), + _ => None, + }) + .ok() +} + +/// Clear the current selection for this seat +#[instrument(name = "wayland_primary_selection", level = "debug", skip_all, fields(seat = seat.name()))] +pub fn clear_primary_selection(dh: &DisplayHandle, seat: &Seat) +where + D: SeatHandler + PrimarySelectionHandler + 'static, +{ + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + seat_data.borrow_mut().set_primary_selection::(dh, None); +} + +mod handlers { + use std::cell::RefCell; + + use tracing::error; + use wayland_protocols::wp::primary_selection::zv1::server::{ + zwp_primary_selection_device_manager_v1::{ + self as primary_device_manager, ZwpPrimarySelectionDeviceManagerV1 as PrimaryDeviceManager, + }, + zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1 as PrimaryDevice, + zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1 as PrimarySource, + }; + use wayland_server::{Dispatch, DisplayHandle}; + + use crate::{ + input::{Seat, SeatHandler}, + wayland::{ + Dispatch2, GlobalData, GlobalDispatch2, + selection::{device::SelectionDevice, seat_data::SeatData}, + }, + }; + + use super::PrimarySelectionHandler; + use super::{ + PrimaryDeviceManagerGlobalData, device::PrimaryDeviceUserData, source::PrimarySourceUserData, + }; + + impl GlobalDispatch2 for PrimaryDeviceManagerGlobalData + where + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: PrimarySelectionHandler, + D: 'static, + { + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &wayland_server::Client) -> bool { + (self.filter)(client) + } + } + + impl Dispatch2 for GlobalData + where + D: Dispatch, + D: Dispatch, + D: PrimarySelectionHandler, + D: SeatHandler, + D: 'static, + { + fn request( + &self, + _state: &mut D, + client: &wayland_server::Client, + _resource: &PrimaryDeviceManager, + request: primary_device_manager::Request, + dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + primary_device_manager::Request::CreateSource { id } => { + data_init.init(id, PrimarySourceUserData::new(dhandle.clone())); + } + primary_device_manager::Request::GetDevice { id, seat: wl_seat } => { + match Seat::::from_resource(&wl_seat) { + Some(seat) => { + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let device = SelectionDevice::Primary( + data_init.init(id, PrimaryDeviceUserData { wl_seat }), + ); + + let seat_data = seat + .user_data() + .get::>>() + .unwrap(); + + seat_data.borrow_mut().add_device(device); + } + None => { + error!( + primary_selection_device = ?id, + client = ?client, + "Unmanaged seat given to a primary selection device." + ); + } + } + } + primary_device_manager::Request::Destroy => {} + _ => unreachable!(), + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/primary_selection/source.rs b/vendor/smithay/src/wayland/selection/primary_selection/source.rs new file mode 100644 index 0000000..cb0fbcb --- /dev/null +++ b/vendor/smithay/src/wayland/selection/primary_selection/source.rs @@ -0,0 +1,95 @@ +use std::{cell::RefCell, sync::Mutex}; + +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::{ + self as primary_source, ZwpPrimarySelectionSourceV1 as PrimarySource, +}; +use wayland_server::{DisplayHandle, backend::ClientId}; + +use crate::{ + input::Seat, + wayland::{ + Dispatch2, + selection::{offer::OfferReplySource, seat_data::SeatData, source::SelectionSourceProvider}, + }, +}; + +use super::PrimarySelectionHandler; + +/// The metadata describing a data source +#[derive(Debug, Default, Clone)] +pub struct SourceMetadata { + /// The MIME types supported by this source + pub mime_types: Vec, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct PrimarySourceUserData { + pub(crate) inner: Mutex, + display_handle: DisplayHandle, +} + +impl PrimarySourceUserData { + pub(super) fn new(display_handle: DisplayHandle) -> Self { + Self { + inner: Default::default(), + display_handle, + } + } +} + +impl Dispatch2 for PrimarySourceUserData +where + D: PrimarySelectionHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + _resource: &PrimarySource, + request: primary_source::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let _primary_selection_state = state.primary_selection_state(); + let mut data = self.inner.lock().unwrap(); + + match request { + primary_source::Request::Offer { mime_type } => { + data.mime_types.push(mime_type); + } + primary_source::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, source: &PrimarySource) { + // Remove the source from the used ones. + let seat = match state + .primary_selection_state() + .used_sources + .remove(source) + .as_ref() + .and_then(Seat::::from_resource) + { + Some(seat) => seat, + None => return, + }; + + let mut seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut(); + + match seat_data.get_primary_selection() { + Some(OfferReplySource::Client(SelectionSourceProvider::Primary(set_source))) + if set_source == source => + { + seat_data.set_primary_selection::(&self.display_handle, None) + } + _ => (), + } + } +} diff --git a/vendor/smithay/src/wayland/selection/seat_data.rs b/vendor/smithay/src/wayland/selection/seat_data.rs new file mode 100644 index 0000000..effe84a --- /dev/null +++ b/vendor/smithay/src/wayland/selection/seat_data.rs @@ -0,0 +1,228 @@ +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::EVT_PRIMARY_SELECTION_SINCE; +use wayland_server::protocol::wl_data_device::WlDataDevice; +use wayland_server::{Client, DisplayHandle, Resource}; + +use super::device::SelectionDevice; +use super::offer::{OfferReplySource, SelectionOffer}; +use super::{SelectionHandler, SelectionTarget}; + +/// Seat data used to handle regular selection operations. +/// +/// The data is shared across primary, data device, and data control selections. +pub(crate) struct SeatData { + known_devices: Vec, + clipboard_selection: Option>, + clipboard_selection_focus: Option, + primary_selection: Option>, + primary_selection_focus: Option, +} + +impl SeatData { + /// Create a new [`SeatData`] with empty selections and without focusing any client. + pub fn new() -> Self { + Default::default() + } + + pub fn retain_devices bool>(&mut self, retain: F) { + self.known_devices.retain(retain); + } + + /// Register a new [SelectionDevice] into [SeatData]. + pub fn add_device(&mut self, device: SelectionDevice) { + self.known_devices.push(device); + } + + /// Get the iterator over [`WlDataDevice`]. Mostly used for the drag and drop handling. + pub fn known_data_devices(&self) -> impl Iterator { + self.known_devices.iter().filter_map(|device| match device { + SelectionDevice::DataDevice(device) => Some(device), + _ => None, + }) + } + + /// Change focus for the clipboard selection to `new_focus` client. Providing `None` will + /// remove the focus. + /// + /// If the focus is different from the existing focus, the current selection will be offered to the client. + pub fn set_clipboard_focus(&mut self, dh: &DisplayHandle, new_focus: Option) + where + D: SelectionHandler + 'static, + { + if self.clipboard_selection_focus != new_focus { + self.clipboard_selection_focus = new_focus; + self.send_selection::(dh, SelectionTarget::Clipboard, None, false) + } + } + + /// Set the clipboard selection to the `new_selection`, providing `None` will clear the + /// selection. + pub fn set_clipboard_selection( + &mut self, + dh: &DisplayHandle, + new_selection: Option>, + ) where + D: SelectionHandler + 'static, + { + if let Some(OfferReplySource::Client(source)) = self.clipboard_selection.as_ref() { + match new_selection.as_ref() { + Some(OfferReplySource::Client(new_source)) if new_source == source => (), + _ => source.cancel(), + } + } + self.clipboard_selection = new_selection; + self.send_selection::(dh, SelectionTarget::Clipboard, None, true) + } + + /// Change focus for the primary selection to `new_focus` client. Providing `None` will + /// remove the focus. + /// + /// If the focus is different from the existing focus, the current selection will be offered to the client. + pub fn set_primary_focus(&mut self, dh: &DisplayHandle, new_focus: Option) + where + D: SelectionHandler + 'static, + { + if self.primary_selection_focus != new_focus { + self.primary_selection_focus = new_focus; + self.send_selection::(dh, SelectionTarget::Primary, None, false) + } + } + + /// Set the primary selection to the `new_selection`, providing `None` will clear the + /// selection. + pub fn set_primary_selection(&mut self, dh: &DisplayHandle, new_selection: Option>) + where + D: SelectionHandler + 'static, + { + if let Some(OfferReplySource::Client(source)) = self.primary_selection.as_ref() { + match new_selection.as_ref() { + Some(OfferReplySource::Client(new_source)) if new_source == source => (), + _ => source.cancel(), + } + } + self.primary_selection = new_selection; + self.send_selection::(dh, SelectionTarget::Primary, None, true) + } + + /// Get the current selection which will be used as a clipboard selection. + pub fn get_clipboard_selection(&self) -> Option<&OfferReplySource> { + self.clipboard_selection.as_ref() + } + + /// Get the current selection which will be used as a primary selection. + pub fn get_primary_selection(&self) -> Option<&OfferReplySource> { + self.primary_selection.as_ref() + } + + /// Send selection for the given [`SelectionTarget`]. The data control devices should only + /// be update when the new selection is actually being set. + /// + /// The `restrict_to` option ensures that the selection is only send to the specified device, + /// however, when the previous selection source provider dies. It'll re-broadcast the state. + /// + /// `update_data_control` checks whether the data control devices should be updated. Usually + /// they shouldn't be updated when you just change the focus between clients, however, when + /// selection source dies the state is re-broadcasted. + pub fn send_selection( + &mut self, + dh: &DisplayHandle, + ty: SelectionTarget, + restrict_to: Option<&SelectionDevice>, + update_data_control: bool, + ) where + D: SelectionHandler + 'static, + { + let (client, selection) = match ty { + SelectionTarget::Primary => (self.primary_selection_focus.as_ref(), &mut self.primary_selection), + SelectionTarget::Clipboard => ( + self.clipboard_selection_focus.as_ref(), + &mut self.clipboard_selection, + ), + }; + + for device in self + .known_devices + .iter() + .filter(|&device| restrict_to.is_none_or(|r| r == device)) + .filter(|&device| match device { + // NOTE: filter by actual type here to not get a missmpatches when using selections + // later on. + SelectionDevice::DataDevice(_) => ty == SelectionTarget::Clipboard, + SelectionDevice::Primary(_) => ty == SelectionTarget::Primary, + SelectionDevice::WlrDataControl(data_control) => { + // Primary selection is available for data control only since v2. + update_data_control + && (data_control.version() >= EVT_PRIMARY_SELECTION_SINCE + || ty != SelectionTarget::Primary) + } + SelectionDevice::ExtDataControl(_) => update_data_control, + }) + { + // Data control doesn't require focus and should always get selection updates, unless + // it was requested not to update them. + if !matches!( + device, + SelectionDevice::WlrDataControl(_) | SelectionDevice::ExtDataControl(_) + ) && dh + .get_client(device.id()) + .map(|c| Some(&c) != client) + .unwrap_or(true) + { + continue; + } + + match (&*selection, ty) { + (None, SelectionTarget::Primary) => { + device.unset_primary_selection(); + continue; + } + (None, SelectionTarget::Clipboard) => { + device.unset_selection(); + continue; + } + (Some(selection), _) => { + // DataControl devices is the client itself, however other devices use + // the currently focused one as a client. + let client_id = match device { + SelectionDevice::WlrDataControl(device) => { + dh.get_client(device.id()).ok().map(|c| c.id()) + } + SelectionDevice::ExtDataControl(device) => { + dh.get_client(device.id()).ok().map(|c| c.id()) + } + _ => client.map(|c| c.id()), + }; + + let client_id = match client_id { + Some(client_id) => client_id, + None => continue, + }; + + let offer = SelectionOffer::new::(dh, device, client_id, selection.clone()); + + device.offer(&offer); + + for mime_type in selection.mime_types() { + offer.offer(mime_type); + } + + match ty { + SelectionTarget::Primary => device.primary_selection(&offer), + SelectionTarget::Clipboard => device.selection(&offer), + } + } + }; + } + } +} + +impl Default for SeatData { + fn default() -> Self { + Self { + known_devices: Vec::new(), + clipboard_selection: None, + clipboard_selection_focus: None, + primary_selection: None, + primary_selection_focus: None, + } + } +} diff --git a/vendor/smithay/src/wayland/selection/source.rs b/vendor/smithay/src/wayland/selection/source.rs new file mode 100644 index 0000000..795d6c3 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/source.rs @@ -0,0 +1,106 @@ +use std::os::unix::io::{AsFd, OwnedFd}; + +use wayland_protocols::ext::data_control::v1::server::ext_data_control_source_v1::ExtDataControlSourceV1; +use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1 as PrimarySource; +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; +use wayland_server::{Resource, protocol::wl_data_source::WlDataSource}; + +use crate::wayland::selection::primary_selection::PrimarySourceUserData; + +use super::SelectionTarget; +use super::data_device::DataSourceUserData; +use super::ext_data_control::ExtDataControlSourceUserData; +use super::private::selection_dispatch; +use super::wlr_data_control::DataControlSourceUserData; + +/// The source of the selection data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SelectionSource { + /// The selection source provider. + pub(crate) provider: SelectionSourceProvider, +} + +impl SelectionSource { + /// Mime types associated with the source. + pub fn mime_types(&self) -> Vec { + self.provider.mime_types() + } +} + +/// Provider of the selection data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SelectionSourceProvider { + /// The source was from the regular data device. + DataDevice(WlDataSource), + /// The primary selection was used as a source. + Primary(PrimarySource), + /// The data control selection was used as source. + WlrDataControl(ZwlrDataControlSourceV1), + /// The data control selection was used as source. + ExtDataControl(ExtDataControlSourceV1), +} + +impl SelectionSourceProvider { + /// Mark selection source as no longer valid. + pub fn cancel(&self) { + selection_dispatch!(self; Self(source) => source.cancelled()) + } + + /// Send the data with the specified `mime_type` using the given `fd`. + pub fn send(&self, mime_type: String, fd: OwnedFd) { + selection_dispatch!(self; Self(source) => source.send(mime_type, fd.as_fd())) + } + + /// Check whether the given selection source contains provided `mime_type`. + pub fn contains_mime_type(&self, mime_type: &String) -> bool { + match self { + Self::DataDevice(source) => { + let data: &DataSourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.contains(mime_type) + } + Self::Primary(source) => { + let data: &PrimarySourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.contains(mime_type) + } + Self::WlrDataControl(source) => { + let data: &DataControlSourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.contains(mime_type) + } + Self::ExtDataControl(source) => { + let data: &ExtDataControlSourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.contains(mime_type) + } + } + } + + /// Get the mime types associated with the given source. + pub fn mime_types(&self) -> Vec { + match self { + Self::DataDevice(source) => { + let data: &DataSourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.clone() + } + Self::Primary(source) => { + let data: &PrimarySourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.clone() + } + Self::WlrDataControl(source) => { + let data: &DataControlSourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.clone() + } + Self::ExtDataControl(source) => { + let data: &ExtDataControlSourceUserData = source.data().unwrap(); + data.inner.lock().unwrap().mime_types.clone() + } + } + } +} + +/// The selection used by the compositor UI elements, like EGUI panels. +#[derive(Debug, Clone)] +pub struct CompositorSelectionProvider { + /// The target used for the end compositor selection. + pub ty: SelectionTarget, + pub mime_types: Vec, + pub user_data: U, +} diff --git a/vendor/smithay/src/wayland/selection/wlr_data_control/device.rs b/vendor/smithay/src/wayland/selection/wlr_data_control/device.rs new file mode 100644 index 0000000..6048dd1 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/wlr_data_control/device.rs @@ -0,0 +1,149 @@ +use std::cell::RefCell; +use std::sync::Arc; + +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::{ + self, ZwlrDataControlDeviceV1, +}; +use wayland_server::protocol::wl_seat::WlSeat; +use wayland_server::{Client, DisplayHandle, Resource}; + +use crate::input::Seat; +use crate::wayland::Dispatch2; +use crate::wayland::selection::device::SelectionDevice; +use crate::wayland::selection::offer::OfferReplySource; +use crate::wayland::selection::seat_data::SeatData; +use crate::wayland::selection::source::SelectionSourceProvider; +use crate::wayland::selection::{SelectionSource, SelectionTarget}; + +use super::DataControlHandler; + +#[allow(missing_debug_implementations)] +#[doc(hidden)] +pub struct DataControlDeviceUserData { + pub(crate) primary_selection_filter: Arc Fn(&'c Client) -> bool + Send + Sync>>, + pub(crate) wl_seat: WlSeat, +} + +impl Dispatch2 for DataControlDeviceUserData +where + D: DataControlHandler, + D: 'static, +{ + fn request( + &self, + handler: &mut D, + client: &Client, + resource: &ZwlrDataControlDeviceV1, + request: ::Request, + dh: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, D>, + ) { + let seat = match Seat::::from_resource(&self.wl_seat) { + Some(seat) => seat, + None => return, + }; + + match request { + zwlr_data_control_device_v1::Request::SetSelection { source, .. } => { + // Each source can only be used once. + if let Some(source) = source.as_ref() { + if handler + .data_control_state() + .used_sources + .insert(source.clone(), self.wl_seat.clone()) + .is_some() + { + resource.post_error( + zwlr_data_control_device_v1::Error::UsedSource, + "selection source can be used only once.", + ); + return; + } + } + + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let source = source.map(SelectionSourceProvider::WlrDataControl); + + handler.new_selection( + SelectionTarget::Clipboard, + source.clone().map(|provider| SelectionSource { provider }), + seat.clone(), + ); + + seat.user_data() + .get::>>() + .unwrap() + .borrow_mut() + .set_clipboard_selection::(dh, source.map(OfferReplySource::Client)); + } + zwlr_data_control_device_v1::Request::SetPrimarySelection { source, .. } => { + // When the primary selection is disabled, we should simply ignore the requests. + if !(*self.primary_selection_filter)(client) { + return; + } + + // Each source can only be used once. + if let Some(source) = source.as_ref() { + if handler + .data_control_state() + .used_sources + .insert(source.clone(), self.wl_seat.clone()) + .is_some() + { + resource.post_error( + zwlr_data_control_device_v1::Error::UsedSource, + "selection source can be used only once.", + ); + return; + } + } + + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let source = source.map(SelectionSourceProvider::WlrDataControl); + + handler.new_selection( + SelectionTarget::Primary, + source.clone().map(|provider| SelectionSource { provider }), + seat.clone(), + ); + + seat.user_data() + .get::>>() + .unwrap() + .borrow_mut() + .set_primary_selection::(dh, source.map(OfferReplySource::Client)); + } + zwlr_data_control_device_v1::Request::Destroy => seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut() + .retain_devices(|ndd| match ndd { + SelectionDevice::WlrDataControl(ndd) => ndd != resource, + _ => true, + }), + + _ => unreachable!(), + } + } + + fn destroyed( + &self, + _state: &mut D, + _client: wayland_server::backend::ClientId, + resource: &ZwlrDataControlDeviceV1, + ) { + if let Some(seat) = Seat::::from_resource(&self.wl_seat) { + if let Some(seat_data) = seat.user_data().get::>>() { + seat_data.borrow_mut().retain_devices(|ndd| match ndd { + SelectionDevice::WlrDataControl(ndd) => ndd != resource, + _ => true, + }); + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/wlr_data_control/mod.rs b/vendor/smithay/src/wayland/selection/wlr_data_control/mod.rs new file mode 100644 index 0000000..0074889 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/wlr_data_control/mod.rs @@ -0,0 +1,259 @@ +//! Automatic handling of the `wlr_data_control` protocol +//! +//! ## Initialization +//! +//! To initialize this implementation, create [`DataControlState`], store it in your `State` +//! struct, and implement the required trait, as shown in the example: +//! +//! ``` +//! # extern crate wayland_server; +//! # #[macro_use] extern crate smithay; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::selection::SelectionHandler; +//! use smithay::wayland::selection::wlr_data_control::{DataControlState, DataControlHandler}; +//! # use smithay::input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}; +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! # struct State { data_control_state: DataControlState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the data_control state +//! let data_control_state = DataControlState::new::( +//! &display.handle(), None, |_| true +//! ); +//! +//! // insert the DataControlState into your state +//! // .. +//! +//! // implement the necessary traits +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! # impl SeatHandler for State { +//! # type KeyboardFocus = WlSurface; +//! # type PointerFocus = WlSurface; +//! # type TouchFocus = WlSurface; +//! # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! # } +//! # impl PointerConstraintsHandler for State {} +//! impl SelectionHandler for State { +//! type SelectionUserData = (); +//! } +//! impl DataControlHandler for State { +//! fn data_control_state(&mut self) -> &mut DataControlState { &mut self.data_control_state } +//! // ... override default implementations here to customize handling ... +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! Be aware that data control clients rely on other selection providers to be implemneted, like +//! wl_data_device or zwp_primary_selection. + +use std::collections::HashMap; +use std::sync::Arc; + +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_seat::WlSeat; +use wayland_server::{Client, DisplayHandle, GlobalDispatch}; + +mod device; +mod source; + +pub use device::DataControlDeviceUserData; +pub use source::DataControlSourceUserData; + +use super::SelectionHandler; +use super::primary_selection::PrimarySelectionState; + +/// Access the data control state. +pub trait DataControlHandler: Sized + SelectionHandler { + /// [`DataControlState`] getter. + fn data_control_state(&mut self) -> &mut DataControlState; +} + +/// State of the data control. +#[derive(Debug)] +pub struct DataControlState { + manager_global: GlobalId, + /// Used sources. + /// + /// Protocol states that each source can only be used once. We + /// also use it during destruction to get seat data. + pub(crate) used_sources: HashMap, +} + +impl DataControlState { + /// Register new [ZwlrDataControlManagerV1] global. + /// + /// Passing `primary_selection` will enable support for primary selection as well. + pub fn new( + display: &DisplayHandle, + primary_selection: Option<&PrimarySelectionState>, + filter: F, + ) -> Self + where + D: GlobalDispatch + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = DataControlManagerGlobalData { + primary_selection_filter: primary_selection + .map(|x| Arc::clone(&x.filter)) + .unwrap_or_else(|| Arc::new(Box::new(|_| false))), + filter: Box::new(filter), + }; + let manager_global = display.create_global::(2, data); + Self { + manager_global, + used_sources: Default::default(), + } + } + + /// [ZwlrDataControlManagerV1] GlobalId getter. + pub fn global(&self) -> GlobalId { + self.manager_global.clone() + } +} + +#[allow(missing_debug_implementations)] +#[doc(hidden)] +pub struct DataControlManagerGlobalData { + /// Filter whether the clients can read/modify the primary selection. + primary_selection_filter: Arc Fn(&'c Client) -> bool + Send + Sync>>, + + /// Filter whether the clients can view global. + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +#[allow(missing_debug_implementations)] +#[doc(hidden)] +#[derive(Clone)] +pub struct DataControlManagerUserData { + /// Filter whether the clients can read/modify the primary selection. + primary_selection_filter: Arc Fn(&'c Client) -> bool + Send + Sync>>, +} + +mod handlers { + use std::cell::RefCell; + use std::sync::Arc; + + use tracing::error; + use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1; + use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1; + use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1; + use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_source_v1::ZwlrDataControlSourceV1; + use wayland_server::{Client, Dispatch, DisplayHandle}; + + use crate::input::Seat; + use crate::wayland::selection::SelectionTarget; + use crate::wayland::selection::device::SelectionDevice; + use crate::wayland::selection::seat_data::SeatData; + use crate::wayland::{Dispatch2, GlobalDispatch2}; + + use super::DataControlDeviceUserData; + use super::DataControlHandler; + use super::DataControlManagerGlobalData; + use super::DataControlManagerUserData; + use super::DataControlSourceUserData; + + impl GlobalDispatch2 for DataControlManagerGlobalData + where + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: DataControlHandler, + D: 'static, + { + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init( + resource, + DataControlManagerUserData { + primary_selection_filter: Arc::clone(&self.primary_selection_filter), + }, + ); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } + } + + impl Dispatch2 for DataControlManagerUserData + where + D: Dispatch, + D: Dispatch, + D: DataControlHandler, + D: 'static, + { + fn request( + &self, + _handler: &mut D, + client: &wayland_server::Client, + _resource: &ZwlrDataControlManagerV1, + request: ::Request, + dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwlr_data_control_manager_v1::Request::CreateDataSource { id } => { + data_init.init(id, DataControlSourceUserData::new(dh.clone())); + } + zwlr_data_control_manager_v1::Request::GetDataDevice { id, seat: wl_seat } => { + match Seat::::from_resource(&wl_seat) { + Some(seat) => { + seat.user_data() + .insert_if_missing(|| RefCell::new(SeatData::::new())); + + let device = SelectionDevice::WlrDataControl(data_init.init( + id, + DataControlDeviceUserData { + wl_seat, + primary_selection_filter: Arc::clone(&self.primary_selection_filter), + }, + )); + + let mut seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut(); + + seat_data.add_device(device.clone()); + + // NOTE: broadcast selection only to the newly created device. + let device = Some(&device); + seat_data.send_selection::(dh, SelectionTarget::Clipboard, device, true); + if (*self.primary_selection_filter)(client) { + seat_data.send_selection::(dh, SelectionTarget::Primary, device, true); + } + } + None => { + error!( + data_control_device = ?id, + client = ?client, + "Unmanaged seat given to a primary selection device." + ); + } + } + } + zwlr_data_control_manager_v1::Request::Destroy => (), + _ => unreachable!(), + } + } + } +} diff --git a/vendor/smithay/src/wayland/selection/wlr_data_control/source.rs b/vendor/smithay/src/wayland/selection/wlr_data_control/source.rs new file mode 100644 index 0000000..b9fbeb0 --- /dev/null +++ b/vendor/smithay/src/wayland/selection/wlr_data_control/source.rs @@ -0,0 +1,108 @@ +use std::cell::RefCell; +use std::sync::Mutex; + +use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_source_v1::{ + self, ZwlrDataControlSourceV1, +}; +use wayland_server::DisplayHandle; +use wayland_server::backend::ClientId; + +use crate::input::Seat; +use crate::wayland::Dispatch2; +use crate::wayland::selection::SelectionTarget; +use crate::wayland::selection::offer::OfferReplySource; +use crate::wayland::selection::seat_data::SeatData; +use crate::wayland::selection::source::SelectionSourceProvider; + +use super::DataControlHandler; + +#[doc(hidden)] +#[derive(Debug)] +pub struct DataControlSourceUserData { + pub(crate) inner: Mutex, + display_handle: DisplayHandle, +} + +impl DataControlSourceUserData { + pub(crate) fn new(display_handle: DisplayHandle) -> Self { + Self { + inner: Default::default(), + display_handle, + } + } +} + +/// The metadata describing a data source +#[derive(Debug, Default, Clone)] +pub struct SourceMetadata { + /// The MIME types supported by this source + pub mime_types: Vec, +} + +impl Dispatch2 for DataControlSourceUserData +where + D: DataControlHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &ZwlrDataControlSourceV1, + request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwlr_data_control_source_v1::Request::Offer { mime_type } => { + let mut data = self.inner.lock().unwrap(); + data.mime_types.push(mime_type); + } + zwlr_data_control_source_v1::Request::Destroy => (), + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, source: &ZwlrDataControlSourceV1) { + // Remove the source from the used ones. + let seat = match state + .data_control_state() + .used_sources + .remove(source) + .as_ref() + .and_then(Seat::::from_resource) + { + Some(seat) => seat, + None => return, + }; + + let mut seat_data = seat + .user_data() + .get::>>() + .unwrap() + .borrow_mut(); + + for target in [SelectionTarget::Primary, SelectionTarget::Clipboard] { + let selection = match target { + SelectionTarget::Primary => seat_data.get_primary_selection(), + SelectionTarget::Clipboard => seat_data.get_clipboard_selection(), + }; + + match selection { + Some(OfferReplySource::Client(SelectionSourceProvider::WlrDataControl(set_source))) + if set_source == source => + { + match target { + SelectionTarget::Primary => { + seat_data.set_primary_selection::(&self.display_handle, None) + } + SelectionTarget::Clipboard => { + seat_data.set_clipboard_selection::(&self.display_handle, None) + } + } + } + _ => (), + }; + } + } +} diff --git a/vendor/smithay/src/wayland/session_lock/lock.rs b/vendor/smithay/src/wayland/session_lock/lock.rs new file mode 100644 index 0000000..e7430a9 --- /dev/null +++ b/vendor/smithay/src/wayland/session_lock/lock.rs @@ -0,0 +1,145 @@ +//! ext-session-lock lock. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use crate::wayland::compositor::SurfaceAttributes; +use crate::wayland::compositor::{self, BufferAssignment}; +use _session_lock::ext_session_lock_surface_v1::ExtSessionLockSurfaceV1; +use _session_lock::ext_session_lock_v1::{Error, ExtSessionLockV1, Request}; +use wayland_protocols::ext::session_lock::v1::server::{self as _session_lock}; +use wayland_server::protocol::wl_output::WlOutput; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, Resource}; + +use crate::wayland::Dispatch2; +use crate::wayland::session_lock::surface::{ExtLockSurfaceUserData, LockSurface, LockSurfaceAttributes}; +use crate::wayland::session_lock::{LockStatus, SessionLockHandler}; + +/// Surface role for ext-session-lock surfaces. +const LOCK_SURFACE_ROLE: &str = "ext_session_lock_surface_v1"; + +/// [`ExtSessionLockV1`] state. +#[derive(Debug)] +pub struct SessionLockState { + pub(super) done: Arc, + locked_outputs: Mutex>, +} + +impl SessionLockState { + pub(super) fn new() -> Self { + Self { + done: Arc::new(AtomicBool::new(false)), + locked_outputs: Default::default(), + } + } +} + +impl Dispatch2 for SessionLockState +where + D: Dispatch, + D: SessionLockHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + lock: &ExtSessionLockV1, + request: Request, + _display: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + Request::GetLockSurface { id, surface, output } => { + // Assign surface a role and ensure it never had one before. + if compositor::give_role(&surface, LOCK_SURFACE_ROLE).is_err() { + lock.post_error(Error::Role, "Surface already has a role."); + return; + } + + // Ensure output is not already locked. + let mut locked_outputs = self.locked_outputs.lock().unwrap(); + if locked_outputs.contains(&output) { + lock.post_error(Error::DuplicateOutput, "Output is already locked."); + return; + } + locked_outputs.push(output.clone()); + drop(locked_outputs); + + // Ensure surface has no existing buffers attached. + let has_buffer = compositor::with_states(&surface, |states| { + let cached = &states.cached_state; + let mut guard = cached.get::(); + let pending = matches!(guard.pending().buffer, Some(BufferAssignment::NewBuffer(_))); + let current = matches!(guard.current().buffer, Some(BufferAssignment::NewBuffer(_))); + pending || current + }); + if has_buffer { + lock.post_error(Error::AlreadyConstructed, "Surface has a buffer attached."); + return; + } + + let data = ExtLockSurfaceUserData { + surface: surface.downgrade(), + done: Arc::clone(&self.done), + }; + let lock_surface = data_init.init(id, data); + + // Initialize surface data. + compositor::with_states(&surface, |states| { + let inserted = states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(LockSurfaceAttributes::new(lock_surface.clone())) + }); + + if !inserted { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + attributes.surface = lock_surface.clone(); + } + }); + + // Add pre-commit hook for updating surface state. + compositor::add_pre_commit_hook::(&surface, LockSurface::pre_commit_hook); + + if !self.done.load(Ordering::Acquire) { + // Call compositor handler. + let lock_surface = LockSurface::new(lock.clone(), surface, lock_surface); + state.new_surface(lock_surface.clone(), output); + + // Send initial configure when the interface is bound. + lock_surface.send_configure(); + } + } + Request::UnlockAndDestroy => { + // Ensure session is locked, and with the same lock instance. + if !state.lock_state().lock_status.lock().unwrap().is_locked_by(lock) { + lock.post_error(Error::InvalidUnlock, "Session is not locked."); + } else { + *state.lock_state().lock_status.lock().unwrap() = LockStatus::Unlocked; + state.unlock(); + } + } + Request::Destroy => { + // Ensure session is not locked. + if state.lock_state().lock_status.lock().unwrap().is_locked_by(lock) { + lock.post_error(Error::InvalidDestroy, "Cannot destroy session lock while locked."); + } + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: wayland_server::backend::ClientId, lock: &ExtSessionLockV1) { + let mut lock_status = state.lock_state().lock_status.lock().unwrap(); + if lock_status.is_locked_by(lock) { + // The client has disconnected without unlocking the session, so reset our state. It + // is up to the compositor's policy to decide whether it is allowed for another client + // to connect and take over the session-locker responsibility. + *lock_status = LockStatus::Defunct; + } + } +} diff --git a/vendor/smithay/src/wayland/session_lock/mod.rs b/vendor/smithay/src/wayland/session_lock/mod.rs new file mode 100644 index 0000000..6ac4f8d --- /dev/null +++ b/vendor/smithay/src/wayland/session_lock/mod.rs @@ -0,0 +1,250 @@ +//! Utilities for handling the `ext-session-lock` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation create the [`SessionLockManagerState`] and +//! implement the [`SessionLockHandler`], as shown in this example: +//! +//! ``` +//! use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; +//! use smithay::wayland::session_lock::{ +//! LockSurface, SessionLockManagerState, SessionLockHandler, SessionLocker, +//! }; +//! +//! # struct State { session_lock_state: SessionLockManagerState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the compositor state +//! let session_lock_state = SessionLockManagerState::new::(&display.handle(), |_| true); +//! +//! // Insert the SessionLockManagerState into your state. +//! +//! // Implement the necessary trait. +//! impl SessionLockHandler for State { +//! fn lock_state(&mut self) -> &mut SessionLockManagerState { +//! &mut self.session_lock_state +//! } +//! +//! fn lock(&mut self, _confirmation: SessionLocker) { +//! // Lock and clear the screen. +//! +//! // Call `confirmation.lock()` after a cleared frame was presented on all outputs. +//! +//! // Dropping `confirmation` will cancel the locking. +//! } +//! +//! fn unlock(&mut self) { +//! // Remove session lock. +//! } +//! +//! fn new_surface(&mut self, _surface: LockSurface, _output: WlOutput) { +//! // Display `LockSurface` on `WlOutput`. +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use _session_lock::ext_session_lock_manager_v1::{ExtSessionLockManagerV1, Request}; +use _session_lock::ext_session_lock_v1::ExtSessionLockV1; +use wayland_protocols::ext::session_lock::v1::server as _session_lock; +use wayland_server::protocol::wl_output::WlOutput; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +mod lock; +mod surface; + +pub use lock::SessionLockState; +pub use surface::{ + ExtLockSurfaceUserData, LockSurface, LockSurfaceAttributes, LockSurfaceCachedState, LockSurfaceConfigure, + LockSurfaceData, LockSurfaceState, +}; + +const MANAGER_VERSION: u32 = 1; + +#[derive(Debug)] +enum LockStatus { + /// The session is unlocked. + Unlocked, + /// The session has been locked, and is owned by the specified instance. + Locked(ExtSessionLockV1), + /// The session was locked, but the locking client disconnected without unlocking. + Defunct, +} + +impl LockStatus { + fn is_locked_by(&self, session_lock: &ExtSessionLockV1) -> bool { + matches!(self, Self::Locked(owning_session_lock) if owning_session_lock == session_lock) + } +} + +/// State of the [`ExtSessionLockManagerV1`] Global. +#[derive(Debug)] +pub struct SessionLockManagerState { + lock_status: Arc>, +} + +impl SessionLockManagerState { + /// Create new [`ExtSessionLockManagerV1`] global. + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: SessionLockHandler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = SessionLockManagerGlobalData { + filter: Box::new(filter), + }; + display.create_global::(MANAGER_VERSION, data); + + Self { + lock_status: Arc::new(Mutex::new(LockStatus::Unlocked)), + } + } +} + +#[allow(missing_debug_implementations)] +#[doc(hidden)] +pub struct SessionLockManagerGlobalData { + /// Filter whether the clients can view global. + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +impl GlobalDispatch2 for SessionLockManagerGlobalData +where + D: Dispatch, + D: SessionLockHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _display: &DisplayHandle, + _client: &Client, + manager: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(manager, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: SessionLockHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _manager: &ExtSessionLockManagerV1, + request: Request, + _display: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + Request::Lock { id } => { + let lock_state = SessionLockState::new(); + let done = Arc::clone(&lock_state.done); + let lock = data_init.init(id, lock_state); + let lock_status = Arc::clone(&state.lock_state().lock_status); + state.lock(SessionLocker::new(lock, lock_status, done)); + } + Request::Destroy => (), + _ => unreachable!(), + } + } +} + +/// Handler trait for ext-session-lock. +pub trait SessionLockHandler { + /// Session lock state. + fn lock_state(&mut self) -> &mut SessionLockManagerState; + + /// Handle compositor locking requests. + /// + /// The [`SessionLocker`] parameter is used to confirm once the session was + /// locked and no more client data is accessible using the + /// [`SessionLocker::lock`] method. + /// + /// If locking was not possible, dropping the [`SessionLocker`] will + /// automatically notify the requesting client about the failure. + fn lock(&mut self, confirmation: SessionLocker); + + /// Handle compositor lock removal. + fn unlock(&mut self); + + /// Add a new lock surface for an output. + /// + /// Note that this may be called for surfaces created on different [`SessionLocker`] instances + /// if more than one client is racing to lock the screen, so you should match [`LockSurface`] + /// instances with [`SessionLocker`] instances using the underlying [`ExtSessionLockV1`] + /// instance of both. + /// + /// Once you have dropped the [`SessionLocker`] instances that will not be locking the session, + /// this function will no longer be called for surfaces created by those locker instances. + fn new_surface(&mut self, surface: LockSurface, output: WlOutput); + + /// A surface has acknowledged a configure serial. + fn ack_configure(&mut self, _surface: WlSurface, _configure: LockSurfaceConfigure) {} +} + +/// Manage session locking. +/// +/// See [`SessionLockHandler::lock`] for more detail. +#[derive(Debug)] +pub struct SessionLocker { + lock: Option, + lock_status: Arc>, + done: Arc, +} + +impl Drop for SessionLocker { + fn drop(&mut self) { + // If the session wasn't locked, we notify clients about the failure. + if let Some(lock) = self.lock.take() { + lock.finished(); + self.done.store(true, Ordering::Release); + } + } +} + +impl SessionLocker { + fn new(lock: ExtSessionLockV1, lock_status: Arc>, done: Arc) -> Self { + Self { + lock: Some(lock), + lock_status, + done, + } + } + + /// Get the underlying [`ExtSessionLockV1`] + pub fn ext_session_lock(&self) -> &ExtSessionLockV1 { + self.lock.as_ref().unwrap() + } + + /// Notify the client that the session lock was successful. + pub fn lock(mut self) { + if let Some(lock) = self.lock.take() { + *self.lock_status.lock().unwrap() = LockStatus::Locked(lock.clone()); + lock.locked(); + } + } +} diff --git a/vendor/smithay/src/wayland/session_lock/surface.rs b/vendor/smithay/src/wayland/session_lock/surface.rs new file mode 100644 index 0000000..490047e --- /dev/null +++ b/vendor/smithay/src/wayland/session_lock/surface.rs @@ -0,0 +1,425 @@ +//! ext-session-lock surface. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use crate::backend::renderer::buffer_dimensions; +use crate::utils::{IsAlive, Logical, SERIAL_COUNTER, Serial, Size}; +use crate::wayland::compositor::{self, BufferAssignment, Cacheable, SurfaceAttributes}; +use crate::wayland::viewporter::{ViewportCachedState, ViewporterSurfaceState}; +use _session_lock::ext_session_lock_surface_v1::{Error, ExtSessionLockSurfaceV1, Request}; +use tracing::trace_span; +use wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1; +use wayland_protocols::ext::session_lock::v1::server::{self as _session_lock, ext_session_lock_surface_v1}; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::{Client, DataInit, DisplayHandle, Resource, Weak}; + +use crate::wayland::Dispatch2; +use crate::wayland::session_lock::SessionLockHandler; + +/// User data for ext-session-lock surfaces. +#[derive(Debug)] +pub struct ExtLockSurfaceUserData { + // `LockSurfaceAttributes` stored in the surface `data_map` contains a + // `ExtSessionLockSurfaceV1`. So this reference needs to be weak to avoid a + // cycle. + pub(crate) surface: Weak, + pub(super) done: Arc, +} + +impl Dispatch2 for ExtLockSurfaceUserData +where + D: SessionLockHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + lock_surface: &ExtSessionLockSurfaceV1, + request: Request, + _display: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + Request::AckConfigure { serial } => { + let Ok(surface) = self.surface.upgrade() else { + return; + }; + + // Find configure for this serial. + let serial = Serial::from(serial); + let configure = compositor::with_states(&surface, |states| { + let surface_data = states.data_map.get::>(); + surface_data.unwrap().lock().unwrap().ack_configure(serial) + }); + + match configure { + Some(configure) => { + if !self.done.load(Ordering::Acquire) { + state.ack_configure(surface.clone(), configure); + } + } + None => lock_surface.post_error( + Error::InvalidSerial, + format!("wrong configure serial: {}", ::from(serial)), + ), + } + } + Request::Destroy => (), + _ => unreachable!(), + } + } + + fn destroyed( + &self, + _state: &mut D, + _client: wayland_server::backend::ClientId, + _resource: &ExtSessionLockSurfaceV1, + ) { + if let Ok(surface) = self.surface.upgrade() { + compositor::with_states(&surface, |states| { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + attributes.reset(); + + let mut guard = states.cached_state.get::(); + *guard.pending() = Default::default(); + *guard.current() = Default::default(); + }); + } + } +} + +/// Data associated with session lock surface +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::session_lock::LockSurfaceData; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// states.data_map.get::(); +/// }); +/// ``` +pub type LockSurfaceData = Mutex; + +/// Attributes for ext-session-lock surfaces. +#[derive(Debug)] +pub struct LockSurfaceAttributes { + pub(crate) surface: ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, + + /// Holds the pending state as set by the server. + pub server_pending: Option, + + /// Holds the configures the server has sent out to the client waiting to be + /// acknowledged by the client. All pending configures that are older than + /// the acknowledged one will be discarded during processing + /// layer_surface.ack_configure. + pub pending_configures: Vec, + + /// Holds the last configure that has been acknowledged by the client. This state should be + /// cloned to the current during a commit. Note that this state can be newer than the last + /// acked state at the time of the last commit. + pub last_acked: Option, +} + +impl LockSurfaceAttributes { + pub(crate) fn new(surface: ext_session_lock_surface_v1::ExtSessionLockSurfaceV1) -> Self { + Self { + surface, + server_pending: None, + pending_configures: vec![], + last_acked: None, + } + } + + fn ack_configure(&mut self, serial: Serial) -> Option { + let configure = self + .pending_configures + .iter() + .find(|configure| configure.serial == serial) + .cloned()?; + + self.pending_configures + .retain(|configure| configure.serial > serial); + self.last_acked = Some(configure); + + Some(configure) + } + + fn reset(&mut self) { + self.server_pending = None; + self.pending_configures = Vec::new(); + self.last_acked = None; + } + + fn current_server_state(&self) -> Option<&LockSurfaceState> { + self.pending_configures + .last() + .map(|c| &c.state) + .or(self.last_acked.as_ref().map(|c| &c.state)) + } +} + +/// Handle for a ext-session-lock surface. +#[derive(Clone, Debug)] +pub struct LockSurface { + lock: ExtSessionLockV1, + shell_surface: ExtSessionLockSurfaceV1, + surface: WlSurface, +} + +impl PartialEq for LockSurface { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.surface == other.surface + } +} + +impl LockSurface { + pub(crate) fn new( + lock: ExtSessionLockV1, + surface: WlSurface, + shell_surface: ExtSessionLockSurfaceV1, + ) -> Self { + Self { + lock, + surface, + shell_surface, + } + } + + /// Check if the surface is still alive. + #[inline] + pub fn alive(&self) -> bool { + self.surface.alive() + } + + /// Returns the lock instance this surface is associated with. + pub fn ext_session_lock(&self) -> &ExtSessionLockV1 { + &self.lock + } + + /// Get the current pending configure state. + pub fn get_pending_state(&self, attributes: &mut LockSurfaceAttributes) -> Option { + let server_pending = attributes.server_pending.take()?; + + // Check if last state matches pending state. + match attributes.current_server_state() { + Some(state) if state == &server_pending => None, + _ => Some(server_pending), + } + } + + /// Send a configure to the surface. + /// + /// You can manipulate the client's state using + /// [`LockSurface::with_pending_state`]. + pub fn send_configure(&self) { + compositor::with_states(&self.surface, |states| { + // Get surface attributes. + let attributes = states.data_map.get::>(); + let mut attributes = attributes.unwrap().lock().unwrap(); + + // Create our new configure event. + let pending = match self.get_pending_state(&mut attributes) { + Some(pending) => pending, + None => return, + }; + let configure = LockSurfaceConfigure::new(pending); + + // Extract client configure state. + let (width, height) = configure.state.size.unwrap_or_default().into(); + let serial = configure.serial; + + // Update pending state. + attributes.pending_configures.push(configure); + + // Send configure to the client. + self.shell_surface.configure(serial.into(), width, height); + }) + } + + /// Access the underlying [`WlSurface`]. + #[inline] + pub fn wl_surface(&self) -> &WlSurface { + &self.surface + } + + /// Manipulate this surface's pending state. + pub fn with_pending_state(&self, f: F) -> T + where + F: FnOnce(&mut LockSurfaceState) -> T, + { + compositor::with_states(&self.surface, |states| { + let attributes = states.data_map.get::>(); + let mut attributes = attributes.unwrap().lock().unwrap(); + + // Ensure pending state is initialized. + if attributes.server_pending.is_none() { + attributes.server_pending = + Some(attributes.current_server_state().cloned().unwrap_or_default()); + } + + let server_pending = attributes.server_pending.as_mut().unwrap(); + f(server_pending) + }) + } + + /// Provides access to the current committed cached state. + pub fn with_cached_state(&self, f: F) -> T + where + F: FnOnce(&LockSurfaceCachedState) -> T, + { + compositor::with_states(&self.surface, |states| { + let mut guard = states.cached_state.get::(); + f(guard.current()) + }) + } + + /// Provides access to the current committed state. + /// + /// This is the state that the client last acked before making the current commit. + pub fn with_committed_state(&self, f: F) -> T + where + F: FnOnce(Option<&LockSurfaceState>) -> T, + { + self.with_cached_state(move |state| f(state.last_acked.as_ref().map(|c| &c.state))) + } + + /// Handles the role specific commit error checking + /// + /// This should be called when the underlying WlSurface + /// handles a wl_surface.commit request. + pub(crate) fn pre_commit_hook(_state: &mut D, _dh: &DisplayHandle, surface: &WlSurface) { + let _span = trace_span!("session-lock-surface pre-commit", surface = %surface.id()).entered(); + + compositor::with_states(surface, |states| { + let role = states.data_map.get::().unwrap().lock().unwrap(); + + if !role.surface.is_alive() { + return; + } + + let Some(last_acked) = role.last_acked else { + role.surface.post_error( + ext_session_lock_surface_v1::Error::CommitBeforeFirstAck, + "Committed before the first ack_configure.", + ); + return; + }; + let LockSurfaceConfigure { state, serial: _ } = &last_acked; + + let mut guard_layer = states.cached_state.get::(); + let pending = guard_layer.pending(); + + // The presence of last_acked always follows the buffer assignment because the + // surface is not allowed to attach a buffer without acking the initial configure. + let had_buffer_before = pending.last_acked.is_some(); + + let mut guard_surface = states.cached_state.get::(); + let surface_attrs = guard_surface.pending(); + let has_buffer = match &surface_attrs.buffer { + Some(BufferAssignment::NewBuffer(buffer)) => { + // Verify buffer size. + if let Some(buf_size) = buffer_dimensions(buffer) { + let viewport = states + .data_map + .get::() + .map(|v| v.lock().unwrap()); + let surface_size = if let Some(dest) = viewport.as_ref().and_then(|_| { + let mut guard = states.cached_state.get::(); + let viewport_state = guard.pending(); + viewport_state.dst + }) { + Size::from((dest.w as u32, dest.h as u32)) + } else { + let scale = surface_attrs.buffer_scale; + let transform = surface_attrs.buffer_transform.into(); + let surface_size = buf_size.to_logical(scale, transform); + + Size::from((surface_size.w as u32, surface_size.h as u32)) + }; + + if Some(surface_size) != state.size { + role.surface.post_error( + ext_session_lock_surface_v1::Error::DimensionsMismatch, + "Surface dimensions do not match acked configure.", + ); + return; + } + } + + true + } + Some(BufferAssignment::Removed) => { + role.surface.post_error( + ext_session_lock_surface_v1::Error::NullBuffer, + "Surface attached a NULL buffer.", + ); + return; + } + None => had_buffer_before, + }; + + if has_buffer { + // The surface remains, or became mapped, track the last acked state. + pending.last_acked = Some(last_acked); + } else { + // The surface remains unmapped, meaning that it's in the initial configure stage. + pending.last_acked = None; + } + + // Lock surfaces aren't allowed to attach a null buffer, and therefore to unmap. + }); + } +} + +/// State of an ext-session-lock surface. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub struct LockSurfaceState { + /// The suggested size of the surface. + pub size: Option>, +} + +/// A configure message for ext-session-lock surfaces. +#[derive(Debug, Copy, Clone)] +pub struct LockSurfaceConfigure { + /// The state associated with this configure. + pub state: LockSurfaceState, + + /// A serial number to track acknowledgment from the client. + pub serial: Serial, +} + +impl LockSurfaceConfigure { + fn new(state: LockSurfaceState) -> Self { + Self { + serial: SERIAL_COUNTER.next_serial(), + state, + } + } +} + +/// Represents the client pending state +#[derive(Debug, Default, Copy, Clone)] +pub struct LockSurfaceCachedState { + /// Configure last acknowledged by the client at the time of the commit. + /// + /// Reset to `None` when the surface unmaps. + pub last_acked: Option, +} + +impl Cacheable for LockSurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} diff --git a/vendor/smithay/src/wayland/shell/kde/decoration.rs b/vendor/smithay/src/wayland/shell/kde/decoration.rs new file mode 100644 index 0000000..2bb4dc3 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/kde/decoration.rs @@ -0,0 +1,146 @@ +//! KDE Window decoration manager +//! +//! This interface allows a compositor to announce support for KDE's legacy server-side decorations. +//! +//! A client can use this protocol to request being decorated by a supporting compositor. +//! +//! ``` +//! extern crate wayland_server; +//! extern crate smithay; +//! +//! use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState}; +//! use wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager::Mode; +//! +//! # struct State { kde_decoration_state: KdeDecorationState }; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! // Create the new KdeDecorationState. +//! let state = KdeDecorationState::new::(&display.handle(), Mode::Server); +//! +//! // Insert KdeDecorationState into your compositor state. +//! // … +//! +//! // Implement KDE server decoration handlers. +//! impl KdeDecorationHandler for State { +//! fn kde_decoration_state(&self) -> &KdeDecorationState { +//! &self.kde_decoration_state +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration::{ + Mode, OrgKdeKwinServerDecoration, +}; +use wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager::{ + Mode as DefaultMode, OrgKdeKwinServerDecorationManager, +}; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::{Client, Dispatch, DisplayHandle, GlobalDispatch, WEnum}; + +use crate::wayland::GlobalData; + +/// KDE server decoration handler. +pub trait KdeDecorationHandler { + /// Return the KDE server decoration state. + fn kde_decoration_state(&self) -> &KdeDecorationState; + + /// Handle new decoration object creation. + /// + /// Called whenever a new decoration object is created, usually this happens when a new window + /// is opened. + fn new_decoration(&mut self, _surface: &WlSurface, _decoration: &OrgKdeKwinServerDecoration) {} + + /// Handle surface decoration mode requests. + /// + /// Called when a surface requests a specific decoration mode or acknowledged the compositor's + /// decoration request. + /// + /// **It is up to the compositor to prevent feedback loops**, a client is free to ignore modes + /// suggested by [`OrgKdeKwinServerDecoration::mode`] and instead request their preferred mode + /// instead. + fn request_mode( + &mut self, + _surface: &WlSurface, + decoration: &OrgKdeKwinServerDecoration, + mode: WEnum, + ) { + if let WEnum::Value(mode) = mode { + decoration.mode(mode); + } + } + + /// Handle decoration object removal for a surface. + fn release(&mut self, _decoration: &OrgKdeKwinServerDecoration, _surface: &WlSurface) {} +} + +/// KDE server decoration state. +#[derive(Debug)] +pub struct KdeDecorationState { + pub(crate) default_mode: DefaultMode, + + kde_decoration_manager: GlobalId, +} + +/// Data associated with a KdeDecorationManager global. +#[allow(missing_debug_implementations)] +pub struct KdeDecorationManagerGlobalData { + pub(crate) filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct KwinServerDecorationData(pub(crate) WlSurface); + +impl KdeDecorationState { + /// Create a new KDE server decoration global. + pub fn new(display: &DisplayHandle, default_mode: DefaultMode) -> Self + where + D: GlobalDispatch + + Dispatch + + Dispatch + + KdeDecorationHandler + + 'static, + { + Self::new_with_filter::(display, default_mode, |_| true) + } + + /// Create a new KDE server decoration global with a filter. + /// + /// Filters can be used to limit visibility of a global to certain clients. + pub fn new_with_filter(display: &DisplayHandle, default_mode: DefaultMode, filter: F) -> Self + where + D: GlobalDispatch + + Dispatch + + Dispatch + + KdeDecorationHandler + + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = KdeDecorationManagerGlobalData { + filter: Box::new(filter), + }; + let kde_decoration_manager = + display.create_global::(1, data); + + Self { + kde_decoration_manager, + default_mode, + } + } + + /// Returns the id of the [`OrgKdeKwinServerDecorationManager`] global. + pub fn global(&self) -> GlobalId { + self.kde_decoration_manager.clone() + } + + /// Updates the default decoration mode. + /// + /// The new default mode will only be sent to new binds to the manager object; it is up to you + /// to update any existing decoration instances with the new mode, if desired. + pub fn set_default_mode(&mut self, default_mode: DefaultMode) { + self.default_mode = default_mode; + } +} diff --git a/vendor/smithay/src/wayland/shell/kde/handlers.rs b/vendor/smithay/src/wayland/shell/kde/handlers.rs new file mode 100644 index 0000000..ebb100e --- /dev/null +++ b/vendor/smithay/src/wayland/shell/kde/handlers.rs @@ -0,0 +1,100 @@ +//! Handlers for KDE decoration events. +use tracing::trace; + +use wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration::{ + OrgKdeKwinServerDecoration, Request, +}; +use wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager::{ + OrgKdeKwinServerDecorationManager, Request as ManagerRequest, +}; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, New, Resource}; + +use crate::wayland::shell::kde::decoration::KdeDecorationHandler; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +use super::decoration::{KdeDecorationManagerGlobalData, KwinServerDecorationData}; + +impl GlobalDispatch2 for KdeDecorationManagerGlobalData +where + D: Dispatch + + Dispatch + + KdeDecorationHandler + + 'static, +{ + fn bind( + &self, + state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let kde_decoration_manager = data_init.init(resource, GlobalData); + + // Set default decoration mode. + let default_mode = state.kde_decoration_state().default_mode; + kde_decoration_manager.default_mode(default_mode); + + trace!("Bound decoration manager global"); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch + KdeDecorationHandler + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _kde_decoration_manager: &OrgKdeKwinServerDecorationManager, + request: ManagerRequest, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + let (id, surface) = match request { + ManagerRequest::Create { id, surface } => (id, surface), + _ => unreachable!(), + }; + + let kde_decoration = data_init.init(id, KwinServerDecorationData(surface)); + + let surface = &kde_decoration.data::().unwrap().0; + state.new_decoration(surface, &kde_decoration); + + trace!(surface = ?surface, "Created decoration object for surface"); + } +} + +impl Dispatch2 for KwinServerDecorationData +where + D: KdeDecorationHandler + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + kde_decoration: &OrgKdeKwinServerDecoration, + request: Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let surface = &self.0; + + trace!( + surface = ?surface, + request = ?request, + "Decoration request for surface" + ); + + match request { + Request::RequestMode { mode } => state.request_mode(surface, kde_decoration, mode), + Request::Release => state.release(kde_decoration, surface), + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/shell/kde/mod.rs b/vendor/smithay/src/wayland/shell/kde/mod.rs new file mode 100644 index 0000000..a9598d8 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/kde/mod.rs @@ -0,0 +1,4 @@ +//! Handler utilities for KDE shell protocols. + +pub mod decoration; +mod handlers; diff --git a/vendor/smithay/src/wayland/shell/mod.rs b/vendor/smithay/src/wayland/shell/mod.rs new file mode 100644 index 0000000..554cbcc --- /dev/null +++ b/vendor/smithay/src/wayland/shell/mod.rs @@ -0,0 +1,81 @@ +//! Handler utilities for the various shell protocols +//! +//! Wayland, via its different protocol extensions, supports different kind of +//! shells. Here a shell represent the logic associated to displaying windows and +//! arranging them on the screen. +//! +//! The shell protocols thus define what kind of interactions a client can have with +//! the compositor to properly display its contents on the screen. +//! +//! Smithay currently provides three of them: +//! +//! - The [`xdg`](xdg/index.html) module provides handlers for the `xdg_shell` protocol, which is +//! the current standard for desktop apps +//! - The [`wlr_layer`](wlr_layer/index.html) module provides handlers for the `wlr_layer_shell` +//! protocol, which is for windows rendering above/below normal XDG windows +//! - The [`kde`](kde/index.html) module provides handlers for KDE-specific protocols + +use crate::{utils::Serial, wayland::compositor}; +use thiserror::Error; +use wayland_server::protocol::wl_surface::WlSurface; +use xdg::XdgToplevelSurfaceData; + +pub mod kde; +pub mod wlr_layer; +pub mod xdg; + +/// Represents the possible errors returned from +/// a surface ping +#[derive(Debug, Error)] +pub enum PingError { + /// The operation failed because the underlying surface has been destroyed + #[error("the ping failed cause the underlying surface has been destroyed")] + DeadSurface, + /// There is already a pending ping + #[error("there is already a ping pending `{0:?}`")] + PingAlreadyPending(Serial), +} + +/// Returns true if the surface is toplevel equivalent. +/// +/// Currently is method only checks if the surface roles is `xdg_toplevel`, +/// but may be extended to other shell-protocols in the future, if applicable. +pub fn is_toplevel_equivalent(surface: &WlSurface) -> bool { + // xdg_toplevel is toplevel like, so verify if the role matches. + let role = compositor::get_role(surface); + + // When changing this, don't forget to change the check in is_valid_parent() below. + matches!(role, Some(xdg::XDG_TOPLEVEL_ROLE)) +} + +/// Returns true if the `parent` is valid to set for `child`. +/// +/// This will check that `parent` is toplevel equivalent, then make sure that it doesn't introduce +/// a parent loop. +pub fn is_valid_parent(child: &WlSurface, parent: &WlSurface) -> bool { + if !is_toplevel_equivalent(parent) { + return false; + } + + // Check that we're not making a parent loop. + let mut next_parent = Some(parent.clone()); + while let Some(parent) = next_parent.clone() { + // Did we find a cycle? + if *child == parent { + return false; + } + + compositor::with_states(&parent, |states| { + if let Some(data) = states.data_map.get::() { + // Get xdg-toplevel parent. + let role = data.lock().unwrap(); + next_parent = role.parent.clone(); + } else { + // Reached a surface we don't know how to get a parent of. + next_parent = None; + } + }); + } + + true +} diff --git a/vendor/smithay/src/wayland/shell/wlr_layer/handlers.rs b/vendor/smithay/src/wayland/shell/wlr_layer/handlers.rs new file mode 100644 index 0000000..fc9a40c --- /dev/null +++ b/vendor/smithay/src/wayland/shell/wlr_layer/handlers.rs @@ -0,0 +1,392 @@ +use std::sync::Mutex; + +use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_shell_v1::{self, ZwlrLayerShellV1}; +use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_surface_v1; +use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; +use wayland_server::protocol::wl_surface; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, Resource, Weak}; + +use crate::utils::{ + Serial, + alive_tracker::{AliveTracker, IsAlive}, +}; +use crate::wayland::shell::xdg::XdgPopupSurfaceData; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2, compositor, shell::wlr_layer::Layer}; + +use super::{ + Anchor, KeyboardInteractivity, LayerSurfaceAttributes, LayerSurfaceCachedState, LayerSurfaceData, + Margins, WlrLayerShellGlobalData, WlrLayerShellHandler, WlrLayerShellState, +}; + +use super::LAYER_SURFACE_ROLE; + +/* + * layer_shell + */ + +impl GlobalDispatch2 for WlrLayerShellGlobalData +where + D: Dispatch, + D: Dispatch, + D: WlrLayerShellHandler, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: wayland_server::New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: WlrLayerShellHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + shell: &ZwlrLayerShellV1, + request: zwlr_layer_shell_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_layer_shell_v1::Request::GetLayerSurface { + id, + surface: wl_surface, + output, + layer, + namespace, + } => { + let layer: Layer = match layer.try_into() { + Ok(layer) => layer, + Err(layer) => { + shell.post_error( + zwlr_layer_shell_v1::Error::InvalidLayer, + format!("invalid layer: {layer:?}"), + ); + return; + } + }; + + if compositor::give_role(&wl_surface, LAYER_SURFACE_ROLE).is_err() { + shell.post_error(zwlr_layer_shell_v1::Error::Role, "Surface already has a role."); + return; + } + + let id = data_init.init( + id, + WlrLayerSurfaceUserData { + shell_data: state.shell_state().clone(), + wl_surface: wl_surface.downgrade(), + alive_tracker: Default::default(), + }, + ); + + let initial = compositor::with_states(&wl_surface, |states| { + let inserted = states + .data_map + .insert_if_missing_threadsafe(|| Mutex::new(LayerSurfaceAttributes::new(id.clone()))); + + if !inserted { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + attributes.surface = id.clone(); + } + + states + .cached_state + .get::() + .pending() + .layer = layer; + + inserted + }); + + if initial { + compositor::add_pre_commit_hook::( + &wl_surface, + super::LayerSurface::pre_commit_hook, + ); + } + + let handle = super::LayerSurface { + wl_surface, + shell_surface: id, + }; + + state + .shell_state() + .known_layers + .lock() + .unwrap() + .push(handle.clone()); + + WlrLayerShellHandler::new_layer_surface(state, handle, output, layer, namespace); + } + zwlr_layer_shell_v1::Request::Destroy => { + // Handled by destructor + } + _ => {} + } + } +} + +/* + * layer_surface + */ + +/// User data for wlr layer surface +#[derive(Debug)] +pub struct WlrLayerSurfaceUserData { + shell_data: WlrLayerShellState, + // `LayerSurfaceAttributes` stored in the surface `data_map`, contains a + // `ZwlrLayerSurfaceV1`. So this reference needs to be weak to avoid a + // cycle. + wl_surface: Weak, + alive_tracker: AliveTracker, +} + +impl IsAlive for ZwlrLayerSurfaceV1 { + #[inline] + fn alive(&self) -> bool { + let data: &WlrLayerSurfaceUserData = self.data().unwrap(); + data.alive_tracker.alive() + } +} + +impl Dispatch2 for WlrLayerSurfaceUserData +where + D: WlrLayerShellHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + layer_surface: &ZwlrLayerSurfaceV1, + request: zwlr_layer_surface_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_layer_surface_v1::Request::SetSize { width, height } => { + let _ = with_surface_pending_state(layer_surface, |data| { + data.size = (width as i32, height as i32).into(); + }); + } + zwlr_layer_surface_v1::Request::SetAnchor { anchor } => { + match Anchor::try_from(anchor) { + Ok(anchor) => { + let _ = with_surface_pending_state(layer_surface, |data| { + data.anchor = Anchor::from_bits(anchor.bits()).unwrap_or_default(); + }); + } + Err((err, msg)) => { + layer_surface.post_error(err, msg); + } + }; + } + zwlr_layer_surface_v1::Request::SetExclusiveZone { zone } => { + let _ = with_surface_pending_state(layer_surface, |data| { + data.exclusive_zone = zone.into(); + }); + } + zwlr_layer_surface_v1::Request::SetMargin { + top, + right, + bottom, + left, + } => { + let _ = with_surface_pending_state(layer_surface, |data| { + data.margin = Margins { + top, + right, + bottom, + left, + }; + }); + } + zwlr_layer_surface_v1::Request::SetKeyboardInteractivity { + keyboard_interactivity, + } => { + match KeyboardInteractivity::try_from(keyboard_interactivity) { + Ok(keyboard_interactivity) => { + let _ = with_surface_pending_state(layer_surface, |data| { + data.keyboard_interactivity = keyboard_interactivity; + }); + } + Err((err, msg)) => { + layer_surface.post_error(err, msg); + } + }; + } + zwlr_layer_surface_v1::Request::SetLayer { layer } => { + match Layer::try_from(layer) { + Ok(layer) => { + let _ = with_surface_pending_state(layer_surface, |data| { + data.layer = layer; + }); + } + Err((err, msg)) => { + layer_surface.post_error(err, msg); + } + }; + } + zwlr_layer_surface_v1::Request::SetExclusiveEdge { edge } => { + match Anchor::try_from(edge) { + Ok(edge) => { + let _ = with_surface_pending_state(layer_surface, |data| { + if edge.is_empty() { + data.exclusive_edge = None; + } else if edge.bits().count_ones() == 1 { + data.exclusive_edge = Some(edge); + } else { + layer_surface.post_error( + zwlr_layer_surface_v1::Error::InvalidExclusiveEdge, + "exclusive edge cannot have multiple edges", + ); + } + }); + } + Err((err, msg)) => { + layer_surface.post_error(err, msg); + } + }; + } + zwlr_layer_surface_v1::Request::GetPopup { popup } => { + let Ok(parent_surface) = self.wl_surface.upgrade() else { + return; + }; + + let data = popup + .data::() + .unwrap(); + + compositor::with_states(&data.wl_surface, move |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .parent = Some(parent_surface); + }); + + WlrLayerShellHandler::new_popup( + state, + make_surface_handle(layer_surface).unwrap(), + crate::wayland::shell::xdg::handlers::make_popup_handle(&popup), + ); + } + zwlr_layer_surface_v1::Request::AckConfigure { serial } => { + let Ok(surface) = self.wl_surface.upgrade() else { + return; + }; + + let serial = Serial::from(serial); + + let found_configure = compositor::with_states(&surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .ack_configure(serial) + }); + + let configure = match found_configure { + Some(configure) => configure, + None => { + layer_surface.post_error( + zwlr_layer_surface_v1::Error::InvalidSurfaceState, + format!("wrong configure serial: {}", ::from(serial)), + ); + return; + } + }; + + WlrLayerShellHandler::ack_configure(state, surface, configure); + } + zwlr_layer_surface_v1::Request::Destroy => { + // Handled by destroyed handler + } + _ => {} + } + } + + fn destroyed( + &self, + state: &mut D, + _client_id: wayland_server::backend::ClientId, + layer_surface: &ZwlrLayerSurfaceV1, + ) { + self.alive_tracker.destroy_notify(); + + // remove this surface from the known ones (as well as any leftover dead surface) + let mut layers = self.shell_data.known_layers.lock().unwrap(); + if let Some(index) = layers + .iter() + .position(|layer| layer.shell_surface.id() == layer_surface.id()) + { + let layer = layers.remove(index); + drop(layers); + let surface = layer.wl_surface().clone(); + WlrLayerShellHandler::layer_destroyed(state, layer); + compositor::with_states(&surface, |states| { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + attributes.reset(); + + let mut guard = states.cached_state.get::(); + *guard.pending() = Default::default(); + *guard.current() = Default::default(); + }); + } + } +} + +fn with_surface_pending_state( + layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + f: F, +) -> Result +where + F: FnOnce(&mut LayerSurfaceCachedState) -> T, +{ + let data = layer_surface.data::().unwrap(); + let surface = data.wl_surface.upgrade()?; + Ok(compositor::with_states(&surface, |states| { + f(states.cached_state.get::().pending()) + })) +} + +pub fn make_surface_handle( + resource: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, +) -> Result { + let data = resource.data::().unwrap(); + let wl_surface = data.wl_surface.upgrade()?; + Ok(crate::wayland::shell::wlr_layer::LayerSurface { + wl_surface, + shell_surface: resource.clone(), + }) +} diff --git a/vendor/smithay/src/wayland/shell/wlr_layer/mod.rs b/vendor/smithay/src/wayland/shell/wlr_layer/mod.rs new file mode 100644 index 0000000..8e3d3ce --- /dev/null +++ b/vendor/smithay/src/wayland/shell/wlr_layer/mod.rs @@ -0,0 +1,588 @@ +//! Utilities for handling shell surfaces with the `wlr_layer_shell` protocol +//! +//! This interface should be suitable for the implementation of many desktop shell components, +//! and a broad number of other applications that interact with the desktop. +//! +//! ### Initialization +//! +//! To initialize this handler, create the [`WlrLayerShellState`], store it inside your `State` and +//! implement the [`WlrLayerShellHandler`], as shown in this example: +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use smithay::wayland::shell::wlr_layer::{WlrLayerShellState, WlrLayerShellHandler, LayerSurface, Layer}; +//! use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; +//! +//! # struct State { layer_shell_state: WlrLayerShellState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! let layer_shell_state = WlrLayerShellState::new::( +//! &display.handle(), +//! ); +//! +//! // - Put your layer_shell_state into your `State`. +//! // - And implement `LayerShellHandler +//! impl WlrLayerShellHandler for State { +//! fn shell_state(&mut self) -> &mut WlrLayerShellState { +//! &mut self.layer_shell_state +//! } +//! +//! fn new_layer_surface( +//! &mut self, +//! surface: LayerSurface, +//! output: Option, +//! layer: Layer, +//! namespace: String, +//! ) { +//! # let _ = (surface, output, layer, namespace); +//! // your implementation +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` + +use std::sync::{Arc, Mutex}; + +use tracing::{trace, trace_span}; +use wayland_protocols_wlr::layer_shell::v1::server::{ + zwlr_layer_shell_v1::ZwlrLayerShellV1, zwlr_layer_surface_v1, +}; +use wayland_server::{ + Client, DisplayHandle, GlobalDispatch, Resource as _, + backend::GlobalId, + protocol::{wl_output::WlOutput, wl_surface}, +}; + +use crate::{ + utils::{Logical, SERIAL_COUNTER, Serial, Size, alive_tracker::IsAlive}, + wayland::{ + compositor::{self, BufferAssignment, Cacheable, SurfaceAttributes}, + shell::xdg, + }, +}; + +mod handlers; +mod types; + +pub use handlers::WlrLayerSurfaceUserData; +pub use types::{Anchor, ExclusiveZone, KeyboardInteractivity, Layer, Margins}; + +/// The role of a wlr_layer_shell_surface +pub const LAYER_SURFACE_ROLE: &str = "zwlr_layer_surface_v1"; + +/// Data associated with layer surface +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::shell::wlr_layer::LayerSurfaceData; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// states.data_map.get::(); +/// }); +/// ``` +pub type LayerSurfaceData = Mutex; + +/// Attributes for layer surface +#[derive(Debug)] +pub struct LayerSurfaceAttributes { + surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + /// Holds the state if the surface has sent the initial + /// configure event to the client. It is expected that + /// during the first commit a initial + /// configure event is sent to the client + pub initial_configure_sent: bool, + /// Holds the configures the server has sent out + /// to the client waiting to be acknowledged by + /// the client. All pending configures that are older + /// than the acknowledged one will be discarded during + /// processing layer_surface.ack_configure. + pending_configures: Vec, + /// Holds the pending state as set by the server. + pub server_pending: Option, + /// Holds the last configure that has been acknowledged by the client. This state should be + /// cloned to the current during a commit. Note that this state can be newer than the last + /// acked state at the time of the last commit. + pub last_acked: Option, +} + +impl LayerSurfaceAttributes { + fn new(surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1) -> Self { + Self { + surface, + initial_configure_sent: false, + pending_configures: Vec::new(), + server_pending: None, + last_acked: None, + } + } + + fn ack_configure(&mut self, serial: Serial) -> Option { + let configure = self + .pending_configures + .iter() + .find(|configure| configure.serial == serial) + .cloned()?; + + self.last_acked = Some(configure); + + self.pending_configures.retain(|c| c.serial > serial); + Some(configure) + } + + fn reset(&mut self) { + self.initial_configure_sent = false; + self.pending_configures = Vec::new(); + self.server_pending = None; + self.last_acked = None; + } + + fn current_server_state(&self) -> LayerSurfaceState { + self.pending_configures + .last() + .map(|c| &c.state) + .or(self.last_acked.as_ref().map(|c| &c.state)) + .cloned() + .unwrap_or_default() + } + + fn has_pending_changes(&self) -> bool { + self.server_pending + .as_ref() + .map(|s| *s != self.current_server_state()) + .unwrap_or(false) + } +} + +/// State of a layer surface +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub struct LayerSurfaceState { + /// The suggested size of the surface + pub size: Option>, +} + +/// Represents the client pending state +#[derive(Debug, Default, Copy, Clone)] +pub struct LayerSurfaceCachedState { + /// The size requested by the client + pub size: Size, + /// Anchor bitflags, describing how the layers surface should be positioned and sized + pub anchor: Anchor, + /// Description of exclusive zone + pub exclusive_zone: ExclusiveZone, + /// Edge for exclusive zone + pub exclusive_edge: Option, + /// Describes distance from the anchor point of the output + pub margin: Margins, + /// Describes how keyboard events are delivered to this surface + pub keyboard_interactivity: KeyboardInteractivity, + /// The layer that the surface is rendered on + pub layer: Layer, + /// Configure last acknowledged by the client at the time of the commit. + /// + /// Reset to `None` when the surface unmaps. + pub last_acked: Option, +} + +impl Cacheable for LayerSurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +/// Shell global state +/// +/// This state allows you to retrieve a list of surfaces +/// currently known to the shell global. +#[derive(Debug, Clone)] +pub struct WlrLayerShellState { + known_layers: Arc>>, + shell_global: GlobalId, +} + +/// Data associated with a layer shell global +#[allow(missing_debug_implementations)] +pub struct WlrLayerShellGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +impl WlrLayerShellState { + /// Create a new `wlr_layer_shell` global + pub fn new(display: &DisplayHandle) -> WlrLayerShellState + where + D: GlobalDispatch, + D: 'static, + { + Self::new_with_filter::(display, |_| true) + } + + /// Create a new `wlr_layer_shell` global with a client filter + pub fn new_with_filter(display: &DisplayHandle, filter: F) -> WlrLayerShellState + where + D: GlobalDispatch, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let shell_global = display.create_global::( + 5, + WlrLayerShellGlobalData { + filter: Box::new(filter), + }, + ); + + WlrLayerShellState { + known_layers: Default::default(), + shell_global, + } + } + + /// Get shell global id + pub fn shell_global(&self) -> GlobalId { + self.shell_global.clone() + } + + /// Access all the shell surfaces known by this handler + pub fn layer_surfaces(&self) -> impl DoubleEndedIterator + use<> { + self.known_layers.lock().unwrap().clone().into_iter() + } +} + +/// Handler for wlr layer shell +#[allow(unused_variables)] +pub trait WlrLayerShellHandler { + /// [WlrLayerShellState] getter + fn shell_state(&mut self) -> &mut WlrLayerShellState; + + /// A new layer surface was created + /// + /// You likely need to send a [`LayerSurfaceConfigure`] to the surface, to hint the + /// client as to how its layer surface should be sized. + fn new_layer_surface( + &mut self, + surface: LayerSurface, + output: Option, + layer: Layer, + namespace: String, + ); + + /// A new popup was assigned a layer surface as it's parent + fn new_popup(&mut self, parent: LayerSurface, popup: xdg::PopupSurface) {} + + /// A surface has acknowledged a configure serial. + fn ack_configure(&mut self, surface: wl_surface::WlSurface, configure: LayerSurfaceConfigure) {} + + /// A layer surface was destroyed. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn layer_destroyed(&mut self, surface: LayerSurface) {} +} + +/// A handle to a layer surface +#[derive(Debug, Clone)] +pub struct LayerSurface { + wl_surface: wl_surface::WlSurface, + shell_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, +} + +impl std::cmp::PartialEq for LayerSurface { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.wl_surface == other.wl_surface + } +} + +impl LayerSurface { + /// Checks if the surface is still alive + #[inline] + pub fn alive(&self) -> bool { + self.wl_surface.alive() && self.shell_surface.alive() + } + + /// Gets the current pending state for a configure + /// + /// Returns `Some` if either no initial configure has been sent or + /// the `server_pending` is `Some` and different from the last pending + /// configure or `last_acked` if there is no pending + /// + /// Returns `None` if either no `server_pending` or the pending + /// has already been sent to the client or the pending is equal + /// to the `last_acked` + fn get_pending_state(&self, attributes: &mut LayerSurfaceAttributes) -> Option { + if !attributes.initial_configure_sent { + return Some( + attributes + .server_pending + .take() + .unwrap_or_else(|| attributes.current_server_state()), + ); + } + + // Check if the state really changed, it is possible + // that with_pending_state has been called without + // modifying the state. + if !attributes.has_pending_changes() { + return None; + } + + attributes.server_pending.take() + } + + /// Send a pending configure event to this layer surface to suggest it a new configuration + /// + /// If changes have occurred a configure event will be send to the clients and the serial will be returned + /// (for tracking the configure in [`WlrLayerShellHandler::ack_configure`] if desired). + /// If no changes occurred no event will be send and `None` will be returned. + /// + /// See [`send_configure`](LayerSurface::send_configure) and [`has_pending_changes`](LayerSurface::has_pending_changes) + /// for more information. + pub fn send_pending_configure(&self) -> Option { + if self.has_pending_changes() { + Some(self.send_configure()) + } else { + None + } + } + + /// Send a configure event to this layer surface to suggest it a new configuration + /// + /// The serial of this configure will be tracked waiting for the client to ACK it. + /// + /// You can manipulate the state that will be sent to the client with the [`with_pending_state`](#method.with_pending_state) + /// method. + /// + /// Note: This will always send a configure event, if you intend to only send a configure event on changes take a look at + /// [`send_pending_configure`](LayerSurface::send_pending_configure) + pub fn send_configure(&self) -> Serial { + let configure = compositor::with_states(&self.wl_surface, |states| { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + + let state = self + .get_pending_state(&mut attributes) + .unwrap_or_else(|| attributes.current_server_state()); + + let configure = LayerSurfaceConfigure { + serial: SERIAL_COUNTER.next_serial(), + state, + }; + + attributes.pending_configures.push(configure); + attributes.initial_configure_sent = true; + + configure + }); + + // send surface configure + let (width, height) = configure.state.size.unwrap_or_default().into(); + let serial = configure.serial; + self.shell_surface + .configure(serial.into(), width as u32, height as u32); + serial + } + + /// Send a "close" event to the client + pub fn send_close(&self) { + self.shell_surface.closed() + } + + /// Access the underlying `wl_surface` of this layer surface + /// + /// Returns `None` if the layer surface actually no longer exists. + #[inline] + pub fn wl_surface(&self) -> &wl_surface::WlSurface { + &self.wl_surface + } + + /// Allows the pending state of this layer to + /// be manipulated. + /// + /// This should be used to inform the client about size and state changes, + /// for example after a resize request from the client. + /// + /// The state will be sent to the client when calling [`send_configure`](#method.send_configure). + pub fn with_pending_state(&self, f: F) -> T + where + F: FnOnce(&mut LayerSurfaceState) -> T, + { + compositor::with_states(&self.wl_surface, |states| { + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + if attributes.server_pending.is_none() { + attributes.server_pending = Some(attributes.current_server_state()); + } + + let server_pending = attributes.server_pending.as_mut().unwrap(); + f(server_pending) + }) + } + + /// Tests this [`LayerSurface`] for pending changes + /// + /// Returns `true` if [`with_pending_state`](LayerSurface::with_pending_state) was used to manipulate the state + /// and resulted in a different state or if the initial configure is still pending. + pub fn has_pending_changes(&self) -> bool { + compositor::with_states(&self.wl_surface, |states| { + let attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + + !attributes.initial_configure_sent || attributes.has_pending_changes() + }) + } + + /// Provides access to the current committed cached state. + pub fn with_cached_state(&self, f: F) -> T + where + F: FnOnce(&LayerSurfaceCachedState) -> T, + { + compositor::with_states(&self.wl_surface, |states| { + let mut guard = states.cached_state.get::(); + f(guard.current()) + }) + } + + /// Provides access to the current committed state. + /// + /// This is the state that the client last acked before making the current commit. + pub fn with_committed_state(&self, f: F) -> T + where + F: FnOnce(Option<&LayerSurfaceState>) -> T, + { + self.with_cached_state(move |state| f(state.last_acked.as_ref().map(|c| &c.state))) + } + + /// Handles the role specific commit error checking + /// + /// This should be called when the underlying WlSurface + /// handles a wl_surface.commit request. + pub(crate) fn pre_commit_hook( + _state: &mut D, + _dh: &DisplayHandle, + surface: &wl_surface::WlSurface, + ) { + let _span = trace_span!("layer-surface pre-commit", surface = %surface.id()).entered(); + + compositor::with_states(surface, |states| { + let mut role = states.data_map.get::().unwrap().lock().unwrap(); + + if !role.surface.is_alive() { + return; + } + + let mut guard_layer = states.cached_state.get::(); + let pending = guard_layer.pending(); + + if pending.size.w == 0 && !pending.anchor.anchored_horizontally() { + role.surface.post_error( + zwlr_layer_surface_v1::Error::InvalidSize, + "width 0 requested without setting left and right anchors", + ); + return; + } + + if pending.size.h == 0 && !pending.anchor.anchored_vertically() { + role.surface.post_error( + zwlr_layer_surface_v1::Error::InvalidSize, + "height 0 requested without setting top and bottom anchors", + ); + return; + } + + if let Some(edge) = pending.exclusive_edge { + if !pending.anchor.contains(edge) { + role.surface.post_error( + zwlr_layer_surface_v1::Error::InvalidExclusiveEdge, + "exclusive edge is not an anchor", + ); + return; + } + } + + // The presence of last_acked always follows the buffer assignment because the + // surface is not allowed to attach a buffer without acking the initial configure. + let had_buffer_before = pending.last_acked.is_some(); + + let mut guard_surface = states.cached_state.get::(); + let has_buffer = match &guard_surface.pending().buffer { + Some(BufferAssignment::NewBuffer(_)) => true, + Some(BufferAssignment::Removed) => false, + None => had_buffer_before, + }; + // Need to check had_buffer_before in case the client attaches a null buffer for the + // initial commit---we don't want to consider that as "got unmapped" and reset role. + // Reproducer: waybar. + let got_unmapped = had_buffer_before && !has_buffer; + + if has_buffer { + let Some(last_acked) = role.last_acked else { + role.surface.post_error( + zwlr_layer_surface_v1::Error::InvalidSurfaceState, + "must ack the initial configure before attaching buffer", + ); + return; + }; + + // The surface remains, or became mapped, track the last acked state. + pending.last_acked = Some(last_acked); + } else { + // The surface remains, or became, unmapped, meaning that it's in the initial + // configure stage. + pending.last_acked = None; + } + + if got_unmapped { + trace!( + "got unmapped; resetting role and cached state; have {} pending configures", + role.pending_configures.len() + ); + + // All attributes are discarded when an xdg_surface is unmapped. Though, we keep + // the list of pending configures because there's no way for a surface to tell + // an in-flight configure apart from our next initial configure after unmapping. + let pending_configures = std::mem::take(&mut role.pending_configures); + role.reset(); + role.pending_configures = pending_configures; + *guard_layer.pending() = Default::default(); + } + }); + } + + /// Access the underlying `zwlr_layer_surface_v1` of this layer surface + /// + pub fn shell_surface(&self) -> &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1 { + &self.shell_surface + } +} + +/// A configure message for layer surfaces +#[derive(Debug, Copy, Clone)] +pub struct LayerSurfaceConfigure { + /// The state associated with this configure + pub state: LayerSurfaceState, + + /// A serial number to track ACK from the client + /// + /// This should be an ever increasing number, as the ACK-ing + /// from a client for a serial will validate all pending lower + /// serials. + pub serial: Serial, +} diff --git a/vendor/smithay/src/wayland/shell/wlr_layer/types.rs b/vendor/smithay/src/wayland/shell/wlr_layer/types.rs new file mode 100644 index 0000000..6847012 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/wlr_layer/types.rs @@ -0,0 +1,249 @@ +use std::cmp::Ordering; + +use wayland_protocols_wlr::layer_shell::v1::server::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; +use wayland_server::WEnum; + +/// Available layers for surfaces +/// +/// These values indicate which layers a surface can be rendered in. +/// They are ordered by z depth, bottom-most first. +/// Traditional shell surfaces will typically be rendered between the bottom and top layers. +/// Fullscreen shell surfaces are typically rendered at the top layer. +/// Multiple surfaces can share a single layer, and ordering within a single layer is undefined. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layer { + /// The lowest layer, used usually for wallpapers + Background, + /// The layer bellow the windows and above the wallpaper + Bottom, + /// The layer above the windows and bellow overlay + Top, + /// The top layer above all other layers + Overlay, +} + +impl TryFrom> for Layer { + type Error = (zwlr_layer_shell_v1::Error, String); + + #[inline] + fn try_from(layer: WEnum) -> Result { + use zwlr_layer_shell_v1::Layer; + + match layer { + WEnum::Value(Layer::Background) => Ok(Self::Background), + WEnum::Value(Layer::Bottom) => Ok(Self::Bottom), + WEnum::Value(Layer::Top) => Ok(Self::Top), + WEnum::Value(Layer::Overlay) => Ok(Self::Overlay), + layer => Err(( + zwlr_layer_shell_v1::Error::InvalidLayer, + format!("invalid layer: {layer:?}"), + )), + } + } +} + +impl Default for Layer { + #[inline] + fn default() -> Self { + Self::Background + } +} + +/// Types of keyboard interaction possible for a layer shell surface +/// +/// The rationale for this is twofold: +/// - some applications are not interested in keyboard events +/// and not allowing them to be focused can improve the desktop experience +/// - some applications will want to take exclusive keyboard focus. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyboardInteractivity { + /// This value indicates that this surface is not interested in keyboard events + /// and the compositor should never assign it the keyboard focus. + /// + /// This is the default value, set for newly created layer shell surfaces. + /// + /// This is useful for e.g. desktop widgets that display information + /// or only have interaction with non-keyboard input devices. + None, + /// Request exclusive keyboard focus if this surface is above the shell surface layer. + /// + /// For the top and overlay layers, the seat will always give exclusive keyboard focus + /// to the top-most layer which has keyboard interactivity set to exclusive. + /// If this layer contains multiple surfaces with keyboard interactivity set to exclusive, + /// the compositor determines the one receiving keyboard events in an implementation- defined manner. + /// In this case, no guarantee is made when this surface will receive keyboard focus (if ever). + /// + /// For the bottom and background layers, the compositor is allowed to use normal focus semantics. + /// + /// This setting is mainly intended for applications that need to + /// ensure they receive all keyboard events, such as a lock screen or a password prompt. + Exclusive, + /// This requests the compositor to allow this surface + /// to be focused and unfocused by the user in an implementation-defined manner. + /// The user should be able to unfocus this surface even regardless of the layer it is on. + /// + /// Typically, the compositor will want to use its normal mechanism + /// to manage keyboard focus between layer shell surfaces + /// with this setting and regular toplevels on the desktop layer (e.g. click to focus). + /// Nevertheless, it is possible for a compositor to require a special interaction + /// to focus or unfocus layer shell surfaces + /// + /// This setting is mainly intended for desktop shell components that allow keyboard interaction. + /// Using this option can allow implementing a desktop shell that can be fully usable without the mouse. + OnDemand, +} + +impl Default for KeyboardInteractivity { + #[inline] + fn default() -> Self { + Self::None + } +} + +impl TryFrom> for KeyboardInteractivity { + type Error = (zwlr_layer_surface_v1::Error, String); + + #[inline] + fn try_from(ki: WEnum) -> Result { + use zwlr_layer_surface_v1::KeyboardInteractivity; + + match ki { + WEnum::Value(KeyboardInteractivity::None) => Ok(Self::None), + WEnum::Value(KeyboardInteractivity::Exclusive) => Ok(Self::Exclusive), + WEnum::Value(KeyboardInteractivity::OnDemand) => Ok(Self::OnDemand), + ki => Err(( + zwlr_layer_surface_v1::Error::InvalidKeyboardInteractivity, + format!("wrong keyboard interactivity value: {ki:?}"), + )), + } + } +} + +bitflags::bitflags! { + /// Anchor bitflags, describing how the layers surface should be positioned and sized + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct Anchor: u32 { + /// The top edge of the anchor rectangle + const TOP = 1; + /// The bottom edge of the anchor rectangle + const BOTTOM = 2; + /// The left edge of the anchor rectangle + const LEFT = 4; + /// The right edge of the anchor rectangle + const RIGHT = 8; + } +} + +impl Anchor { + /// Check if anchored horizontally + /// + /// If it is anchored to `left` and `right` anchor at the same time + /// it returns `true` + pub fn anchored_horizontally(&self) -> bool { + self.contains(Self::LEFT) && self.contains(Self::RIGHT) + } + /// Check if anchored vertically + /// + /// If it is anchored to `top` and `bottom` anchor at the same time + /// it returns `true` + pub fn anchored_vertically(&self) -> bool { + self.contains(Self::TOP) && self.contains(Self::BOTTOM) + } +} + +impl Default for Anchor { + #[inline] + fn default() -> Self { + Self::empty() + } +} + +impl TryFrom> for Anchor { + type Error = (zwlr_layer_surface_v1::Error, String); + + #[inline] + fn try_from(anchor: WEnum) -> Result { + let a = if let WEnum::Value(anchor) = anchor { + Anchor::from_bits(anchor.bits()) + } else { + None + }; + + a.ok_or(( + zwlr_layer_surface_v1::Error::InvalidAnchor, + format!("invalid anchor {anchor:?}"), + )) + } +} + +/// Exclusive zone descriptor +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExclusiveZone { + /// Requests that the compositor avoids occluding an area with other surfaces. + /// + /// A exclusive zone value is the distance from the edge in surface-local coordinates to consider exclusive. + /// + /// A exclusive value is only meaningful if the surface is + /// anchored to one edge or an edge and both perpendicular edges. + /// + /// If the surface is: + /// - not anchored + /// - anchored to only two perpendicular edges (a corner), + /// - anchored to only two parallel edges or anchored to all edges, + /// + /// The exclusive value should be treated the same as [`ExclusiveZone::Neutral`]. + Exclusive(u32), + /// If set to Neutral, + /// the surface indicates that it would like to be moved to avoid occluding surfaces with a exclusive zone. + Neutral, + /// If set to DontCare, + /// the surface indicates that it would not like to be moved to accommodate for other surfaces, + /// and the compositor should extend it all the way to the edges it is anchored to. + DontCare, +} + +impl Default for ExclusiveZone { + #[inline] + fn default() -> Self { + Self::Neutral + } +} + +impl From for ExclusiveZone { + #[inline] + fn from(v: i32) -> Self { + match v.cmp(&0) { + Ordering::Greater => Self::Exclusive(v as u32), + Ordering::Equal => Self::Neutral, + Ordering::Less => Self::DontCare, + } + } +} + +impl From for i32 { + #[inline] + fn from(z: ExclusiveZone) -> i32 { + match z { + ExclusiveZone::Exclusive(v) => v as i32, + ExclusiveZone::Neutral => 0, + ExclusiveZone::DontCare => -1, + } + } +} + +/// Describes distance from the anchor point of the output, in surface-local coordinates. +/// +/// If surface did not anchor curtain edge, margin for that edge should be ignored. +/// +/// The exclusive zone should includes the margins. +#[derive(Debug, Default, Clone, Copy)] +pub struct Margins { + /// Distance from [`Anchor::TOP`] + pub top: i32, + /// Distance from [`Anchor::TOP`] + pub right: i32, + /// Distance from [`Anchor::BOTTOM`] + pub bottom: i32, + /// Distance from [`Anchor::LEFT`] + pub left: i32, +} diff --git a/vendor/smithay/src/wayland/shell/xdg/decoration.rs b/vendor/smithay/src/wayland/shell/xdg/decoration.rs new file mode 100644 index 0000000..bbe55ac --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/decoration.rs @@ -0,0 +1,303 @@ +//! XDG Window decoration manager +//! +//! This interface allows a compositor to announce support for server-side decorations. +//! +//! A client can use this protocol to request being decorated by a supporting compositor. +//! +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use smithay::wayland::shell::xdg::{ToplevelSurface, XdgShellHandler}; +//! # use smithay::utils::Serial; +//! # use smithay::wayland::shell::xdg::{XdgShellState, PopupSurface, PositionerState}; +//! # use smithay::reexports::wayland_server::protocol::{wl_seat, wl_surface}; +//! use smithay::wayland::shell::xdg::decoration::{XdgDecorationState, XdgDecorationHandler}; +//! use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode; +//! +//! # struct State { decoration_state: XdgDecorationState, seat_state: SeatState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! // Create a decoration state +//! let decoration_state = XdgDecorationState::new::( +//! &display.handle(), +//! ); +//! +//! // store that state inside your compositor state +//! // ... +//! +//! // implement the necessary traits +//! impl XdgShellHandler for State { +//! # fn xdg_shell_state(&mut self) -> &mut XdgShellState { unimplemented!() } +//! # fn new_toplevel(&mut self, surface: ToplevelSurface) { unimplemented!() } +//! # fn new_popup( +//! # &mut self, +//! # surface: PopupSurface, +//! # positioner: PositionerState, +//! # ) { unimplemented!() } +//! # fn grab( +//! # &mut self, +//! # surface: PopupSurface, +//! # seat: wl_seat::WlSeat, +//! # serial: Serial, +//! # ) { unimplemented!() } +//! # fn reposition_request( +//! # &mut self, +//! # surface: PopupSurface, +//! # positioner: PositionerState, +//! # token: u32, +//! # ) { unimplemented!() } +//! // ... +//! } +//! impl XdgDecorationHandler for State { +//! fn new_decoration(&mut self, toplevel: ToplevelSurface) { +//! toplevel.with_pending_state(|state| { +//! // Advertise server side decoration +//! state.decoration_mode = Some(Mode::ServerSide); +//! }); +//! toplevel.send_configure(); +//! } +//! fn request_mode(&mut self, toplevel: ToplevelSurface, mode: Mode) { /* ... */ } +//! fn unset_mode(&mut self, toplevel: ToplevelSurface) { /* ... */ } +//! } +//! +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! type Target = wl_surface::WlSurface; +//! impl SeatHandler for State { +//! type KeyboardFocus = Target; +//! type PointerFocus = Target; +//! type TouchFocus = Target; +//! +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) { +//! // handle focus changes, if you need to ... +//! } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { +//! // handle new images for the cursor ... +//! } +//! } +//! # impl PointerConstraintsHandler for State {} +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You are ready to go! +// TODO: Describe how to change decoration mode. + +use wayland_protocols::xdg::decoration::zv1::server::{ + zxdg_decoration_manager_v1, + zxdg_toplevel_decoration_v1::{self, Mode}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, backend::GlobalId, +}; + +use super::{ToplevelSurface, XdgShellHandler}; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2, shell::xdg::XdgShellSurfaceUserData}; + +/// Delegate type for handling xdg decoration events. +#[derive(Debug)] +pub struct XdgDecorationState { + global: GlobalId, +} + +/// Data associated with a XdgDecorationManager global. +#[allow(missing_debug_implementations)] +pub struct XdgDecorationManagerGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +impl XdgDecorationState { + /// Creates a new delegate type for handling xdg decoration events. + /// + /// A global id is also returned to allow destroying the global in the future. + pub fn new(display: &DisplayHandle) -> XdgDecorationState + where + D: GlobalDispatch< + zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, + XdgDecorationManagerGlobalData, + > + Dispatch + + 'static, + { + Self::new_with_filter::(display, |_| true) + } + + /// Creates a new delegate type for handling xdg decoration events with a filter. + /// + /// Filters can be used to limit visibility of a global to certain clients. + /// + /// A global id is also returned to allow destroying the global in the future. + pub fn new_with_filter(display: &DisplayHandle, filter: F) -> XdgDecorationState + where + D: GlobalDispatch< + zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, + XdgDecorationManagerGlobalData, + > + Dispatch + + 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = XdgDecorationManagerGlobalData { + filter: Box::new(filter), + }; + let global = + display.create_global::(1, data); + + XdgDecorationState { global } + } + + /// Returns the xdg-decoration global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +/// Handler trait for xdg decoration events. +pub trait XdgDecorationHandler { + /// Notification the client supports server side decoration on the toplevel. + fn new_decoration(&mut self, toplevel: ToplevelSurface); + + /// Notification the client prefers the provided decoration decoration mode on the toplevel. + fn request_mode(&mut self, toplevel: ToplevelSurface, mode: Mode); + + /// Notification the client does not prefer a particular decoration mode on the toplevel. + fn unset_mode(&mut self, toplevel: ToplevelSurface); +} + +pub(super) fn send_decoration_configure( + id: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + mode: Mode, +) { + id.configure(mode) +} + +impl GlobalDispatch2 + for XdgDecorationManagerGlobalData +where + D: Dispatch + + Dispatch + + XdgShellHandler + + XdgDecorationHandler + + 'static, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch + + XdgShellHandler + + XdgDecorationHandler + + 'static, +{ + fn request( + &self, + state: &mut D, + _: &Client, + resource: &zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, + request: zxdg_decoration_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + use self::zxdg_decoration_manager_v1::Request; + + match request { + Request::GetToplevelDecoration { id, toplevel } => { + let data = toplevel.data::().unwrap(); + + if !data.alive_tracker.alive() { + resource.post_error( + zxdg_toplevel_decoration_v1::Error::Orphaned, + "toplevel is already destroyed", + ); + return; + } + + let mut decoration_guard = data.decoration.lock().unwrap(); + + if decoration_guard.is_some() { + resource.post_error( + zxdg_toplevel_decoration_v1::Error::AlreadyConstructed, + "toplevel decoration is already constructed", + ); + return; + } + + let toplevel = state.xdg_shell_state().get_toplevel(&toplevel).unwrap(); + let toplevel_decoration = data_init.init(id, toplevel.clone()); + + *decoration_guard = Some(toplevel_decoration); + drop(decoration_guard); + + state.new_decoration(toplevel); + } + + Request::Destroy => {} + + _ => unreachable!(), + } + } +} + +// zxdg_toplevel_decoration_v1 + +impl Dispatch2 for ToplevelSurface +where + D: XdgDecorationHandler, +{ + fn request( + &self, + state: &mut D, + _: &Client, + resource: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, + request: zxdg_toplevel_decoration_v1::Request, + _dh: &DisplayHandle, + _: &mut DataInit<'_, D>, + ) { + use self::zxdg_toplevel_decoration_v1::{Error, Request}; + + match request { + Request::SetMode { mode } => { + if let WEnum::Value(mode) = mode { + state.request_mode(self.clone(), mode); + } else { + resource.post_error(Error::InvalidMode, "invalid decoration mode"); + } + } + + Request::UnsetMode => { + state.unset_mode(self.clone()); + } + + Request::Destroy => { + if let Some(data) = self.xdg_toplevel().data::() { + data.decoration.lock().unwrap().take(); + } + } + + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/shell/xdg/dialog.rs b/vendor/smithay/src/wayland/shell/xdg/dialog.rs new file mode 100644 index 0000000..a845ac5 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/dialog.rs @@ -0,0 +1,272 @@ +//! XDG Dialog Windows +//! +//! This interface allows a compositor to announce support for xdg-dialog toplevel hints, eg. modal hint. +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use smithay::wayland::shell::xdg::{ToplevelSurface, XdgShellHandler}; +//! # use smithay::utils::Serial; +//! # use smithay::wayland::shell::xdg::{XdgShellState, PopupSurface, PositionerState}; +//! # use smithay::reexports::wayland_server::protocol::{wl_seat, wl_surface}; +//! use smithay::wayland::shell::xdg::dialog::{ToplevelDialogHint, XdgDialogState, XdgDialogHandler}; +//! +//! # struct State { dialog_state: XdgDialogState, seat_state: SeatState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! // Create a dialog state +//! let dialog_state = XdgDialogState::new::( +//! &display.handle(), +//! ); +//! +//! // store that state inside your compositor state +//! // ... +//! +//! // implement the necessary traits +//! impl XdgShellHandler for State { +//! # fn xdg_shell_state(&mut self) -> &mut XdgShellState { unimplemented!() } +//! # fn new_toplevel(&mut self, surface: ToplevelSurface) { unimplemented!() } +//! # fn new_popup( +//! # &mut self, +//! # surface: PopupSurface, +//! # positioner: PositionerState, +//! # ) { unimplemented!() } +//! # fn grab( +//! # &mut self, +//! # surface: PopupSurface, +//! # seat: wl_seat::WlSeat, +//! # serial: Serial, +//! # ) { unimplemented!() } +//! # fn reposition_request( +//! # &mut self, +//! # surface: PopupSurface, +//! # positioner: PositionerState, +//! # token: u32, +//! # ) { unimplemented!() } +//! // ... +//! } +//! impl XdgDialogHandler for State { +//! fn dialog_hint_changed(&mut self, toplevel: ToplevelSurface, hint: ToplevelDialogHint) { /* ... */ } +//! } +//! +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! type Target = wl_surface::WlSurface; +//! impl SeatHandler for State { +//! type KeyboardFocus = Target; +//! type PointerFocus = Target; +//! type TouchFocus = Target; +//! +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) { +//! // handle focus changes, if you need to ... +//! } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { +//! // handle new images for the cursor ... +//! } +//! } +//! # impl PointerConstraintsHandler for State {} +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You are ready to go! + +use wayland_protocols::xdg::dialog::v1::server::{ + xdg_dialog_v1::{self, XdgDialogV1}, + xdg_wm_dialog_v1::{self, XdgWmDialogV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::GlobalId, + protocol::wl_surface::WlSurface, +}; + +use super::{ToplevelSurface, XdgShellHandler}; +use crate::wayland::{ + Dispatch2, GlobalData, GlobalDispatch2, compositor, + shell::xdg::{XdgShellSurfaceUserData, XdgToplevelSurfaceData}, +}; + +/// Delegate type for handling xdg dialog events. +#[derive(Debug)] +pub struct XdgDialogState { + global: GlobalId, +} + +impl XdgDialogState { + /// Creates a new delegate type for handling xdg dialog events. + /// + /// A global id is also returned to allow destroying the global in the future. + pub fn new(display: &DisplayHandle) -> XdgDialogState + where + D: GlobalDispatch + Dispatch + 'static, + { + let global = display.create_global::(1, GlobalData); + XdgDialogState { global } + } + + /// Returns the xdg-wm-dialog global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +/// Handler trait for xdg dialog events. +pub trait XdgDialogHandler: XdgShellHandler { + /// The client has changed the dialog hint associated with the toplevel + fn dialog_hint_changed(&mut self, toplevel: ToplevelSurface, hint: ToplevelDialogHint) { + let _ = toplevel; + let _ = hint; + } +} + +// xdg_wm_dialog_v1 + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, +{ + fn request( + &self, + state: &mut D, + _: &Client, + resource: &XdgWmDialogV1, + request: xdg_wm_dialog_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + use xdg_wm_dialog_v1::Request; + + match request { + Request::GetXdgDialog { id, toplevel } => { + let data = toplevel.data::().unwrap(); + + let mut dialog_guard = data.dialog.lock().unwrap(); + + if dialog_guard.is_some() { + resource.post_error( + xdg_wm_dialog_v1::Error::AlreadyUsed, + "toplevel dialog is already constructed", + ); + return; + } + + let toplevel = state.xdg_shell_state().get_toplevel(&toplevel).unwrap(); + let toplevel_dialog = data_init.init(id, toplevel.clone()); + + *dialog_guard = Some(toplevel_dialog); + drop(dialog_guard); + + if set_dialog_hint(toplevel.wl_surface(), ToplevelDialogHint::Dialog) { + state.dialog_hint_changed(toplevel, ToplevelDialogHint::Dialog); + } + } + + Request::Destroy => {} + + _ => unreachable!(), + } + } +} + +// xdg_dialog_v1 + +impl Dispatch2 for ToplevelSurface { + fn request( + &self, + state: &mut D, + _: &Client, + _: &XdgDialogV1, + request: xdg_dialog_v1::Request, + _dh: &DisplayHandle, + _: &mut DataInit<'_, D>, + ) { + use xdg_dialog_v1::Request; + + match request { + Request::SetModal => { + if set_dialog_hint(self.wl_surface(), ToplevelDialogHint::Modal) { + state.dialog_hint_changed(self.clone(), ToplevelDialogHint::Modal); + } + } + + Request::UnsetModal => { + if set_dialog_hint(self.wl_surface(), ToplevelDialogHint::Dialog) { + state.dialog_hint_changed(self.clone(), ToplevelDialogHint::Dialog); + } + } + + Request::Destroy => { + if set_dialog_hint(self.wl_surface(), ToplevelDialogHint::Unknown) { + state.dialog_hint_changed(self.clone(), ToplevelDialogHint::Unknown); + } + + if let Some(data) = self.xdg_toplevel().data::() { + data.dialog.lock().unwrap().take(); + } + } + + _ => unreachable!(), + } + } +} + +/// Dialog hint assigned to toplevels by the xdg-dialog-v1 protocol +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub enum ToplevelDialogHint { + /// The toplevel is not associated with an xdg_dialog_v1 object. + #[default] + Unknown, + /// The toplevel is a "dialog" (e.g. a temporary window) relative + /// to its parent (see xdg_toplevel.set_parent). + Dialog, + /// The toplevel is a dialog with "modal" behaviour. Modal dialogs + /// typically require to be fully addressed by the user (i.e. closed) + /// before resuming interaction with the parent toplevel, + /// and may require a distinct presentation. + Modal, +} + +/// Returns true if changed +fn set_dialog_hint(wl_surface: &WlSurface, hint: ToplevelDialogHint) -> bool { + compositor::with_states(wl_surface, |states| { + let role = &mut states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + if role.dialog_hint != hint { + role.dialog_hint = hint; + true + } else { + false + } + }) +} diff --git a/vendor/smithay/src/wayland/shell/xdg/handlers.rs b/vendor/smithay/src/wayland/shell/xdg/handlers.rs new file mode 100644 index 0000000..155a85c --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/handlers.rs @@ -0,0 +1,15 @@ +use super::{ + PopupConfigure, PositionerState, ShellClient, ShellClientData, SurfaceCachedState, ToplevelConfigure, + XdgPopupSurfaceRoleAttributes, XdgShellHandler, XdgToplevelSurfaceRoleAttributes, +}; + +mod wm_base; +pub use wm_base::XdgWmBaseUserData; + +mod positioner; +pub use positioner::XdgPositionerUserData; + +mod surface; +pub(in crate::wayland::shell) use surface::make_popup_handle; +pub use surface::{XdgShellSurfaceUserData, XdgSurfaceUserData}; +pub(super) use surface::{get_parent, send_popup_configure, send_toplevel_configure}; diff --git a/vendor/smithay/src/wayland/shell/xdg/handlers/positioner.rs b/vendor/smithay/src/wayland/shell/xdg/handlers/positioner.rs new file mode 100644 index 0000000..b8cfa10 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/handlers/positioner.rs @@ -0,0 +1,95 @@ +use std::sync::Mutex; + +use crate::{utils::Rectangle, utils::Serial, wayland::Dispatch2}; + +use wayland_protocols::xdg::shell::server::{xdg_positioner, xdg_positioner::XdgPositioner}; + +use wayland_server::{DataInit, DisplayHandle, Resource, WEnum}; + +use super::{PositionerState, XdgShellHandler}; + +/* + * xdg_positioner + */ + +/// User data for Xdg Positioner +#[derive(Default, Debug)] +pub struct XdgPositionerUserData { + pub(crate) inner: Mutex, +} + +impl Dispatch2 for XdgPositionerUserData +where + D: XdgShellHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + positioner: &XdgPositioner, + request: xdg_positioner::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let mut state = self.inner.lock().unwrap(); + match request { + xdg_positioner::Request::SetSize { width, height } => { + if width < 1 || height < 1 { + positioner.post_error( + xdg_positioner::Error::InvalidInput, + "Invalid size for positioner.", + ); + } else { + state.rect_size = (width, height).into(); + } + } + xdg_positioner::Request::SetAnchorRect { x, y, width, height } => { + if width < 1 || height < 1 { + positioner.post_error( + xdg_positioner::Error::InvalidInput, + "Invalid size for positioner's anchor rectangle.", + ); + } else { + state.anchor_rect = Rectangle::new((x, y).into(), (width, height).into()); + } + } + xdg_positioner::Request::SetAnchor { anchor } => { + if let WEnum::Value(anchor) = anchor { + state.anchor_edges = anchor; + } + } + xdg_positioner::Request::SetGravity { gravity } => { + if let WEnum::Value(gravity) = gravity { + state.gravity = gravity; + } + } + xdg_positioner::Request::SetConstraintAdjustment { + constraint_adjustment, + } => { + if let WEnum::Value(constraint_adjustment) = constraint_adjustment { + state.constraint_adjustment = constraint_adjustment; + } + } + xdg_positioner::Request::SetOffset { x, y } => { + state.offset = (x, y).into(); + } + xdg_positioner::Request::SetReactive => { + state.reactive = true; + } + xdg_positioner::Request::SetParentSize { + parent_width, + parent_height, + } => { + state.parent_size = Some((parent_width, parent_height).into()); + } + xdg_positioner::Request::SetParentConfigure { serial } => { + state.parent_configure = Some(Serial::from(serial)); + } + xdg_positioner::Request::Destroy => { + // handled by destructor + } + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/shell/xdg/handlers/surface.rs b/vendor/smithay/src/wayland/shell/xdg/handlers/surface.rs new file mode 100644 index 0000000..f046222 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/handlers/surface.rs @@ -0,0 +1,374 @@ +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; + +use indexmap::IndexSet; + +use crate::utils::alive_tracker::{AliveTracker, IsAlive}; +use crate::wayland::shell::xdg::{XdgPopupSurfaceData, XdgToplevelSurfaceData}; +use crate::{ + utils::{Rectangle, Serial}, + wayland::{ + Dispatch2, compositor, + shell::xdg::{PopupState, XDG_POPUP_ROLE, XDG_TOPLEVEL_ROLE}, + }, +}; + +use wayland_protocols::{ + xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1, + xdg::dialog::v1::server::xdg_dialog_v1, + xdg::shell::server::{ + xdg_popup::XdgPopup, xdg_surface, xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel, xdg_wm_base, + }, +}; + +use wayland_server::{DataInit, Dispatch, DisplayHandle, Resource, Weak, protocol::wl_surface}; + +use super::{ + PopupConfigure, SurfaceCachedState, ToplevelConfigure, XdgPopupSurfaceRoleAttributes, + XdgPositionerUserData, XdgShellHandler, XdgToplevelSurfaceRoleAttributes, +}; + +mod toplevel; +use toplevel::make_toplevel_handle; +pub use toplevel::{get_parent, send_toplevel_configure}; + +mod popup; +pub use popup::{make_popup_handle, send_popup_configure}; + +/// User data of XdgSurface +#[derive(Debug)] +pub struct XdgSurfaceUserData { + pub(crate) known_surfaces: Arc>>>, + pub(crate) wl_surface: wl_surface::WlSurface, + pub(crate) wm_base: xdg_wm_base::XdgWmBase, + pub(crate) has_active_role: AtomicBool, +} + +impl Dispatch2 for XdgSurfaceUserData +where + D: Dispatch, + D: Dispatch, + D: XdgShellHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + xdg_surface: &XdgSurface, + request: xdg_surface::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + xdg_surface::Request::Destroy => { + self.known_surfaces + .lock() + .unwrap() + .swap_remove(&xdg_surface.downgrade()); + + if !self.wl_surface.alive() { + // the wl_surface is destroyed, this means the client is not + // trying to change the role but it's a cleanup (possibly a + // disconnecting client), ignore the protocol check. + return; + } + + if compositor::get_role(&self.wl_surface).is_none() { + // No role assigned to the surface, we can exit early. + return; + } + + if self.has_active_role.load(Ordering::Acquire) { + self.wm_base.post_error( + xdg_wm_base::Error::Role, + "xdg_surface was destroyed before its role object", + ); + } + } + xdg_surface::Request::GetToplevel { id } => { + // We now can assign a role to the surface + let surface = &self.wl_surface; + let shell = &self.wm_base; + + if compositor::give_role(surface, XDG_TOPLEVEL_ROLE).is_err() { + shell.post_error(xdg_wm_base::Error::Role, "Surface already has a role."); + return; + } + + self.has_active_role.store(true, Ordering::Release); + + let initial = compositor::with_states(surface, |states| { + let initial = states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(XdgToplevelSurfaceRoleAttributes::default()) + }); + + // Initialize the toplevel capabilities from the default capabilities + let default_capabilities = &state.xdg_shell_state().default_capabilities; + let mut attributes = states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap(); + if attributes.server_pending.is_none() { + let current = attributes.current_server_state(); + attributes.server_pending = Some(current); + } + let current_capabilities = &mut attributes.server_pending.as_mut().unwrap().capabilities; + current_capabilities.replace(default_capabilities.capabilities.iter().copied()); + + initial + }); + + if initial { + compositor::add_pre_commit_hook::( + surface, + super::super::ToplevelSurface::pre_commit_hook, + ); + } + + let toplevel = data_init.init( + id, + XdgShellSurfaceUserData { + wl_surface: self.wl_surface.clone(), + xdg_surface: xdg_surface.clone(), + wm_base: self.wm_base.clone(), + decoration: Default::default(), + dialog: Default::default(), + alive_tracker: Default::default(), + }, + ); + + state + .xdg_shell_state() + .known_toplevels + .push(make_toplevel_handle(&toplevel)); + + let handle = make_toplevel_handle(&toplevel); + + XdgShellHandler::new_toplevel(state, handle); + } + xdg_surface::Request::GetPopup { + id, + parent, + positioner, + } => { + let positioner_data = *positioner + .data::() + .unwrap() + .inner + .lock() + .unwrap(); + + let parent_surface = parent.map(|parent| { + let parent_data = parent.data::().unwrap(); + parent_data.wl_surface.clone() + }); + + // We now can assign a role to the surface + let surface = &self.wl_surface; + let shell = &self.wm_base; + + let attributes = XdgPopupSurfaceRoleAttributes { + parent: parent_surface, + server_pending: Some(PopupState { + // Set the positioner data as the popup geometry + geometry: positioner_data.get_geometry(), + positioner: positioner_data, + }), + ..Default::default() + }; + if compositor::give_role(surface, XDG_POPUP_ROLE).is_err() { + shell.post_error(xdg_wm_base::Error::Role, "Surface already has a role."); + return; + } + + self.has_active_role.store(true, Ordering::Release); + + let initial = compositor::with_states(surface, |states| { + let inserted = states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(XdgPopupSurfaceRoleAttributes::default()) + }); + *states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() = attributes; + inserted + }); + + if initial { + compositor::add_pre_commit_hook::( + surface, + super::super::PopupSurface::pre_commit_hook, + ); + } + + let popup = data_init.init( + id, + XdgShellSurfaceUserData { + wl_surface: self.wl_surface.clone(), + xdg_surface: xdg_surface.clone(), + wm_base: self.wm_base.clone(), + decoration: Default::default(), + dialog: Default::default(), + alive_tracker: Default::default(), + }, + ); + + state + .xdg_shell_state() + .known_popups + .push(make_popup_handle(&popup)); + + let handle = make_popup_handle(&popup); + + XdgShellHandler::new_popup(state, handle, positioner_data); + } + xdg_surface::Request::SetWindowGeometry { x, y, width, height } => { + // Check the role of the surface, this can be either xdg_toplevel + // or xdg_popup. If none of the role matches the xdg_surface has no role set + // which is a protocol error. + let surface = &self.wl_surface; + + let role = compositor::get_role(surface); + + if role.is_none() { + xdg_surface.post_error( + xdg_surface::Error::NotConstructed, + "xdg_surface must have a role.", + ); + return; + } + + if role != Some(XDG_TOPLEVEL_ROLE) && role != Some(XDG_POPUP_ROLE) { + self.wm_base.post_error( + xdg_wm_base::Error::Role, + "xdg_surface must have a role of xdg_toplevel or xdg_popup.", + ); + } + + if width <= 0 || height <= 0 { + xdg_surface.post_error( + xdg_surface::Error::InvalidSize, + "width and height of the window geometry must be greater than zero.", + ); + return; + } + + compositor::with_states(surface, |states| { + states.cached_state.get::().pending().geometry = + Some(Rectangle::new((x, y).into(), (width, height).into())); + }); + } + xdg_surface::Request::AckConfigure { serial } => { + let serial = Serial::from(serial); + let surface = &self.wl_surface; + + // Check the role of the surface, this can be either xdg_toplevel + // or xdg_popup. If none of the role matches the xdg_surface has no role set + // which is a protocol error. + if compositor::get_role(surface).is_none() { + xdg_surface.post_error( + xdg_surface::Error::NotConstructed, + "xdg_surface must have a role.", + ); + return; + } + + // Find the correct configure state for the provided serial + // discard all configure states that are older than the provided + // serial. + // If no matching serial can be found raise a protocol error + // + // Invoke the user impl with the found configuration + // This has to include the serial and the role specific data. + // - For xdg_popup there is no data. + // - For xdg_toplevel send the state data including + // width, height, min/max size, maximized, fullscreen, resizing, activated + // + // This can be used to integrate custom protocol extensions + let found_configure = compositor::with_states(surface, |states| { + if states.role == Some(XDG_TOPLEVEL_ROLE) { + Ok(states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .ack_configure(serial)) + } else if states.role == Some(XDG_POPUP_ROLE) { + Ok(states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .ack_configure(serial)) + } else { + Err(()) + } + }); + + let configure = match found_configure { + Ok(Some(configure)) => configure, + Ok(None) => { + self.wm_base.post_error( + xdg_wm_base::Error::InvalidSurfaceState, + format!("wrong configure serial: {}", ::from(serial)), + ); + return; + } + Err(()) => { + self.wm_base.post_error( + xdg_wm_base::Error::Role, + "xdg_surface must have a role of xdg_toplevel or xdg_popup.", + ); + return; + } + }; + + XdgShellHandler::ack_configure(state, surface.clone(), configure); + } + _ => unreachable!(), + } + } +} + +/// User data of xdg toplevel surface +#[derive(Debug)] +pub struct XdgShellSurfaceUserData { + pub(crate) wl_surface: wl_surface::WlSurface, + pub(crate) wm_base: xdg_wm_base::XdgWmBase, + pub(crate) xdg_surface: xdg_surface::XdgSurface, + pub(crate) decoration: Mutex>, + pub(crate) dialog: Mutex>, + + pub(crate) alive_tracker: AliveTracker, +} + +impl XdgShellSurfaceUserData { + /// Associated xdg_surface + pub fn xdg_surface(&self) -> &xdg_surface::XdgSurface { + &self.xdg_surface + } +} + +impl IsAlive for XdgToplevel { + #[inline] + fn alive(&self) -> bool { + let data: &XdgShellSurfaceUserData = self.data().unwrap(); + data.alive_tracker.alive() + } +} + +impl IsAlive for XdgPopup { + #[inline] + fn alive(&self) -> bool { + let data: &XdgShellSurfaceUserData = self.data().unwrap(); + data.alive_tracker.alive() + } +} diff --git a/vendor/smithay/src/wayland/shell/xdg/handlers/surface/popup.rs b/vendor/smithay/src/wayland/shell/xdg/handlers/surface/popup.rs new file mode 100644 index 0000000..eb2cb55 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/handlers/surface/popup.rs @@ -0,0 +1,133 @@ +use std::sync::atomic::Ordering; + +use crate::{ + utils::Serial, + wayland::{ + Dispatch2, + compositor::{self, with_states}, + shell::xdg::{PopupCachedState, SurfaceCachedState, XdgPopupSurfaceData, XdgPositionerUserData}, + }, +}; + +use wayland_protocols::xdg::shell::server::xdg_popup::{self, XdgPopup}; + +use wayland_server::{DataInit, DisplayHandle, Resource, backend::ClientId}; + +use super::{PopupConfigure, XdgShellHandler, XdgShellSurfaceUserData, XdgSurfaceUserData}; + +impl Dispatch2 for XdgShellSurfaceUserData +where + D: XdgShellHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + popup: &XdgPopup, + request: xdg_popup::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + xdg_popup::Request::Destroy => { + if let Some(surface_data) = self.xdg_surface.data::() { + surface_data.has_active_role.store(false, Ordering::Release); + } + } + xdg_popup::Request::Grab { seat, serial } => { + let handle = crate::wayland::shell::xdg::PopupSurface { + wl_surface: self.wl_surface.clone(), + shell_surface: popup.clone(), + }; + + let serial = Serial::from(serial); + + with_states(handle.wl_surface(), |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .requested_grab = Some((seat, serial)); + }); + } + xdg_popup::Request::Reposition { positioner, token } => { + let handle = crate::wayland::shell::xdg::PopupSurface { + wl_surface: self.wl_surface.clone(), + shell_surface: popup.clone(), + }; + + let positioner_data = *positioner + .data::() + .unwrap() + .inner + .lock() + .unwrap(); + + XdgShellHandler::reposition_request(state, handle, positioner_data, token); + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client_id: ClientId, xdg_popup: &XdgPopup) { + self.alive_tracker.destroy_notify(); + + // remove this surface from the known ones (as well as any leftover dead surface) + if let Some(index) = state + .xdg_shell_state() + .known_popups + .iter() + .position(|pop| pop.shell_surface.id() == xdg_popup.id()) + { + let popup = state.xdg_shell_state().known_popups.remove(index); + let surface = popup.wl_surface().clone(); + XdgShellHandler::popup_destroyed(state, popup); + compositor::with_states(&surface, |states| { + *states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() = Default::default(); + + let mut guard = states.cached_state.get::(); + *guard.pending() = Default::default(); + *guard.current() = Default::default(); + + let mut guard = states.cached_state.get::(); + *guard.pending() = Default::default(); + *guard.current() = Default::default(); + }) + } + } +} + +pub fn send_popup_configure(resource: &XdgPopup, configure: PopupConfigure) { + let data = resource.data::().unwrap(); + + let serial = configure.serial; + let geometry = configure.state.geometry; + + // Send repositioned if token is set + if let Some(token) = configure.reposition_token { + resource.repositioned(token); + } + + // Send the popup configure + resource.configure(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h); + + // Send the base xdg_surface configure event to mark + // the configure as finished + data.xdg_surface.configure(serial.into()); +} + +pub fn make_popup_handle(resource: &XdgPopup) -> crate::wayland::shell::xdg::PopupSurface { + let data = resource.data::().unwrap(); + crate::wayland::shell::xdg::PopupSurface { + wl_surface: data.wl_surface.clone(), + shell_surface: resource.clone(), + } +} diff --git a/vendor/smithay/src/wayland/shell/xdg/handlers/surface/toplevel.rs b/vendor/smithay/src/wayland/shell/xdg/handlers/surface/toplevel.rs new file mode 100644 index 0000000..cf2a007 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/handlers/surface/toplevel.rs @@ -0,0 +1,324 @@ +use std::sync::atomic::Ordering; + +use crate::{ + utils::Serial, + wayland::{ + Dispatch2, compositor, + shell::{ + is_valid_parent, + xdg::{ToplevelCachedState, XdgToplevelSurfaceData}, + }, + }, +}; + +use wayland_protocols::xdg::{ + decoration::zv1::server::zxdg_toplevel_decoration_v1, + shell::server::xdg_toplevel::{self, XdgToplevel}, +}; + +use wayland_server::{DataInit, DisplayHandle, Resource, WEnum, backend::ClientId, protocol::wl_surface}; + +use super::{ + SurfaceCachedState, ToplevelConfigure, XdgShellHandler, XdgShellSurfaceUserData, XdgSurfaceUserData, + XdgToplevelSurfaceRoleAttributes, +}; + +impl Dispatch2 for XdgShellSurfaceUserData +where + D: XdgShellHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + toplevel: &XdgToplevel, + request: xdg_toplevel::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + xdg_toplevel::Request::Destroy => { + if let Some(surface_data) = self.xdg_surface.data::() { + surface_data.has_active_role.store(false, Ordering::Release); + } + + if let Some(decoration) = self.decoration.lock().unwrap().clone() { + decoration.post_error( + zxdg_toplevel_decoration_v1::Error::Orphaned, + "The xdg_toplevel_decoration object must be destroyed before its xdg_toplevel.", + ); + } + } + xdg_toplevel::Request::SetParent { parent } => { + let parent_surface = parent.map(|toplevel_surface_parent| { + toplevel_surface_parent + .data::() + .unwrap() + .wl_surface + .clone() + }); + + let old_parent = get_parent(toplevel); + let changed = old_parent != parent_surface; + + // Parent is not double buffered, we can set it directly + if !set_parent(toplevel, parent_surface) { + toplevel.post_error(xdg_toplevel::Error::InvalidParent, "invalid parent toplevel"); + } + + if changed { + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::parent_changed(state, handle); + } + } + xdg_toplevel::Request::SetTitle { title } => { + // Title is not double buffered, we can set it directly + let changed = with_surface_toplevel_role_data(toplevel, |role| { + if role.title.as_ref() != Some(&title) { + role.title = Some(title); + true + } else { + false + } + }); + + if changed { + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::title_changed(state, handle); + } + } + xdg_toplevel::Request::SetAppId { app_id } => { + // AppId is not double buffered, we can set it directly + let changed = with_surface_toplevel_role_data(toplevel, |role| { + if role.app_id.as_ref() != Some(&app_id) { + role.app_id = Some(app_id); + true + } else { + false + } + }); + + if changed { + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::app_id_changed(state, handle); + } + } + xdg_toplevel::Request::ShowWindowMenu { seat, serial, x, y } => { + // This has to be handled by the compositor + let handle = make_toplevel_handle(toplevel); + let serial = Serial::from(serial); + + XdgShellHandler::show_window_menu(state, handle, seat, serial, (x, y).into()); + } + xdg_toplevel::Request::Move { seat, serial } => { + // This has to be handled by the compositor + let handle = make_toplevel_handle(toplevel); + let serial = Serial::from(serial); + + XdgShellHandler::move_request(state, handle, seat, serial); + } + xdg_toplevel::Request::Resize { seat, serial, edges } => { + if let WEnum::Value(edges) = edges { + // This has to be handled by the compositor + let handle = make_toplevel_handle(toplevel); + let serial = Serial::from(serial); + + XdgShellHandler::resize_request(state, handle, seat, serial, edges); + } + } + xdg_toplevel::Request::SetMaxSize { width, height } => { + if width < 0 || height < 0 { + toplevel.post_error( + xdg_toplevel::Error::InvalidSize, + "The max size cannot be negative", + ); + return; + } + with_toplevel_pending_state(self, |toplevel_data| { + toplevel_data.max_size = (width, height).into(); + }); + } + xdg_toplevel::Request::SetMinSize { width, height } => { + if width < 0 || height < 0 { + toplevel.post_error( + xdg_toplevel::Error::InvalidSize, + "The min size cannot be negative", + ); + return; + } + with_toplevel_pending_state(self, |toplevel_data| { + toplevel_data.min_size = (width, height).into(); + }); + } + xdg_toplevel::Request::SetMaximized => { + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::maximize_request(state, handle); + } + xdg_toplevel::Request::UnsetMaximized => { + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::unmaximize_request(state, handle); + } + xdg_toplevel::Request::SetFullscreen { output } => { + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::fullscreen_request(state, handle, output); + } + xdg_toplevel::Request::UnsetFullscreen => { + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::unfullscreen_request(state, handle); + } + xdg_toplevel::Request::SetMinimized => { + // This has to be handled by the compositor, may not be + // supported and just ignored + let handle = make_toplevel_handle(toplevel); + XdgShellHandler::minimize_request(state, handle); + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client_id: ClientId, xdg_toplevel: &XdgToplevel) { + self.alive_tracker.destroy_notify(); + self.decoration.lock().unwrap().take(); + + if let Some(index) = state + .xdg_shell_state() + .known_toplevels + .iter() + .position(|top| top.shell_surface.id() == xdg_toplevel.id()) + { + let toplevel = state.xdg_shell_state().known_toplevels.remove(index); + let surface = toplevel.wl_surface().clone(); + XdgShellHandler::toplevel_destroyed(state, toplevel); + compositor::with_states(&surface, |states| { + *states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() = Default::default(); + + let mut guard = states.cached_state.get::(); + *guard.pending() = Default::default(); + *guard.current() = Default::default(); + + let mut guard = states.cached_state.get::(); + *guard.pending() = Default::default(); + *guard.current() = Default::default(); + }) + } + } +} + +// Utility functions allowing to factor out a lot of the upcoming logic +fn with_surface_toplevel_role_data(toplevel: &xdg_toplevel::XdgToplevel, f: F) -> T +where + F: FnOnce(&mut XdgToplevelSurfaceRoleAttributes) -> T, +{ + let data = toplevel.data::().unwrap(); + compositor::with_states(&data.wl_surface, |states| { + f(&mut states + .data_map + .get::() + .unwrap() + .lock() + .unwrap()) + }) +} + +pub(super) fn make_toplevel_handle( + resource: &xdg_toplevel::XdgToplevel, +) -> crate::wayland::shell::xdg::ToplevelSurface { + let data = resource.data::().unwrap(); + crate::wayland::shell::xdg::ToplevelSurface { + wl_surface: data.wl_surface.clone(), + shell_surface: resource.clone(), + } +} + +pub fn get_parent(toplevel: &xdg_toplevel::XdgToplevel) -> Option { + with_surface_toplevel_role_data(toplevel, |data| data.parent.clone()) +} + +/// Sets the parent of the specified toplevel surface and returns whether the parent was successfully set. +/// +/// The parent must be a toplevel surface. +/// +/// The parent of a surface is not double buffered and therefore may be set directly. +/// +/// If the parent is `None`, the parent-child relationship is removed. +pub fn set_parent(toplevel: &xdg_toplevel::XdgToplevel, parent: Option) -> bool { + if let Some(parent) = &parent { + let data = toplevel.data::().unwrap(); + if !is_valid_parent(&data.wl_surface, parent) { + return false; + } + } + + with_surface_toplevel_role_data(toplevel, |data| { + data.parent = parent; + }); + + true +} + +fn with_toplevel_pending_state(data: &XdgShellSurfaceUserData, f: F) -> T +where + F: FnOnce(&mut SurfaceCachedState) -> T, +{ + compositor::with_states(&data.wl_surface, |states| { + f(states.cached_state.get::().pending()) + }) +} + +pub fn send_toplevel_configure( + resource: &xdg_toplevel::XdgToplevel, + configure: ToplevelConfigure, + send_bounds: bool, + send_capabilities: bool, +) { + let data = resource.data::().unwrap(); + let (width, height) = configure.state.size.unwrap_or_default().into(); + // convert the Vec (which is really a Vec) into Vec + let states = { + let mut states: Vec = + configure.state.states.into_filtered_states(resource.version()); + let ptr = states.as_mut_ptr(); + let len = states.len(); + let cap = states.capacity(); + ::std::mem::forget(states); + unsafe { Vec::from_raw_parts(ptr as *mut u8, len * 4, cap * 4) } + }; + let serial = configure.serial; + + // send bounds if requested + if send_bounds && resource.version() >= xdg_toplevel::EVT_CONFIGURE_BOUNDS_SINCE { + let bounds = configure.state.bounds.unwrap_or_default(); + resource.configure_bounds(bounds.w, bounds.h); + } + + // send the capabilities if requested + if send_capabilities && resource.version() >= xdg_toplevel::EVT_WM_CAPABILITIES_SINCE { + let mut capabilities = configure + .state + .capabilities + .capabilities() + .copied() + .collect::>(); + let capabilities = { + let ptr = capabilities.as_mut_ptr(); + let len = capabilities.len(); + let cap = capabilities.capacity(); + ::std::mem::forget(capabilities); + unsafe { Vec::from_raw_parts(ptr as *mut u8, len * 4, cap * 4) } + }; + resource.wm_capabilities(capabilities); + } + + // Send the toplevel configure + resource.configure(width, height, states); + + // Send the base xdg_surface configure event to mark + // The configure as finished + data.xdg_surface.configure(serial.into()); +} diff --git a/vendor/smithay/src/wayland/shell/xdg/handlers/wm_base.rs b/vendor/smithay/src/wayland/shell/xdg/handlers/wm_base.rs new file mode 100644 index 0000000..01dd898 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/handlers/wm_base.rs @@ -0,0 +1,129 @@ +use std::sync::{Arc, Mutex, atomic::AtomicBool}; + +use indexmap::IndexSet; + +use crate::{ + utils::{IsAlive, Serial, alive_tracker::AliveTracker}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +use wayland_protocols::xdg::shell::server::{ + xdg_positioner::XdgPositioner, xdg_surface, xdg_surface::XdgSurface, xdg_wm_base, xdg_wm_base::XdgWmBase, +}; + +use wayland_server::{DataInit, Dispatch, DisplayHandle, New, Resource, Weak, backend::ClientId}; + +use super::{ShellClient, ShellClientData, XdgPositionerUserData, XdgShellHandler, XdgSurfaceUserData}; + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: Dispatch, + D: XdgShellHandler, + D: 'static, +{ + fn bind( + &self, + state: &mut D, + _dh: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let shell = data_init.init(resource, XdgWmBaseUserData::default()); + + XdgShellHandler::new_client(state, ShellClient::new(&shell)); + } +} + +impl Dispatch2 for XdgWmBaseUserData +where + D: Dispatch, + D: Dispatch, + D: XdgShellHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + wm_base: &XdgWmBase, + request: xdg_wm_base::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + xdg_wm_base::Request::CreatePositioner { id } => { + data_init.init(id, XdgPositionerUserData::default()); + } + xdg_wm_base::Request::GetXdgSurface { id, surface } => { + // Do not assign a role to the surface here + // xdg_surface is not role, only xdg_toplevel and + // xdg_popup are defined as roles + let xdg_surface = data_init.init( + id, + XdgSurfaceUserData { + known_surfaces: self.known_surfaces.clone(), + wl_surface: surface, + wm_base: wm_base.clone(), + has_active_role: AtomicBool::new(false), + }, + ); + self.known_surfaces + .lock() + .unwrap() + .insert(xdg_surface.downgrade()); + } + xdg_wm_base::Request::Pong { serial } => { + let serial = Serial::from(serial); + let valid = { + let mut guard = self.client_data.lock().unwrap(); + if guard.pending_ping == Some(serial) { + guard.pending_ping = None; + true + } else { + false + } + }; + if valid { + XdgShellHandler::client_pong(state, ShellClient::new(wm_base)); + } + } + xdg_wm_base::Request::Destroy => { + if !self.known_surfaces.lock().unwrap().is_empty() { + wm_base.post_error( + xdg_wm_base::Error::DefunctSurfaces, + "xdg_wm_base was destroyed before children", + ); + } + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client_id: ClientId, wm_base: &XdgWmBase) { + XdgShellHandler::client_destroyed(state, ShellClient::new(wm_base)); + self.alive_tracker.destroy_notify(); + } +} + +impl IsAlive for XdgWmBase { + #[inline] + fn alive(&self) -> bool { + let data: &XdgWmBaseUserData = self.data().unwrap(); + data.alive_tracker.alive() + } +} + +/* + * xdg_shell + */ + +/// User data for Xdg Wm Base +#[derive(Default, Debug)] +pub struct XdgWmBaseUserData { + pub(crate) client_data: Mutex, + known_surfaces: Arc>>>, + alive_tracker: AliveTracker, +} diff --git a/vendor/smithay/src/wayland/shell/xdg/mod.rs b/vendor/smithay/src/wayland/shell/xdg/mod.rs new file mode 100644 index 0000000..f9d7862 --- /dev/null +++ b/vendor/smithay/src/wayland/shell/xdg/mod.rs @@ -0,0 +1,2181 @@ +//! Utilities for handling shell surfaces with the `xdg_shell` protocol +//! +//! This module provides automatic handling of shell surfaces objects, by being registered +//! as a global handler for `xdg_shell`. +//! +//! ## Why use this implementation +//! +//! This implementation can track for you the various shell surfaces defined by the +//! clients by handling the `xdg_shell` protocol. +//! +//! It allows you to easily access a list of all shell surfaces defined by your clients +//! access their associated metadata and underlying `wl_surface`s. +//! +//! This handler only handles the protocol exchanges with the client to present you the +//! information in a coherent and relatively easy to use manner. All the actual drawing +//! and positioning logic of windows is out of its scope. +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this handler, create [`XdgShellState`], store it in your `State` struct and +//! implement the [`XdgShellHandler`], as shown in this example: +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use smithay::reexports::wayland_server::protocol::{wl_seat, wl_surface}; +//! use smithay::wayland::shell::xdg::{XdgShellState, XdgShellHandler, ToplevelSurface, PopupSurface, PositionerState}; +//! use smithay::utils::Serial; +//! +//! # struct State { xdg_shell_state: XdgShellState, seat_state: SeatState } +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! let xdg_shell_state = XdgShellState::new::( +//! &display.handle(), +//! ); +//! +//! // insert the xdg_shell_state into your state +//! // ... +//! +//! // implement the necessary traits +//! impl XdgShellHandler for State { +//! fn xdg_shell_state(&mut self) -> &mut XdgShellState { +//! &mut self.xdg_shell_state +//! } +//! +//! // handle the shell requests here. +//! // more optional methods can be used to further customized +//! fn new_toplevel(&mut self, surface: ToplevelSurface) { +//! // ... +//! } +//! fn new_popup( +//! &mut self, +//! surface: PopupSurface, +//! positioner: PositionerState, +//! ) { +//! // ... +//! } +//! fn grab( +//! &mut self, +//! surface: PopupSurface, +//! seat: wl_seat::WlSeat, +//! serial: Serial, +//! ) { +//! // ... +//! } +//! fn reposition_request( +//! &mut self, +//! surface: PopupSurface, +//! positioner: PositionerState, +//! token: u32, +//! ) { +//! // ... +//! } +//! } +//! +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! type Target = wl_surface::WlSurface; +//! impl SeatHandler for State { +//! type KeyboardFocus = Target; +//! type PointerFocus = Target; +//! type TouchFocus = Target; +//! +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) { +//! // handle focus changes, if you need to ... +//! } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { +//! // handle new images for the cursor ... +//! } +//! } +//! # impl PointerConstraintsHandler for State {} +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Access to shell surface and clients data +//! +//! There are mainly 3 kind of objects that you'll manipulate from this implementation: +//! +//! - [`ShellClient`]: +//! This is a handle representing an instantiation of a shell global +//! you can associate client-wise metadata to it through an [`UserDataMap`]. +//! - [`ToplevelSurface`]: +//! This is a handle representing a toplevel surface, you can +//! retrieve a list of all currently alive toplevel surface from the +//! [`XdgShellState`]. +//! - [`PopupSurface`]: +//! This is a handle representing a popup/tooltip surface. Similarly, +//! you can get a list of all currently alive popup surface from the +//! [`XdgShellState`]. +//! +//! You'll obtain these objects though two means: either via the callback methods of +//! the [`XdgShellHandler`], or via methods on the [`XdgShellState`]. + +use crate::utils::alive_tracker::IsAlive; +use crate::utils::{Logical, Point, Rectangle, Size, user_data::UserDataMap}; +use crate::utils::{SERIAL_COUNTER, Serial}; +use crate::wayland::GlobalData; +use crate::wayland::compositor::Cacheable; +use crate::wayland::compositor::{self, BufferAssignment, SurfaceAttributes}; +use crate::wayland::shell::xdg::dialog::ToplevelDialogHint; +use std::cmp::min; +use std::{collections::HashSet, fmt::Debug, sync::Mutex}; + +use tracing::{error, trace, trace_span}; +use wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1; +use wayland_protocols::xdg::shell::server::xdg_positioner::{Anchor, ConstraintAdjustment, Gravity}; +use wayland_protocols::xdg::shell::server::xdg_surface; +use wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase; +use wayland_protocols::xdg::shell::server::{xdg_popup, xdg_positioner, xdg_toplevel, xdg_wm_base}; +use wayland_server::backend::GlobalId; +use wayland_server::{ + DisplayHandle, GlobalDispatch, Resource, + protocol::{wl_output, wl_seat, wl_surface}, +}; + +use super::PingError; + +pub mod decoration; +pub mod dialog; + +// handlers for the xdg_shell protocol +pub(super) mod handlers; +pub use handlers::{XdgPositionerUserData, XdgShellSurfaceUserData, XdgSurfaceUserData, XdgWmBaseUserData}; + +/// The role of an XDG toplevel surface. +pub const XDG_TOPLEVEL_ROLE: &str = "xdg_toplevel"; + +/// The role of an XDG popup surface. +pub const XDG_POPUP_ROLE: &str = "xdg_popup"; + +/// Constant for toplevel state version checking +const XDG_TOPLEVEL_STATE_TILED_SINCE: u32 = 2; +const XDG_TOPLEVEL_STATE_SUSPENDED_SINCE: u32 = 6; +const XDG_TOPLEVEL_STATE_CONSTRAINED_SINCE: u32 = 7; + +macro_rules! xdg_role { + ($state:ty, + $(#[$configure_meta:meta])* $configure_name:ident $({$($(#[$configure_field_meta:meta])* $configure_field_vis:vis$configure_field_name:ident:$configure_field_type:ty),*}),*, + $(#[$attributes_meta:meta])* $attributes_name:ident {$($(#[$attributes_field_meta:meta])* $attributes_field_vis:vis$attributes_field_name:ident:$attributes_field_type:ty),*}, + $(#[$cached_state_meta:meta])* $cached_state_name:ident) => { + + $(#[$configure_meta])* + pub struct $configure_name { + /// The state associated with this configure + pub state: $state, + /// A serial number to track ACK from the client + /// + /// This should be an ever increasing number, as the ACK-ing + /// from a client for a serial will validate all pending lower + /// serials. + pub serial: Serial, + + $($( + $(#[$configure_field_meta])* + $configure_field_vis $configure_field_name: $configure_field_type, + )*)* + } + + $(#[$attributes_meta])* + pub struct $attributes_name { + /// Holds the state if the surface has sent the initial + /// configure event to the client. It is expected that + /// during the first commit a initial + /// configure event is sent to the client + pub initial_configure_sent: bool, + /// Holds the configures the server has sent out + /// to the client waiting to be acknowledged by + /// the client. All pending configures that are older + /// than the acknowledged one will be discarded during + /// processing xdg_surface.ack_configure. + pending_configures: Vec<$configure_name>, + /// Holds the pending state as set by the server. + pub server_pending: Option<$state>, + /// Holds the last configure that has been acknowledged by the client. This state + /// should be cloned to the current during a commit. Note that this state can be + /// newer than the last acked state at the time of the last commit. + pub last_acked: Option<$configure_name>, + + $( + $(#[$attributes_field_meta])* + $attributes_field_vis $attributes_field_name: $attributes_field_type, + )* + } + + impl $attributes_name { + fn ack_configure(&mut self, serial: Serial) -> Option { + let configure = match self + .pending_configures + .iter() + .find(|configure| configure.serial == serial) + { + Some(configure) => (*configure).clone(), + None => { + return None; + } + }; + + // Save the configure as the last acked configure + self.last_acked = Some(configure.clone()); + + // Clean old configures + self.pending_configures.retain(|c| c.serial > serial); + + Some(configure.into()) + } + + /// Gets the latest state that has been configured + /// on the server and sent to the client. + /// + /// The state includes all changes that have been + /// made on the server, including all not yet + /// acked or committed changes, but excludes the + /// current [`server_pending`](#structfield.server_pending) state. + /// + /// This can be used for example to check if the + /// [`server_pending`](#structfield.server_pending) state + /// is different from the last configured. + pub fn current_server_state(&self) -> $state { + // We check if there is already a non-acked pending + // configure and use its state or otherwise we could + // loose some state that was previously configured + // and sent, but not acked before calling with_pending_state + // again. If there is no pending state we try to use the + // last acked state which could contain state changes + // already acked but not committed to the current state. + // In both cases the state already contains all previous + // sent states. This way all pending state is accumulated + // into the current state. + self.pending_configures + .last() + .map(|c| &c.state) + .or_else(|| self.last_acked.as_ref().map(|c| &c.state)) + .cloned() + .unwrap_or_default() + } + + /// Check if the state has pending changes that have + /// not been sent to the client. + /// + /// This differs from just checking if the [`server_pending`](#structfield.server_pending) + /// state is [`Some`] in that it also checks if a current pending + /// state is different from the [`current_server_state`](#method.current_server_state). + pub fn has_pending_changes(&self) -> bool { + self.server_pending.as_ref().map(|s| *s != self.current_server_state()).unwrap_or(false) + } + + /// Returns a list of configures sent to, but not yet acknowledged by the client. + /// + /// The list is ordered by age, so the last configure in the list is the last one sent + /// to the client. + pub fn pending_configures(&self) -> &[$configure_name] { + &self.pending_configures + } + } + + impl Default for $attributes_name { + fn default() -> Self { + Self { + pending_configures: Vec::new(), + initial_configure_sent: false, + server_pending: None, + last_acked: None, + + $( + $attributes_field_name: Default::default(), + )* + } + } + } + + $(#[$cached_state_meta])* + pub struct $cached_state_name { + /// Configure last acknowledged by the client at the time of the commit. + /// + /// Reset to `None` when the surface unmaps. + pub last_acked: Option<$configure_name>, + } + + impl Cacheable for $cached_state_name { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + self.clone() + } + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } + } + }; +} + +xdg_role!( + ToplevelState, + /// A configure message for toplevel surfaces + #[derive(Debug, Clone)] + ToplevelConfigure, + /// Role specific attributes for xdg_toplevel + /// + /// This interface defines an xdg_surface role which allows a surface to, + /// among other things, set window-like properties such as maximize, + /// fullscreen, and minimize, set application-specific metadata like title and + /// id, and well as trigger user interactive operations such as interactive + /// resize and move. + /// + /// Unmapping an xdg_toplevel means that the surface cannot be shown + /// by the compositor until it is explicitly mapped again. + /// All active operations (e.g., move, resize) are cancelled and all + /// attributes (e.g. title, state, stacking, ...) are discarded for + /// an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + /// the state it had right after xdg_surface.get_toplevel. The client + /// can re-map the toplevel by performing a commit without any buffer + /// attached, waiting for a configure event and handling it as usual (see + /// xdg_surface description). + /// + /// Attaching a null buffer to a toplevel unmaps the surface. + #[derive(Debug)] + XdgToplevelSurfaceRoleAttributes { + /// The parent field of a toplevel should be used + /// by the compositor to determine which toplevel + /// should be brought to front. If the parent is focused + /// all of it's child should be brought to front. + pub parent: Option, + /// Hint related to the toplevel's "dialog" status. + /// See ToplevelDialogHint for more information. + /// + /// This value has no effect on toplevels that are not attached to a parent toplevel. + pub dialog_hint: ToplevelDialogHint, + /// Holds the optional title the client has set for + /// this toplevel. For example a web-browser will most likely + /// set this to include the current uri. + /// + /// This string may be used to identify the surface in a task bar, + /// window list, or other user interface elements provided by the + /// compositor. + pub title: Option, + /// Holds the optional app ID the client has set for + /// this toplevel. + /// + /// The app ID identifies the general class of applications to which + /// the surface belongs. The compositor can use this to group multiple + /// surfaces together, or to determine how to launch a new application. + /// + /// For D-Bus activatable applications, the app ID is used as the D-Bus + /// service name. + pub app_id: Option, + /// An `zxdg_toplevel_decoration_v1::configure` event has been sent + /// to the client. + pub initial_decoration_configure_sent: bool + }, + /// Represents the xdg_toplevel pending state + #[derive(Debug, Default, Clone)] + ToplevelCachedState +); + +/// Data associated with XDG toplevel surface +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// states.data_map.get::(); +/// }); +/// ``` +pub type XdgToplevelSurfaceData = Mutex; + +xdg_role!( + PopupState, + /// A configure message for popup surface + #[derive(Debug, Clone, Copy)] + PopupConfigure { + /// The token the client provided in the `xdg_popup::reposition` + /// request. The token itself is opaque, and has no other special meaning. + /// The token is sent in the corresponding `xdg_popup::repositioned` event. + pub reposition_token: Option + }, + /// Role specific attributes for xdg_popup + /// + /// A popup surface is a short-lived, temporary surface. It can be used to + /// implement for example menus, popovers, tooltips and other similar user + /// interface concepts. + /// + /// A popup can be made to take an explicit grab. See xdg_popup.grab for + /// details. + /// + /// When the popup is dismissed, a popup_done event will be sent out, and at + /// the same time the surface will be unmapped. See the xdg_popup.popup_done + /// event for details. + /// + /// Explicitly destroying the xdg_popup object will also dismiss the popup and + /// unmap the surface. Clients that want to dismiss the popup when another + /// surface of their own is clicked should dismiss the popup using the destroy + /// request. + /// + /// A newly created xdg_popup will be stacked on top of all previously created + /// xdg_popup surfaces associated with the same xdg_toplevel. + /// + /// The parent of an xdg_popup must be mapped (see the xdg_surface + /// description) before the xdg_popup itself. + /// + /// The client must call wl_surface.commit on the corresponding wl_surface + /// for the xdg_popup state to take effect. + #[derive(Debug)] + XdgPopupSurfaceRoleAttributes { + /// Holds the parent for the xdg_popup. + /// + /// The parent is allowed to remain unset as long + /// as no commit has been requested for the underlying + /// wl_surface. The parent can either be directly + /// specified during xdg_surface.get_popup or using + /// another protocol extension, for example xdg_layer_shell. + /// + /// It is a protocol error to call commit on a wl_surface with + /// the xdg_popup role when no parent is set. + pub parent: Option, + + /// If the popup requested a grab, this is the seat and serial. + requested_grab: Option<(wl_seat::WlSeat, Serial)> + }, + /// Represents the xdg_popup pending state + #[derive(Debug, Default, Clone)] + PopupCachedState +); + +/// Data associated with XDG popup surface +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::shell::xdg::XdgPopupSurfaceData; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// states.data_map.get::(); +/// }); +/// ``` +pub type XdgPopupSurfaceData = Mutex; + +/// Represents the state of the popup +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct PopupState { + /// The positioner state can be used by the compositor + /// to calculate the best placement for the popup. + /// + /// For example the compositor should prevent that a popup + /// is placed outside the visible rectangle of a output. + pub positioner: PositionerState, + /// Holds the geometry of the popup as defined by the positioner. + /// + /// `Rectangle::width` and `Rectangle::height` holds the size of the + /// of the popup in surface-local coordinates and corresponds to the + /// window geometry + /// + /// `Rectangle::x` and `Rectangle::y` holds the position of the popup + /// The position is relative to the window geometry as defined by + /// xdg_surface.set_window_geometry of the parent surface. + pub geometry: Rectangle, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// The state of a positioner, as set by the client +pub struct PositionerState { + /// Size of the rectangle that needs to be positioned + pub rect_size: Size, + /// Anchor rectangle in the parent surface coordinates + /// relative to which the surface must be positioned + pub anchor_rect: Rectangle, + /// Edges defining the anchor point + pub anchor_edges: xdg_positioner::Anchor, + /// Gravity direction for positioning the child surface + /// relative to its anchor point + pub gravity: xdg_positioner::Gravity, + /// Adjustments to do if previous criteria constrain the + /// surface + pub constraint_adjustment: xdg_positioner::ConstraintAdjustment, + /// Offset placement relative to the anchor point + pub offset: Point, + /// When set reactive, the surface is reconstrained if the conditions + /// used for constraining changed, e.g. the parent window moved. + /// + /// If the conditions changed and the popup was reconstrained, + /// an xdg_popup.configure event is sent with updated geometry, + /// followed by an xdg_surface.configure event. + pub reactive: bool, + /// The parent window geometry the compositor should use when + /// positioning the popup. The compositor may use this information + /// to determine the future state the popup should be constrained using. + /// If this doesn't match the dimension of the parent the popup is + /// eventually positioned against, the behavior is undefined. + /// + /// The arguments are given in the surface-local coordinate space. + pub parent_size: Option>, + /// The serial of an xdg_surface.configure event this positioner will + /// be used in response to. The compositor may use this information + /// together with set_parent_size to determine what future state the + /// popup should be constrained using. + pub parent_configure: Option, +} + +impl Default for PositionerState { + fn default() -> Self { + PositionerState { + anchor_edges: xdg_positioner::Anchor::None, + anchor_rect: Default::default(), + constraint_adjustment: xdg_positioner::ConstraintAdjustment::empty(), + gravity: xdg_positioner::Gravity::None, + offset: Default::default(), + rect_size: Default::default(), + reactive: false, + parent_size: None, + parent_configure: None, + } + } +} + +impl PositionerState { + pub(crate) fn anchor_has_edge(&self, edge: xdg_positioner::Anchor) -> bool { + match edge { + xdg_positioner::Anchor::Top => { + self.anchor_edges == xdg_positioner::Anchor::Top + || self.anchor_edges == xdg_positioner::Anchor::TopLeft + || self.anchor_edges == xdg_positioner::Anchor::TopRight + } + xdg_positioner::Anchor::Bottom => { + self.anchor_edges == xdg_positioner::Anchor::Bottom + || self.anchor_edges == xdg_positioner::Anchor::BottomLeft + || self.anchor_edges == xdg_positioner::Anchor::BottomRight + } + xdg_positioner::Anchor::Left => { + self.anchor_edges == xdg_positioner::Anchor::Left + || self.anchor_edges == xdg_positioner::Anchor::TopLeft + || self.anchor_edges == xdg_positioner::Anchor::BottomLeft + } + xdg_positioner::Anchor::Right => { + self.anchor_edges == xdg_positioner::Anchor::Right + || self.anchor_edges == xdg_positioner::Anchor::TopRight + || self.anchor_edges == xdg_positioner::Anchor::BottomRight + } + _ => unreachable!(), + } + } + + /// Get the anchor point for a popup as defined by this positioner. + /// + /// Defined by `xdg_positioner.set_anchor_rect` and + /// `xdg_positioner.set_anchor`. + pub fn get_anchor_point(&self) -> Point { + let mut point = self.anchor_rect.loc; + + point.y += if self.anchor_has_edge(xdg_positioner::Anchor::Top) { + 0 + } else if self.anchor_has_edge(xdg_positioner::Anchor::Bottom) { + self.anchor_rect.size.h + } else { + self.anchor_rect.size.h / 2 + }; + + point.x += if self.anchor_has_edge(xdg_positioner::Anchor::Left) { + 0 + } else if self.anchor_has_edge(xdg_positioner::Anchor::Right) { + self.anchor_rect.size.w + } else { + self.anchor_rect.size.w / 2 + }; + + point + } + + pub(crate) fn gravity_has_edge(&self, edge: xdg_positioner::Gravity) -> bool { + match edge { + xdg_positioner::Gravity::Top => { + self.gravity == xdg_positioner::Gravity::Top + || self.gravity == xdg_positioner::Gravity::TopLeft + || self.gravity == xdg_positioner::Gravity::TopRight + } + xdg_positioner::Gravity::Bottom => { + self.gravity == xdg_positioner::Gravity::Bottom + || self.gravity == xdg_positioner::Gravity::BottomLeft + || self.gravity == xdg_positioner::Gravity::BottomRight + } + xdg_positioner::Gravity::Left => { + self.gravity == xdg_positioner::Gravity::Left + || self.gravity == xdg_positioner::Gravity::TopLeft + || self.gravity == xdg_positioner::Gravity::BottomLeft + } + xdg_positioner::Gravity::Right => { + self.gravity == xdg_positioner::Gravity::Right + || self.gravity == xdg_positioner::Gravity::TopRight + || self.gravity == xdg_positioner::Gravity::BottomRight + } + _ => unreachable!(), + } + } + + /// Get the geometry for a popup as defined by this positioner. + /// + /// `Rectangle::width` and `Rectangle::height` corresponds to the + /// size set by `xdg_positioner.set_size`. + /// + /// `Rectangle::x` and `Rectangle::y` define the position of the + /// popup relative to its parent surface's `window_geometry`. + /// The position is calculated according to the rules defined + /// in the `xdg_shell` protocol. + /// The `constraint_adjustment` will not be considered by this + /// implementation and the position and size should be re-calculated + /// in the compositor if the compositor implements `constraint_adjustment` + /// + /// [`PositionerState::get_unconstrained_geometry`] does take `constraint_adjustment` into account. + pub fn get_geometry(&self) -> Rectangle { + // From the `xdg_shell` prococol specification: + // + // set_offset: + // + // Specify the surface position offset relative to the position of the + // anchor on the anchor rectangle and the anchor on the surface. For + // example if the anchor of the anchor rectangle is at (x, y), the surface + // has the gravity bottom|right, and the offset is (ox, oy), the calculated + // surface position will be (x + ox, y + oy) + let mut geometry = Rectangle::new(self.offset, self.rect_size); + + // Defines the anchor point for the anchor rectangle. The specified anchor + // is used derive an anchor point that the child surface will be + // positioned relative to. If a corner anchor is set (e.g. 'top_left' or + // 'bottom_right'), the anchor point will be at the specified corner; + // otherwise, the derived anchor point will be centered on the specified + // edge, or in the center of the anchor rectangle if no edge is specified. + geometry.loc += self.get_anchor_point(); + + // Defines in what direction a surface should be positioned, relative to + // the anchor point of the parent surface. If a corner gravity is + // specified (e.g. 'bottom_right' or 'top_left'), then the child surface + // will be placed towards the specified gravity; otherwise, the child + // surface will be centered over the anchor point on any axis that had no + // gravity specified. + if self.gravity_has_edge(xdg_positioner::Gravity::Top) { + geometry.loc.y -= geometry.size.h; + } else if !self.gravity_has_edge(xdg_positioner::Gravity::Bottom) { + geometry.loc.y -= geometry.size.h / 2; + } + + if self.gravity_has_edge(xdg_positioner::Gravity::Left) { + geometry.loc.x -= geometry.size.w; + } else if !self.gravity_has_edge(xdg_positioner::Gravity::Right) { + geometry.loc.x -= geometry.size.w / 2; + } + + geometry + } + + /// Get the geometry for a popup as defined by this positioner, after trying to fit the popup into the + /// target rectangle. + /// + /// `Rectangle::width` and `Rectangle::height` corresponds to the size set by `xdg_positioner.set_size`. + /// + /// `Rectangle::x` and `Rectangle::y` define the position of the popup relative to its parent surface's + /// `window_geometry`. The position is calculated according to the rules defined in the `xdg_shell` + /// protocol. + /// + /// This method does consider `constrain_adjustment` by trying to fit the popup into the provided target + /// rectangle. The target rectangle is in the same coordinate system as the rectangle returned by this + /// method. So, it is relative to the parent surface's geometry. + pub fn get_unconstrained_geometry(mut self, target: Rectangle) -> Rectangle { + // The protocol defines the following order for adjustments: flip, slide, resize. If the flip fails + // to remove the constraints, it is reverted. + // + // The adjustments are applied individually between axes. We can do that reasonably safely, given + // that both our target and our popup are simple rectangles. The code is grouped per adjustment for + // easier copy-paste checking, and because flips replace the geometry entirely, while further + // adjustments change individual fields. + let mut geo = self.get_geometry(); + let (mut off_left, mut off_right, mut off_top, mut off_bottom) = compute_offsets(target, geo); + + // Try to flip horizontally. + if (off_left > 0 || off_right > 0) && self.constraint_adjustment.contains(ConstraintAdjustment::FlipX) + { + let mut new = self; + new.anchor_edges = invert_anchor_x(new.anchor_edges); + new.gravity = invert_gravity_x(new.gravity); + let new_geo = new.get_geometry(); + let (new_off_left, new_off_right, _, _) = compute_offsets(target, new_geo); + + // Apply flip only if it removed the constraint. + if new_off_left <= 0 && new_off_right <= 0 { + self = new; + geo = new_geo; + off_left = 0; + off_right = 0; + // off_top and off_bottom are unchanged since we're using rectangles. + } + } + + // Try to flip vertically. + if (off_top > 0 || off_bottom > 0) && self.constraint_adjustment.contains(ConstraintAdjustment::FlipY) + { + let mut new = self; + new.anchor_edges = invert_anchor_y(new.anchor_edges); + new.gravity = invert_gravity_y(new.gravity); + let new_geo = new.get_geometry(); + let (_, _, new_off_top, new_off_bottom) = compute_offsets(target, new_geo); + + // Apply flip only if it removed the constraint. + if new_off_top <= 0 && new_off_bottom <= 0 { + self = new; + geo = new_geo; + off_top = 0; + off_bottom = 0; + // off_left and off_right are unchanged since we're using rectangles. + } + } + + // Try to slide horizontally. + if (off_left > 0 || off_right > 0) + && self.constraint_adjustment.contains(ConstraintAdjustment::SlideX) + { + // Prefer to show the top-left corner of the popup so that we can easily do a resize + // adjustment next. + if off_left > 0 { + geo.loc.x += off_left; + } else if off_right > 0 { + geo.loc.x -= min(off_right, -off_left); + } + + (off_left, off_right, _, _) = compute_offsets(target, geo); + // off_top and off_bottom are the same since we're using rectangles. + } + + // Try to slide vertically. + if (off_top > 0 || off_bottom > 0) + && self.constraint_adjustment.contains(ConstraintAdjustment::SlideY) + { + // Prefer to show the top-left corner of the popup so that we can easily do a resize + // adjustment next. + if off_top > 0 { + geo.loc.y += off_top; + } else if off_bottom > 0 { + geo.loc.y -= min(off_bottom, -off_top); + } + + (_, _, off_top, off_bottom) = compute_offsets(target, geo); + // off_left and off_right are the same since we're using rectangles. + } + + // Try to resize horizontally. + if self.constraint_adjustment.contains(ConstraintAdjustment::ResizeX) { + // Unconstrain both edges by clamping the left and right sides of the popup rectangle. + // Skip if the offset is larger than the width, in which case the entirety of the popup + // is outside the target rectangle and a clamp would result in a zero-sized geometry. + + if off_left > 0 && off_left < geo.size.w { + geo.loc.x += off_left; + geo.size.w -= off_left; + } + if off_right > 0 && off_right < geo.size.w { + geo.size.w -= off_right; + } + } + + // Try to resize vertically. + if self.constraint_adjustment.contains(ConstraintAdjustment::ResizeY) { + // Unconstrain both edges by clamping the top and bottom sides of the popup rectangle. + // Skip if the offset is larger than the height, in which case the entirety of the popup + // is outside the target rectangle and a clamp would result in a zero-sized geometry. + + if off_top > 0 && off_top < geo.size.h { + geo.loc.y += off_top; + geo.size.h -= off_top; + } + if off_bottom > 0 && off_bottom < geo.size.h { + geo.size.h -= off_bottom; + } + } + + geo + } +} + +fn compute_offsets(target: Rectangle, popup: Rectangle) -> (i32, i32, i32, i32) { + let off_left = target.loc.x - popup.loc.x; + let off_right = (popup.loc.x + popup.size.w) - (target.loc.x + target.size.w); + let off_top = target.loc.y - popup.loc.y; + let off_bottom = (popup.loc.y + popup.size.h) - (target.loc.y + target.size.h); + (off_left, off_right, off_top, off_bottom) +} + +fn invert_anchor_x(anchor: Anchor) -> Anchor { + match anchor { + Anchor::Left => Anchor::Right, + Anchor::Right => Anchor::Left, + Anchor::TopLeft => Anchor::TopRight, + Anchor::TopRight => Anchor::TopLeft, + Anchor::BottomLeft => Anchor::BottomRight, + Anchor::BottomRight => Anchor::BottomLeft, + x => x, + } +} + +fn invert_anchor_y(anchor: Anchor) -> Anchor { + match anchor { + Anchor::Top => Anchor::Bottom, + Anchor::Bottom => Anchor::Top, + Anchor::TopLeft => Anchor::BottomLeft, + Anchor::TopRight => Anchor::BottomRight, + Anchor::BottomLeft => Anchor::TopLeft, + Anchor::BottomRight => Anchor::TopRight, + x => x, + } +} + +fn invert_gravity_x(gravity: Gravity) -> Gravity { + match gravity { + Gravity::Left => Gravity::Right, + Gravity::Right => Gravity::Left, + Gravity::TopLeft => Gravity::TopRight, + Gravity::TopRight => Gravity::TopLeft, + Gravity::BottomLeft => Gravity::BottomRight, + Gravity::BottomRight => Gravity::BottomLeft, + x => x, + } +} + +fn invert_gravity_y(gravity: Gravity) -> Gravity { + match gravity { + Gravity::Top => Gravity::Bottom, + Gravity::Bottom => Gravity::Top, + Gravity::TopLeft => Gravity::BottomLeft, + Gravity::TopRight => Gravity::BottomRight, + Gravity::BottomLeft => Gravity::TopLeft, + Gravity::BottomRight => Gravity::TopRight, + x => x, + } +} + +/// State of a regular toplevel surface +#[derive(Debug, Default, PartialEq, Eq)] +pub struct ToplevelState { + /// The suggested size of the surface + pub size: Option>, + + /// The bounds for this toplevel + /// + /// The bounds can for example correspond to the size of a monitor excluding any panels or + /// other shell components, so that a surface isn't created in a way that it cannot fit. + pub bounds: Option>, + + /// The states for this surface + pub states: ToplevelStateSet, + + /// The output for a fullscreen display + pub fullscreen_output: Option, + + /// The xdg decoration mode of the surface + pub decoration_mode: Option, + + /// The wm capabilities for this toplevel + pub capabilities: WmCapabilitySet, +} + +impl Clone for ToplevelState { + fn clone(&self) -> ToplevelState { + ToplevelState { + fullscreen_output: self.fullscreen_output.clone(), + states: self.states.clone(), + size: self.size, + bounds: self.bounds, + decoration_mode: self.decoration_mode, + capabilities: self.capabilities.clone(), + } + } +} + +/// Container holding the states for a `XdgToplevel` +/// +/// This container will prevent the `XdgToplevel` from +/// having the same `xdg_toplevel::State` multiple times +/// and simplifies setting and un-setting a particularly +/// `xdg_toplevel::State` +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct ToplevelStateSet { + states: Vec, +} + +impl ToplevelStateSet { + /// Returns `true` if the states contains a state. + pub fn contains(&self, state: xdg_toplevel::State) -> bool { + self.states.contains(&state) + } + + /// Adds a state to the states. + /// + /// If the states did not have this state present, `true` is returned. + /// + /// If the states did have this state present, `false` is returned. + pub fn set(&mut self, state: xdg_toplevel::State) -> bool { + if self.contains(state) { + false + } else { + self.states.push(state); + true + } + } + + /// Removes a state from the states. Returns whether the state was + /// present in the states. + pub fn unset(&mut self, state: xdg_toplevel::State) -> bool { + if !self.contains(state) { + false + } else { + self.states.retain(|s| *s != state); + true + } + } + + /// Filter the states according to the provided version + /// of the [`XdgToplevel`] + pub(crate) fn into_filtered_states(mut self, version: u32) -> Vec { + let constrained_supported = version >= XDG_TOPLEVEL_STATE_CONSTRAINED_SINCE; + + // If constrained is supported, everything is, so we can just return + if constrained_supported { + return self.states; + } + + fn version_needed_for_state(state: &xdg_toplevel::State) -> u32 { + match state { + xdg_toplevel::State::TiledLeft + | xdg_toplevel::State::TiledRight + | xdg_toplevel::State::TiledTop + | xdg_toplevel::State::TiledBottom => XDG_TOPLEVEL_STATE_TILED_SINCE, + xdg_toplevel::State::ConstrainedLeft + | xdg_toplevel::State::ConstrainedRight + | xdg_toplevel::State::ConstrainedTop + | xdg_toplevel::State::ConstrainedBottom => XDG_TOPLEVEL_STATE_CONSTRAINED_SINCE, + xdg_toplevel::State::Suspended => XDG_TOPLEVEL_STATE_SUSPENDED_SINCE, + _ => 1, + } + } + + self.states + .retain(|state| version >= version_needed_for_state(state)); + + self.states + } +} + +impl IntoIterator for ToplevelStateSet { + type Item = xdg_toplevel::State; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.states.into_iter() + } +} + +impl From for Vec { + #[inline] + fn from(states: ToplevelStateSet) -> Self { + states.states + } +} + +/// Container holding the [`xdg_toplevel::WmCapabilities`] for a toplevel +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct WmCapabilitySet { + capabilities: HashSet, +} + +impl WmCapabilitySet { + /// Returns `true` if the set contains a capability. + pub fn contains(&self, capability: xdg_toplevel::WmCapabilities) -> bool { + self.capabilities.contains(&capability) + } + + /// Adds a capability to the set. + /// + /// If the set did not have this capability present, `true` is returned. + /// + /// If the set did have this capability present, `false` is returned. + pub fn set(&mut self, capability: xdg_toplevel::WmCapabilities) -> bool { + self.capabilities.insert(capability) + } + + /// Removes a capability from the set. Returns whether the capability was + /// present in the set. + pub fn unset(&mut self, capability: xdg_toplevel::WmCapabilities) -> bool { + self.capabilities.remove(&capability) + } + + /// Replace all capabilities in this set + pub fn replace(&mut self, capabilities: impl IntoIterator) { + self.capabilities.clear(); + self.capabilities.extend(capabilities); + } + + /// Returns the raw [`xdg_toplevel::WmCapabilities`] stored in this set + pub fn capabilities(&self) -> impl Iterator { + self.capabilities.iter() + } +} + +impl From for WmCapabilitySet +where + T: IntoIterator, +{ + #[inline] + fn from(capabilities: T) -> Self { + let capabilities = capabilities.into_iter().collect(); + Self { capabilities } + } +} + +/// Represents the client pending state +#[derive(Debug, Default, Clone, Copy)] +pub struct SurfaceCachedState { + /// Holds the double-buffered geometry that may be specified + /// by xdg_surface.set_window_geometry. + pub geometry: Option>, + /// Minimum size requested for this surface + /// + /// A value of 0 on an axis means this axis is not constrained + /// + /// This is only relevant for xdg_toplevel, and will always be + /// `(0, 0)` for xdg_popup. + pub min_size: Size, + /// Maximum size requested for this surface + /// + /// A value of 0 on an axis means this axis is not constrained + /// + /// This is only relevant for xdg_toplevel, and will always be + /// `(0, 0)` for xdg_popup. + pub max_size: Size, +} + +impl Cacheable for SurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +/// Xdg Shell handler type +#[allow(unused_variables)] +pub trait XdgShellHandler { + /// [XdgShellState] getter + fn xdg_shell_state(&mut self) -> &mut XdgShellState; + + /// A new shell client was instantiated + fn new_client(&mut self, client: ShellClient) {} + + /// The pong for a pending ping of this shell client was received + /// + /// The `ShellHandler` already checked for you that the serial matches the one + /// from the pending ping. + fn client_pong(&mut self, client: ShellClient) {} + + /// A new toplevel surface was created + /// + /// You likely need to send a [`ToplevelConfigure`] to the surface, to hint the + /// client as to how its window should be sized. + fn new_toplevel(&mut self, surface: ToplevelSurface); + + /// A new popup surface was created + /// + /// You likely need to send a [`PopupConfigure`] to the surface, to hint the + /// client as to how its popup should be sized. + /// + /// ## Arguments + /// + /// - `positioner` - The state of the positioner at the timethe popup was requested. + /// The positioner state can be used by the compositor + /// to calculate the best placement for the popup. + /// For example the compositor should prevent that a popup + /// is placed outside the visible rectangle of a output. + fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState); + + /// The client requested the start of an interactive move for this surface + fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) {} + + /// The client requested the start of an interactive resize for this surface + fn resize_request( + &mut self, + surface: ToplevelSurface, + seat: wl_seat::WlSeat, + serial: Serial, + edges: xdg_toplevel::ResizeEdge, + ) { + } + + /// This popup requests a grab of the pointer + /// + /// This means it requests to be sent a `popup_done` event when the pointer leaves + /// the grab area. + fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial); + + /// A toplevel surface requested to be maximized + fn maximize_request(&mut self, surface: ToplevelSurface) { + surface.send_configure(); + } + + /// A toplevel surface requested to stop being maximized + fn unmaximize_request(&mut self, surface: ToplevelSurface) {} + + /// A toplevel surface requested to be set fullscreen + fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option) { + surface.send_configure(); + } + + /// A toplevel surface request to stop being fullscreen + fn unfullscreen_request(&mut self, surface: ToplevelSurface) {} + + /// A toplevel surface requested to be minimized + fn minimize_request(&mut self, surface: ToplevelSurface) {} + + /// The client requests the window menu to be displayed on this surface at this location + /// + /// This menu belongs to the compositor. It is typically expected to contain options for + /// control of the window (maximize/minimize/close/move/etc...). + fn show_window_menu( + &mut self, + surface: ToplevelSurface, + seat: wl_seat::WlSeat, + serial: Serial, + location: Point, + ) { + } + + /// A surface has acknowledged a configure serial. + fn ack_configure(&mut self, surface: wl_surface::WlSurface, configure: Configure) {} + + /// A client requested a reposition, providing a new positioner for a popup. + /// + /// To confirm the new popup position, `PopupSurface::send_repositioned` must be + /// called on the provided surface with the token. + /// + /// ## Arguments + /// + /// - `positioner` - The state of the positioner at the time the reposition request was made. + /// The positioner state can be used by the compositor + /// to calculate the best placement for the popup. + /// For example the compositor should prevent that a popup + /// is placed outside the visible rectangle of a output. + /// - `token` - The passed token will be sent in the corresponding xdg_popup.repositioned event. + /// The new popup position will not take effect until the corresponding configure event + /// is acknowledged by the client. See xdg_popup.repositioned for details. + /// The token itself is opaque, and has no other special meaning. + fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32); + + /// A shell client was destroyed. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn client_destroyed(&mut self, client: ShellClient) {} + + /// A toplevel surface was destroyed. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {} + + /// A popup surface was destroyed. + /// + /// Note: Destruction might happen explicitly by the client, or implicitly + /// when the client quits. In case of implicit destruction the order the + /// callbacks are called in is undefined. + fn popup_destroyed(&mut self, surface: PopupSurface) {} + + /// The toplevel surface set a different app id. + fn app_id_changed(&mut self, surface: ToplevelSurface) {} + + /// The toplevel surface set a different title. + fn title_changed(&mut self, surface: ToplevelSurface) {} + + /// The parent of a toplevel surface has changed. + fn parent_changed(&mut self, surface: ToplevelSurface) {} +} + +/// Shell global state +/// +/// This state allows you to retrieve a list of surfaces +/// currently known to the shell global. +#[derive(Debug)] +pub struct XdgShellState { + known_toplevels: Vec, + known_popups: Vec, + default_capabilities: WmCapabilitySet, + global: GlobalId, +} + +impl XdgShellState { + /// Create a new `xdg_shell` global with all [`WmCapabilities`](xdg_toplevel::WmCapabilities) + pub fn new(display: &DisplayHandle) -> XdgShellState + where + D: GlobalDispatch + 'static, + { + Self::new_with_capabilities::( + display, + [ + xdg_toplevel::WmCapabilities::Fullscreen, + xdg_toplevel::WmCapabilities::Maximize, + xdg_toplevel::WmCapabilities::Minimize, + xdg_toplevel::WmCapabilities::WindowMenu, + ], + ) + } + + /// Create a new `xdg_shell` global with a specific set of [`WmCapabilities`](xdg_toplevel::WmCapabilities) + pub fn new_with_capabilities( + display: &DisplayHandle, + capabilities: impl Into, + ) -> XdgShellState + where + D: GlobalDispatch + 'static, + { + let global = display.create_global::(7, GlobalData); + + XdgShellState { + known_toplevels: Vec::new(), + known_popups: Vec::new(), + default_capabilities: capabilities.into(), + global, + } + } + + /// Replace the capabilities of this global + /// + /// *Note*: This does not update the capabilities on existing toplevels, only new + /// toplevels are affected. To update existing toplevels iterate over them, + /// update their capabilities and send a configure. + pub fn replace_capabilities(&mut self, capabilities: impl Into) { + self.default_capabilities = capabilities.into(); + } + + /// Access all the shell surfaces known by this handler + pub fn toplevel_surfaces(&self) -> &[ToplevelSurface] { + &self.known_toplevels + } + + /// Returns a [`ToplevelSurface`] from an underlying toplevel surface. + pub fn get_toplevel(&self, toplevel: &xdg_toplevel::XdgToplevel) -> Option { + self.known_toplevels + .iter() + .find(|surface| surface.xdg_toplevel() == toplevel) + .cloned() + } + + /// Access all the popup surfaces known by this handler + pub fn popup_surfaces(&self) -> &[PopupSurface] { + &self.known_popups + } + + /// Returns a [`PopupSurface`] from an underlying popup surface. + pub fn get_popup(&self, popup: &xdg_popup::XdgPopup) -> Option { + self.known_popups + .iter() + .find(|surface| surface.xdg_popup() == popup) + .cloned() + } + + /// Returns the xdg shell global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +#[derive(Default, Debug)] +pub(crate) struct ShellClientData { + pending_ping: Option, + data: UserDataMap, +} + +/// A shell client +/// +/// This represents an instantiation of a shell +/// global (be it `wl_shell` or `xdg_shell`). +/// +/// Most of the time, you can consider that a +/// Wayland client will be a single shell client. +/// +/// You can use this handle to access a storage for any +/// client-specific data you wish to associate with it. +#[derive(Debug)] +pub struct ShellClient { + kind: xdg_wm_base::XdgWmBase, +} + +impl std::cmp::PartialEq for ShellClient { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind + } +} + +impl ShellClient { + fn new(resource: &xdg_wm_base::XdgWmBase) -> Self { + Self { + kind: resource.clone(), + } + } + + /// Is the shell client represented by this handle still connected? + #[inline] + pub fn alive(&self) -> bool { + self.kind.alive() + } + + /// Send a ping request to this shell client + /// + /// You'll receive the reply as a [`XdgShellHandler::client_pong`] request. + /// + /// A typical use is to start a timer at the same time you send this ping + /// request, and cancel it when you receive the pong. If the timer runs + /// down to 0 before a pong is received, mark the client as unresponsive. + /// + /// Fails if this shell client already has a pending ping or is already dead. + pub fn send_ping(&self, serial: Serial) -> Result<(), PingError> { + if !self.alive() { + return Err(PingError::DeadSurface); + } + let user_data = self.kind.data::().unwrap(); + let mut guard = user_data.client_data.lock().unwrap(); + if let Some(pending_ping) = guard.pending_ping { + return Err(PingError::PingAlreadyPending(pending_ping)); + } + guard.pending_ping = Some(serial); + self.kind.ping(serial.into()); + + Ok(()) + } + + /// Kill the shell client for being unresponsive. + /// + /// Generally this will be used if the client does not respond to a ping in a reasonable amount of time. + pub fn unresponsive(&self) -> Result<(), crate::utils::DeadResource> { + if !self.alive() { + return Err(crate::utils::DeadResource); + } + self.kind.post_error( + xdg_wm_base::Error::Unresponsive, + "client did not respond to ping on time", + ); + + Ok(()) + } + + /// Access the user data associated with this shell client + pub fn with_data(&self, f: F) -> Result + where + F: FnOnce(&mut UserDataMap) -> T, + { + if !self.alive() { + return Err(crate::utils::DeadResource); + } + let data = self.kind.data::().unwrap(); + let mut guard = data.client_data.lock().unwrap(); + Ok(f(&mut guard.data)) + } +} + +/// A handle to a toplevel surface +#[derive(Debug, Clone)] +pub struct ToplevelSurface { + wl_surface: wl_surface::WlSurface, + shell_surface: xdg_toplevel::XdgToplevel, +} + +impl std::cmp::PartialEq for ToplevelSurface { + #[inline] + fn eq(&self, other: &Self) -> bool { + // self.alive() && other.alive() && + self.wl_surface == other.wl_surface + } +} + +impl ToplevelSurface { + /// Is the toplevel surface referred by this handle still alive? + #[inline] + pub fn alive(&self) -> bool { + self.wl_surface.alive() && self.shell_surface.alive() + } + + /// Supported XDG shell protocol version. + pub fn version(&self) -> u32 { + self.shell_surface.version() + } + + /// Retrieve the shell client owning this toplevel surface + pub fn client(&self) -> ShellClient { + let shell = { + let data = self + .shell_surface + .data::() + .unwrap(); + data.wm_base.clone() + }; + + ShellClient { kind: shell } + } + + /// Gets the current pending state for a configure + /// + /// Returns `Some` if either no initial configure has been sent or + /// the `server_pending` is `Some` and different from the last pending + /// configure or `last_acked` if there is no pending + /// + /// Returns `None` if either no `server_pending` or the pending + /// has already been sent to the client or the pending is equal + /// to the `last_acked` + fn get_pending_state(&self, attributes: &mut XdgToplevelSurfaceRoleAttributes) -> Option { + if !attributes.initial_configure_sent { + return Some( + attributes + .server_pending + .take() + .unwrap_or_else(|| attributes.current_server_state()), + ); + } + + // Check if the state really changed, it is possible + // that with_pending_state has been called without + // modifying the state. + if !attributes.has_pending_changes() { + return None; + } + + attributes.server_pending.take() + } + + /// Send a pending configure event to this toplevel surface to suggest it a new configuration + /// + /// If changes have occurred a configure event will be send to the clients and the serial will be returned + /// (for tracking the configure in [`XdgShellHandler::ack_configure`] if desired). + /// If no changes occurred no event will be send and `None` will be returned. + /// + /// See [`send_configure`](ToplevelSurface::send_configure) and [`has_pending_changes`](ToplevelSurface::has_pending_changes) + /// for more information. + pub fn send_pending_configure(&self) -> Option { + if self.has_pending_changes() { + Some(self.send_configure()) + } else { + None + } + } + + /// Send a configure event to this toplevel surface to suggest it a new configuration + /// + /// The serial of this configure will be tracked waiting for the client to ACK it. + /// + /// You can manipulate the state that will be sent to the client with the [`with_pending_state`](#method.with_pending_state) + /// method. + /// + /// Note: This will always send a configure event, if you intend to only send a configure event on changes take a look at + /// [`send_pending_configure`](ToplevelSurface::send_pending_configure) + pub fn send_configure(&self) -> Serial { + let shell_surface_data = self.shell_surface.data::(); + let decoration = + shell_surface_data.and_then(|data| data.decoration.lock().unwrap().as_ref().cloned()); + let (configure, decoration_mode_changed, bounds_changed, capabilities_changed) = + compositor::with_states(&self.wl_surface, |states| { + let mut attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + let pending = self + .get_pending_state(&mut attributes) + .unwrap_or_else(|| attributes.current_server_state()); + // retrieve the current state before adding it to the + // pending state so that we can compare what has changed + let current = attributes.current_server_state(); + + // test if we should send the decoration mode, either because it changed + // or we never sent it + let decoration_mode_changed = !attributes.initial_decoration_configure_sent + || (pending.decoration_mode != current.decoration_mode); + + // test if we should send a bounds configure event, either because the + // bounds changed or we never sent one + let bounds_changed = !attributes.initial_configure_sent || (pending.bounds != current.bounds); + + // test if we should send a capabilities event, either because the + // capabilities changed or we never sent one + let capabilities_changed = + !attributes.initial_configure_sent || (pending.capabilities != current.capabilities); + + let configure = ToplevelConfigure { + serial: SERIAL_COUNTER.next_serial(), + state: pending, + }; + + attributes.pending_configures.push(configure.clone()); + attributes.initial_configure_sent = true; + if decoration.is_some() { + attributes.initial_decoration_configure_sent = true; + } + + ( + configure, + decoration_mode_changed, + bounds_changed, + capabilities_changed, + ) + }); + + if decoration_mode_changed { + if let Some(decoration) = &decoration { + self::decoration::send_decoration_configure( + decoration, + configure + .state + .decoration_mode + .unwrap_or(zxdg_toplevel_decoration_v1::Mode::ClientSide), + ); + } + } + + let serial = configure.serial; + self::handlers::send_toplevel_configure( + &self.shell_surface, + configure, + bounds_changed, + capabilities_changed, + ); + + serial + } + + /// Did the surface sent the initial + /// configure event to the client. + /// + /// Calls [`compositor::with_states`] internally. + pub fn is_initial_configure_sent(&self) -> bool { + compositor::with_states(&self.wl_surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }) + } + + /// Handles the role specific commit error checking + /// + /// This should be called when the underlying WlSurface + /// handles a wl_surface.commit request. + pub(crate) fn pre_commit_hook( + state: &mut D, + _dh: &DisplayHandle, + surface: &wl_surface::WlSurface, + ) { + let _span = trace_span!("xdg-toplevel pre-commit", surface = %surface.id()).entered(); + + let error = compositor::with_states(surface, |states| { + let mut role = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + let mut guard_toplevel = states.cached_state.get::(); + let pending = guard_toplevel.pending(); + + // The presence of last_acked always follows the buffer assignment because the + // surface is not allowed to attach a buffer without acking the initial configure. + let had_buffer_before = pending.last_acked.is_some(); + + let mut guard_surface = states.cached_state.get::(); + let has_buffer = match &guard_surface.pending().buffer { + Some(BufferAssignment::NewBuffer(_)) => true, + Some(BufferAssignment::Removed) => false, + None => had_buffer_before, + }; + // Need to check had_buffer_before in case the client attaches a null buffer for the + // initial commit---we don't want to consider that as "got unmapped" and reset role. + let got_unmapped = had_buffer_before && !has_buffer; + + if has_buffer { + let Some(last_acked) = role.last_acked.clone() else { + return Some(( + xdg_surface::Error::UnconfiguredBuffer, + "must ack the initial configure before attaching buffer", + )); + }; + + // The surface remains, or became mapped, track the last acked state. + pending.last_acked = Some(last_acked); + } else { + // The surface remains, or became, unmapped, meaning that it's in the initial + // configure stage. + pending.last_acked = None; + } + + if got_unmapped { + trace!( + "got unmapped; resetting role and cached state; have {} pending configures", + role.pending_configures.len() + ); + + // All attributes are discarded when an xdg_surface is unmapped. Though, we keep + // the list of pending configures because there's no way for a surface to tell + // an in-flight configure apart from our next initial configure after unmapping. + // + // This can be reproduced with wleird-unmap if, say, and Activated configure is + // timed right. + let pending_configures = std::mem::take(&mut role.pending_configures); + *role = Default::default(); + role.pending_configures = pending_configures; + *guard_toplevel.pending() = Default::default(); + + // Don't forget to prepare the default capabilities though. + let default_capabilities = &state.xdg_shell_state().default_capabilities; + role.server_pending = Some(Default::default()); + let current_capabilities = &mut role.server_pending.as_mut().unwrap().capabilities; + current_capabilities.replace(default_capabilities.capabilities.iter().copied()); + } + + None + }); + + if let Some((error, msg)) = error { + // The surface attached a buffer without acking the initial configure. + let toplevels = &state.xdg_shell_state().known_toplevels; + if let Some(toplevel) = toplevels.iter().find(|handle| handle.wl_surface == *surface) { + let data = toplevel + .shell_surface + .data::() + .unwrap(); + data.xdg_surface.post_error(error, msg); + } else { + error!("surface missing from known toplevels"); + } + } + } + + /// Send a "close" event to the client + pub fn send_close(&self) { + self.shell_surface.close() + } + + /// Access the underlying `wl_surface` of this toplevel surface + #[inline] + pub fn wl_surface(&self) -> &wl_surface::WlSurface { + &self.wl_surface + } + + /// Access the underlying `xdg_toplevel` of this toplevel surface + pub fn xdg_toplevel(&self) -> &xdg_toplevel::XdgToplevel { + &self.shell_surface + } + + /// Allows the pending state of this toplevel to + /// be manipulated. + /// + /// This should be used to inform the client about size and state changes, + /// for example after a resize request from the client. + /// + /// The state will be sent to the client when calling [`send_configure`](#method.send_configure). + pub fn with_pending_state(&self, f: F) -> T + where + F: FnOnce(&mut ToplevelState) -> T, + { + compositor::with_states(&self.wl_surface, |states| { + let mut attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + if attributes.server_pending.is_none() { + attributes.server_pending = Some(attributes.current_server_state()); + } + + let server_pending = attributes.server_pending.as_mut().unwrap(); + f(server_pending) + }) + } + + /// Tests this [`ToplevelSurface`] for pending changes + /// + /// Returns `true` if [`with_pending_state`](ToplevelSurface::with_pending_state) was used to manipulate the state + /// and resulted in a different state or if the initial configure is still pending. + pub fn has_pending_changes(&self) -> bool { + compositor::with_states(&self.wl_surface, |states| { + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + !attributes.initial_configure_sent || attributes.has_pending_changes() + }) + } + + /// Provides access to the current committed cached state. + pub fn with_cached_state(&self, f: F) -> T + where + F: FnOnce(&ToplevelCachedState) -> T, + { + compositor::with_states(&self.wl_surface, |states| { + let mut guard = states.cached_state.get::(); + f(guard.current()) + }) + } + + /// Provides access to the current committed state. + /// + /// This is the state that the client last acked before making the current commit. + pub fn with_committed_state(&self, f: F) -> T + where + F: FnOnce(Option<&ToplevelState>) -> T, + { + self.with_cached_state(move |state| f(state.last_acked.as_ref().map(|c| &c.state))) + } + + /// Returns the parent of this toplevel surface. + pub fn parent(&self) -> Option { + handlers::get_parent(&self.shell_surface) + } +} + +/// Represents the possible errors that +/// can be returned from [`PopupSurface::send_configure`] +#[derive(Debug, thiserror::Error)] +pub enum PopupConfigureError { + /// The popup has already been configured and the + /// protocol version forbids the popup to + /// be re-configured + #[error("The popup has already been configured")] + AlreadyConfigured, + /// The popup is not allowed to be re-configured, + /// the positioner is not reactive + #[error("The popup positioner is not reactive")] + NotReactive, +} + +/// A handle to a popup surface +/// +/// This is an unified abstraction over the popup surfaces +/// of both `wl_shell` and `xdg_shell`. +#[derive(Debug, Clone)] +pub struct PopupSurface { + wl_surface: wl_surface::WlSurface, + shell_surface: xdg_popup::XdgPopup, +} + +impl std::cmp::PartialEq for PopupSurface { + #[inline] + fn eq(&self, other: &Self) -> bool { + // self.alive() && other.alive() && + self.wl_surface == other.wl_surface + } +} + +impl PopupSurface { + /// Is the toplevel surface referred by this handle still alive? + #[inline] + pub fn alive(&self) -> bool { + self.wl_surface.alive() && self.shell_surface.alive() + } + + /// Gets a reference of the parent WlSurface of + /// this popup. + pub fn get_parent_surface(&self) -> Option { + compositor::with_states(&self.wl_surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .parent + .clone() + }) + } + + /// Retrieve the shell client owning this popup surface + pub fn client(&self) -> ShellClient { + let shell = { + let data = self + .shell_surface + .data::() + .unwrap(); + data.wm_base.clone() + }; + + ShellClient { kind: shell } + } + + /// Get the version of the popup resource + fn version(&self) -> u32 { + self.shell_surface.version() + } + + /// Internal configure function to re-use the configure + /// logic for both [`XdgRequest::send_configure`] and [`XdgRequest::send_repositioned`] + fn send_configure_internal(&self, reposition_token: Option) -> Serial { + let configure = compositor::with_states(&self.wl_surface, |states| { + let mut attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + let pending = attributes + .server_pending + .take() + .unwrap_or_else(|| attributes.current_server_state()); + + let configure = PopupConfigure { + state: pending, + serial: SERIAL_COUNTER.next_serial(), + reposition_token, + }; + + attributes.pending_configures.push(configure); + attributes.initial_configure_sent = true; + + configure + }); + let serial = configure.serial; + self::handlers::send_popup_configure(&self.shell_surface, configure); + serial + } + + /// Send a pending configure event to this popup surface to suggest it a new configuration + /// + /// If changes have occurred a configure event will be send to the clients and the serial will be returned + /// (for tracking the configure in [`XdgShellHandler::ack_configure`] if desired). + /// If no changes occurred no event will be send and `Ok(None)` will be returned. + /// + /// See [`send_configure`](PopupSurface::send_configure) and [`has_pending_changes`](PopupSurface::has_pending_changes) + /// for more information. + pub fn send_pending_configure(&self) -> Result, PopupConfigureError> { + if self.has_pending_changes() { + self.send_configure().map(Some) + } else { + Ok(None) + } + } + + /// Send a configure event to this popup surface to suggest it a new configuration + /// + /// The serial of this configure will be tracked waiting for the client to ACK it. + /// + /// You can manipulate the state that will be sent to the client with the [`with_pending_state`](#method.with_pending_state) + /// method. + /// + /// Returns `Err(`[`PopupConfigureError`]`)` if the initial configure has already been sent and + /// the client protocol version disallows a re-configure or the current [`PositionerState`] + /// is not reactive. + /// + /// Note: This will always send a configure event, if you intend to only send a configure event on changes take a look at + /// [`send_pending_configure`](PopupSurface::send_pending_configure) + pub fn send_configure(&self) -> Result { + // Check if we are allowed to send a configure + compositor::with_states(&self.wl_surface, |states| { + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + if attributes.initial_configure_sent && self.version() < xdg_popup::EVT_REPOSITIONED_SINCE { + // Return error, initial configure already sent and client + // does not support re-configure + return Err(PopupConfigureError::AlreadyConfigured); + } + + let mut cached = states.cached_state.get::(); + let is_reactive = cached + .current() + .last_acked + .as_ref() + .is_some_and(|c| c.state.positioner.reactive); + + if attributes.initial_configure_sent && !is_reactive { + // Return error, the positioner does not allow re-configure + return Err(PopupConfigureError::NotReactive); + } + + Ok(()) + })?; + + let serial = self.send_configure_internal(None); + + Ok(serial) + } + + /// Did the surface sent the initial + /// configure event to the client. + /// + /// Calls [`compositor::with_states`] internally. + pub fn is_initial_configure_sent(&self) -> bool { + compositor::with_states(&self.wl_surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }) + } + + /// Send a configure event, including the `repositioned` event to the client + /// in response to a `reposition` request. + /// + /// For further information see [`send_configure`](#method.send_configure) + pub fn send_repositioned(&self, token: u32) -> Serial { + self.send_configure_internal(Some(token)) + } + + /// Handles the role specific commit error checking + /// + /// This should be called when the underlying WlSurface + /// handles a wl_surface.commit request. + pub(crate) fn pre_commit_hook( + state: &mut D, + _dh: &DisplayHandle, + surface: &wl_surface::WlSurface, + ) { + let _span = trace_span!("xdg-popup pre-commit", surface = %surface.id()).entered(); + + let res = compositor::with_states(surface, |states| { + let mut role = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + if role.parent.is_none() { + return Err(( + xdg_surface::Error::NotConstructed, + "xdg_popup must have parent before mapping", + )); + } + + let mut guard_popup = states.cached_state.get::(); + let pending = guard_popup.pending(); + + // The presence of last_acked always follows the buffer assignment because the + // surface is not allowed to attach a buffer without acking the initial configure. + let had_buffer_before = pending.last_acked.is_some(); + + let grab = role.requested_grab.take(); + if grab.is_some() && had_buffer_before { + let popups = &state.xdg_shell_state().known_popups; + if let Some(popup) = popups.iter().find(|handle| handle.wl_surface == *surface) { + popup + .shell_surface + .post_error(xdg_popup::Error::InvalidGrab, "tried to grab after being mapped"); + } else { + error!("surface missing from known popups"); + } + return Ok(None); + } + + let mut guard_surface = states.cached_state.get::(); + let has_buffer = match &guard_surface.pending().buffer { + Some(BufferAssignment::NewBuffer(_)) => true, + Some(BufferAssignment::Removed) => false, + None => had_buffer_before, + }; + // Need to check had_buffer_before in case the client attaches a null buffer for the + // initial commit---we don't want to consider that as "got unmapped" and reset role. + let got_unmapped = had_buffer_before && !has_buffer; + + if has_buffer { + let Some(last_acked) = role.last_acked else { + return Err(( + xdg_surface::Error::UnconfiguredBuffer, + "must ack the initial configure before attaching buffer", + )); + }; + + // The surface remains, or became mapped, track the last acked state. + pending.last_acked = Some(last_acked); + } else { + // The surface remains, or became, unmapped, meaning that it's in the initial + // configure stage. + pending.last_acked = None; + } + + if got_unmapped { + trace!( + "got unmapped; resetting role and cached state; have {} pending configures", + role.pending_configures.len() + ); + + // All attributes are discarded when an xdg_surface is unmapped. Though, we keep + // the list of pending configures because there's no way for a surface to tell + // an in-flight configure apart from our next initial configure after unmapping. + let pending_configures = std::mem::take(&mut role.pending_configures); + *role = Default::default(); + role.pending_configures = pending_configures; + *guard_popup.pending() = Default::default(); + } + + Ok(grab) + }); + + match res { + Ok(Some((seat, serial))) => { + let popups = &state.xdg_shell_state().known_popups; + if let Some(popup) = popups.iter().find(|handle| handle.wl_surface == *surface) { + let popup = popup.clone(); + XdgShellHandler::grab(state, popup, seat, serial); + } else { + error!("surface missing from known popups"); + } + } + Ok(None) => (), + Err((error, msg)) => { + let popups = &state.xdg_shell_state().known_popups; + if let Some(popup) = popups.iter().find(|handle| handle.wl_surface == *surface) { + let data = popup + .shell_surface + .data::() + .unwrap(); + data.xdg_surface.post_error(error, msg); + } else { + error!("surface missing from known popups"); + } + } + } + } + + /// Send a `popup_done` event to the popup surface + /// + /// It means that the use has dismissed the popup surface, or that + /// the pointer has left the area of popup grab if there was a grab. + pub fn send_popup_done(&self) { + self.shell_surface.popup_done(); + } + + /// Access the underlying `wl_surface` of this popup surface + #[inline] + pub fn wl_surface(&self) -> &wl_surface::WlSurface { + &self.wl_surface + } + + /// Access the underlying `xdg_popup` of this popup surface + pub fn xdg_popup(&self) -> &xdg_popup::XdgPopup { + &self.shell_surface + } + + /// Allows the pending state of this popup to + /// be manipulated. + /// + /// This should be used to inform the client about size and position changes, + /// for example after a move of the parent toplevel. + /// + /// The state will be sent to the client when calling [`send_configure`](#method.send_configure). + pub fn with_pending_state(&self, f: F) -> T + where + F: FnOnce(&mut PopupState) -> T, + { + compositor::with_states(&self.wl_surface, |states| { + let mut attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + if attributes.server_pending.is_none() { + attributes.server_pending = Some(attributes.current_server_state()); + } + + let server_pending = attributes.server_pending.as_mut().unwrap(); + f(server_pending) + }) + } + + /// Tests this [`PopupSurface`] for pending changes + /// + /// Returns `true` if [`with_pending_state`](PopupSurface::with_pending_state) was used to manipulate the state + /// and resulted in a different state or if the initial configure is still pending. + pub fn has_pending_changes(&self) -> bool { + compositor::with_states(&self.wl_surface, |states| { + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + !attributes.initial_configure_sent || attributes.has_pending_changes() + }) + } + + /// Provides access to the current committed cached state. + pub fn with_cached_state(&self, f: F) -> T + where + F: FnOnce(&PopupCachedState) -> T, + { + compositor::with_states(&self.wl_surface, |states| { + let mut guard = states.cached_state.get::(); + f(guard.current()) + }) + } + + /// Provides access to the current committed state. + /// + /// This is the state that the client last acked before making the current commit. + pub fn with_committed_state(&self, f: F) -> T + where + F: FnOnce(Option<&PopupState>) -> T, + { + self.with_cached_state(move |state| f(state.last_acked.as_ref().map(|c| &c.state))) + } +} + +/// Defines the possible configure variants +/// for a XdgSurface that will be issued in +/// the user_impl for notifying about a ack_configure +#[derive(Debug)] +pub enum Configure { + /// A xdg_surface with a role of xdg_toplevel + /// has processed an ack_configure request + Toplevel(ToplevelConfigure), + /// A xdg_surface with a role of xdg_popup + /// has processed an ack_configure request + Popup(PopupConfigure), +} + +impl From for Configure { + #[inline] + fn from(configure: ToplevelConfigure) -> Self { + Configure::Toplevel(configure) + } +} + +impl From for Configure { + #[inline] + fn from(configure: PopupConfigure) -> Self { + Configure::Popup(configure) + } +} diff --git a/vendor/smithay/src/wayland/shm/handlers.rs b/vendor/smithay/src/wayland/shm/handlers.rs new file mode 100644 index 0000000..7de3c66 --- /dev/null +++ b/vendor/smithay/src/wayland/shm/handlers.rs @@ -0,0 +1,242 @@ +use crate::wayland::{ + Dispatch2, GlobalData, GlobalDispatch2, + buffer::BufferHandler, + shm::{ShmBufferUserData, wl_bytes_per_pixel}, +}; + +use super::{ + BufferData, ShmHandler, ShmPoolUserData, + pool::{Pool, ResizeError}, +}; + +use std::{num::NonZeroUsize, os::unix::io::AsRawFd, sync::Arc}; +use wayland_server::{ + DataInit, Dispatch, DisplayHandle, New, Resource, WEnum, + backend::ClientId, + protocol::{ + wl_buffer, + wl_shm::{self, WlShm}, + wl_shm_pool::{self, WlShmPool}, + }, +}; + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: ShmHandler, + D: 'static, +{ + fn bind( + &self, + state: &mut D, + _dh: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let shm = data_init.init(resource, GlobalData); + + // send the formats + for &f in &state.shm_state().formats { + shm.format(f); + } + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch + ShmHandler + 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + shm: &WlShm, + request: wl_shm::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + use wl_shm::{Error, Request}; + + let (pool, fd, size) = match request { + Request::CreatePool { id: pool, fd, size } => (pool, fd, size), + Request::Release => return, + _ => unreachable!(), + }; + + if size <= 0 { + shm.post_error(Error::InvalidStride, "invalid wl_shm_pool size"); + return; + } + + let mmap_pool = match Pool::new(fd, NonZeroUsize::try_from(size as usize).unwrap()) { + Ok(p) => p, + Err(fd) => { + shm.post_error( + wl_shm::Error::InvalidFd, + format!("Failed to mmap fd {}", fd.as_raw_fd()), + ); + return; + } + }; + + data_init.init( + pool, + ShmPoolUserData { + inner: Arc::new(mmap_pool), + }, + ); + } +} + +/* + * wl_shm_pool + */ + +impl Dispatch2 for ShmPoolUserData +where + D: Dispatch + BufferHandler + ShmHandler + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + pool: &WlShmPool, + request: wl_shm_pool::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + use self::wl_shm_pool::Request; + + let arc_pool = &self.inner; + + match request { + Request::CreateBuffer { + id: buffer, + offset, + width, + height, + stride, + format, + } => { + // Validate client parameters + let message = if offset < 0 { + Some("offset must not be negative".to_string()) + } else if width <= 0 || height <= 0 { + Some(format!("invalid width or height ({width}x{height})")) + } else if stride.checked_div(wl_bytes_per_pixel(format)).unwrap_or(0) < width { + // stride is in bytes... + Some(format!( + "width must not be larger than stride (width {width}, stride {})", + stride.checked_div(wl_bytes_per_pixel(format)).unwrap_or(0) + )) + } else if (i32::MAX / stride) < height { + Some(format!( + "height is too large for stride (max {})", + i32::MAX / stride + )) + } else if offset > arc_pool.size() as i32 - (stride * height) { + Some("offset is too large".to_string()) + } else { + None + }; + + if let Some(message) = message { + pool.post_error(wl_shm::Error::InvalidStride, message); + return; + } + + match format { + WEnum::Value(format) => { + if !state.shm_state().formats.contains(&format) { + pool.post_error( + wl_shm::Error::InvalidFormat, + format!("format {format:?} not supported"), + ); + + return; + } + + let data = ShmBufferUserData { + pool: arc_pool.clone(), + data: BufferData { + offset, + width, + height, + stride, + format, + }, + destruction_hooks: Default::default(), + }; + + data_init.init(buffer, data); + } + + WEnum::Unknown(unknown) => { + pool.post_error( + wl_shm::Error::InvalidFormat, + format!("unknown format 0x{unknown:x}"), + ); + } + } + } + + Request::Resize { size } => { + if size <= 0 { + pool.post_error(wl_shm::Error::InvalidFd, "invalid wl_shm_pool size"); + } + + if let Err(err) = arc_pool.resize(NonZeroUsize::try_from(size as usize).unwrap()) { + match err { + ResizeError::InvalidSize => { + pool.post_error(wl_shm::Error::InvalidFd, "cannot shrink wl_shm_pool"); + } + + ResizeError::MremapFailed => { + pool.post_error(wl_shm::Error::InvalidFd, "mremap failed"); + } + } + } + } + + Request::Destroy => {} + + _ => unreachable!(), + } + } +} + +impl Dispatch2 for ShmBufferUserData +where + D: BufferHandler, + D: 'static, +{ + fn request( + &self, + _data: &mut D, + _client: &wayland_server::Client, + _buffer: &wl_buffer::WlBuffer, + request: wl_buffer::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_buffer::Request::Destroy => { + // Handled in the destroyed callback. + } + + _ => unreachable!(), + } + } + + fn destroyed(&self, data: &mut D, _client: ClientId, buffer: &wl_buffer::WlBuffer) { + // Clone to drop the mutex guard + let destruction_hooks = self.destruction_hooks.lock().unwrap().clone(); + for hook in destruction_hooks.iter() { + (hook.cb)(data, buffer); + } + + data.buffer_destroyed(buffer); + } +} diff --git a/vendor/smithay/src/wayland/shm/mod.rs b/vendor/smithay/src/wayland/shm/mod.rs new file mode 100644 index 0000000..aee6f96 --- /dev/null +++ b/vendor/smithay/src/wayland/shm/mod.rs @@ -0,0 +1,501 @@ +//! SHM handling helpers +//! +//! This module provides helpers to handle SHM-based buffers from wayland clients. +//! +//! SHM (Shared Memory) is the most basic way wayland clients can send content to +//! the compositor: by sending a file descriptor to some (likely RAM-backed) storage +//! containing the actual data. This helper handles for you most of the logic for +//! handling these file descriptor and accessing their contents as simple `&[u8]` slices. +//! +//! This module is heavily inspired from +//! [the similar helpers](https://cgit.freedesktop.org/wayland/wayland/tree/src/wayland-shm.c) +//! of the wayland C libraries. +//! +//! To use it, first add a `ShmGlobal` to your display, specifying the formats +//! you want to support (ARGB8888 and XRGB8888 are always considered as supported, +//! as specified by the wayland protocol). +//! +//! ``` +//! extern crate wayland_server; +//! extern crate smithay; +//! +//! use smithay::wayland::buffer::BufferHandler; +//! use smithay::wayland::shm::{ShmState, ShmHandler}; +//! use wayland_server::protocol::wl_shm::Format; +//! +//! # struct State { shm_state: ShmState }; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the ShmState. +//! // Here, we specify that Yuyv and C8 format are supported +//! // additionally to the standard Argb8888 and Xrgb8888. +//! let state = ShmState::new::( +//! &display.handle(), +//! vec![Format::Yuyv, Format::C8], +//! ); +//! +//! // insert the shmstate into your compositor state. +//! // .. +//! +//! // provide the necessary trait implementations +//! impl BufferHandler for State { +//! fn buffer_destroyed(&mut self, buffer: &wayland_server::protocol::wl_buffer::WlBuffer) { +//! // All renderers can handle buffer destruction at this point. +//! // Some parts of window management may also use this function. +//! } +//! } +//! impl ShmHandler for State { +//! fn shm_state(&self) -> &ShmState { +//! &self.shm_state +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` +//! +//! Then, when you have a [`WlBuffer`](wayland_server::protocol::wl_buffer::WlBuffer) +//! and need to retrieve its contents, use the +//! [`with_buffer_contents`] function to do it: +//! +//! ``` +//! # extern crate wayland_server; +//! # extern crate smithay; +//! # fn wrap(buffer: &wayland_server::protocol::wl_buffer::WlBuffer) { +//! use smithay::wayland::shm::{with_buffer_contents, BufferData, BufferAccessError}; +//! +//! let content = with_buffer_contents(&buffer, +//! |ptr: *const u8, len: usize, buffer_metadata: BufferData| { +//! // do something to extract the contents of the buffer +//! } +//! ); +//! +//! match content { +//! Ok(something) => { +//! /* `something` is the value you returned from the closure */ +//! }, +//! Err(BufferAccessError::NotManaged) => { +//! /* This buffer is not managed by the SHM global, but by something else */ +//! }, +//! Err(BufferAccessError::BadMap) => { +//! /* The client supplied invalid content specification for this buffer, +//! and was killed. +//! */ +//! }, +//! Err(BufferAccessError::NotReadable) => { +//! /* The client has not allowed reads to this buffer */ +//! }, +//! Err(BufferAccessError::NotWritable) => unreachable!("cannot be triggered by with_buffer_contents"), +//! } +//! # } +//! ``` +//! +//! **Note** +//! +//! This handler makes itself safe regarding the client providing a wrong size for the memory pool +//! by using a SIGBUS handler. +//! +//! If you are already using an handler for this signal, you probably don't want to use this handler. + +use std::{ + any::Any, + collections::HashSet, + sync::{Arc, Mutex}, +}; + +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, Resource, WEnum, + backend::GlobalId, + protocol::{ + wl_buffer, + wl_shm::{self, WlShm}, + wl_shm_pool::WlShmPool, + }, +}; + +mod handlers; +mod pool; + +use crate::{ + backend::allocator::format::get_bpp, + utils::{HookId, UnmanagedResource, hook::Hook}, + wayland::GlobalData, +}; + +use self::pool::Pool; + +use super::buffer::BufferHandler; + +/// State of SHM module +#[derive(Debug)] +pub struct ShmState { + formats: HashSet, + shm: GlobalId, +} + +impl ShmState { + /// Create a new SHM global advertizing the given formats. + /// + /// This global will always advertize `ARGB8888` and `XRGB8888` since these formats are required by the + /// protocol. Formats given as an argument are also advertized. + /// + /// The global is directly created on the provided [`Display`](wayland_server::Display), + /// and this function returns the a delegate type. The id provided by [`ShmState::global`] may be used to + /// remove this global in the future. + pub fn new(display: &DisplayHandle, formats: impl IntoIterator) -> ShmState + where + D: GlobalDispatch + + Dispatch + + Dispatch + + BufferHandler + + ShmHandler + + 'static, + { + let mut formats = formats.into_iter().collect::>(); + + // Mandatory formats + formats.insert(wl_shm::Format::Argb8888); + formats.insert(wl_shm::Format::Xrgb8888); + + let shm = display.create_global::(2, GlobalData); + + ShmState { formats, shm } + } + + /// Returns the id of the [`WlShm`] global. + pub fn global(&self) -> GlobalId { + self.shm.clone() + } + + /// Updates the list of formats advertised by the global. + /// + /// This will only affect new binds to the wl_shm global. + /// + /// Removing formats will cause old clients trying to create + /// a buffer of a now unsupported format to be killed. + /// + /// This function will never remove the mandatory formats `ARGB8888` and `XRGB8888`. + pub fn update_formats(&mut self, formats: impl IntoIterator) { + self.formats = formats.into_iter().collect::>(); + // Mandatory formats + self.formats.insert(wl_shm::Format::Argb8888); + self.formats.insert(wl_shm::Format::Xrgb8888); + } +} + +/// Shm global handler +pub trait ShmHandler { + /// Return the Shm global state + fn shm_state(&self) -> &ShmState; +} + +/// Error that can occur when accessing an SHM buffer +#[derive(Debug, thiserror::Error)] +pub enum BufferAccessError { + /// This buffer is not managed by the SHM handler + #[error("non-SHM buffer")] + NotManaged, + + /// An error occurred while accessing the memory map + /// + /// This can happen if the client advertized a wrong size + /// for the memory map. + /// + /// If this error occurs, the client has been killed as a result. + #[error("invalid client buffer")] + BadMap, + + /// This buffer cannot be read by the compositor + #[error("Client has not indicated read permission for the buffer")] + NotReadable, + + /// This buffer cannot be written to by the compositor + #[error("Client has not indicated write permission for the buffer")] + NotWritable, +} + +impl From for BufferAccessError { + #[inline] + fn from(_: UnmanagedResource) -> Self { + Self::NotManaged + } +} + +/// Call given closure with the contents of the given buffer +/// +/// If the buffer is managed by the provided `ShmGlobal`, its contents are +/// extracted and the closure is extracted with them: +/// +/// - The first argument is a pointer to the contents of the pool +/// - The second argument is the length of the contents of the pool +/// - The third argument is the specification of this buffer is this pool +/// +/// The pool cannot be provided as a slice since it is shared memory that could be mutated +/// by the client. +/// +/// If the buffer is not managed by the provided `ShmGlobal`, the closure is not called +/// and this method will return `Err(BufferAccessError::NotManaged)` (this will be the case for an +/// EGL buffer for example). +/// +/// # Safety +/// The pointer passed to the callback is only valid while the callback is running. The shared +/// memory may be mutated by the client at any time. Creating a reference or slice into this memory +/// is undefined behavior if it is mutated by the client while the reference exists. +pub fn with_buffer_contents(buffer: &wl_buffer::WlBuffer, f: F) -> Result +where + F: FnOnce(*const u8, usize, BufferData) -> T, +{ + let data = buffer + .data::() + .ok_or(BufferAccessError::NotManaged)?; + + match data.pool.with_data(|ptr, len| f(ptr, len, data.data)) { + Ok(t) => Ok(t), + Err(()) => { + // SIGBUS error occurred + buffer.post_error(wl_shm::Error::InvalidFd, "Bad pool size."); + Err(BufferAccessError::BadMap) + } + } +} + +/// Call given closure with the contents of the given buffer for mutable access +/// +/// If the buffer is managed by the provided `ShmGlobal`, its contents are +/// extracted and the closure is extracted with them: +/// +/// - The first argument is a pointer to the contents of the pool +/// - The second argument is the length of the contents of the pool +/// - The third argument is the specification of this buffer is this pool +/// +/// The pool cannot be provided as a slice since it is shared memory that could be mutated +/// by the client. +/// +/// If the buffer is not managed by the provided `ShmGlobal`, the closure is not called +/// and this method will return `Err(BufferAccessError::NotManaged)` (this will be the case for an +/// EGL buffer for example). +/// +/// # Safety +/// The pointer passed to the callback is only valid while the callback is running. The shared +/// memory may be mutated by the client at any time. Creating a reference or slice into this memory +/// is undefined behavior if it is mutated by the client while the reference exists. +pub fn with_buffer_contents_mut(buffer: &wl_buffer::WlBuffer, f: F) -> Result +where + F: FnOnce(*mut u8, usize, BufferData) -> T, +{ + let data = buffer + .data::() + .ok_or(BufferAccessError::NotManaged)?; + + match data.pool.with_data_mut(|ptr, len| f(ptr, len, data.data)) { + Ok(t) => Ok(t), + Err(()) => { + // SIGBUS error occurred + buffer.post_error(wl_shm::Error::InvalidFd, "Bad pool size."); + Err(BufferAccessError::BadMap) + } + } +} + +/// Returns the bpp of the format +/// +/// Note: This will return 0 for formats that don't have a specified width. +pub fn wl_bytes_per_pixel(format: WEnum) -> i32 { + match format { + WEnum::Value(f) => { + shm_format_to_fourcc(f).map_or(0, |fourcc| get_bpp(fourcc).map_or(0, |bpp| bpp / 8)) + } + WEnum::Unknown(_) => 0, + } + .try_into() + .unwrap() +} + +macro_rules! shm_format_table { + ( + $( + $fourcc: ident => $shm: ident + ),* $(,)? + ) => { + /// Convert from a [`Fourcc`](crate::backend::allocator::Fourcc) format to a wl_shm format + pub const fn fourcc_to_shm_format(value: $crate::backend::allocator::Fourcc) -> Option<$crate::reexports::wayland_server::protocol::wl_shm::Format> { + match value { + $( + $crate::backend::allocator::Fourcc::$fourcc => Some($crate::reexports::wayland_server::protocol::wl_shm::Format::$shm), + )* + _ => None, + } + } + + /// Convert from a wl_shm format to a [`Fourcc`](crate::backend::allocator::Fourcc) format + pub const fn shm_format_to_fourcc(value: $crate::reexports::wayland_server::protocol::wl_shm::Format) -> Option<$crate::backend::allocator::Fourcc> { + match value { + $( + $crate::reexports::wayland_server::protocol::wl_shm::Format::$shm => Some($crate::backend::allocator::Fourcc::$fourcc), + )* + _ => None, + } + } + } +} + +shm_format_table! { + Argb8888 => Argb8888, + Xrgb8888 => Xrgb8888, + C8 => C8, + Rgb332 => Rgb332, + Bgr233 => Bgr233, + Xrgb4444 => Xrgb4444, + Xbgr4444 => Xbgr4444, + Rgbx4444 => Rgbx4444, + Bgrx4444 => Bgrx4444, + Argb4444 => Argb4444, + Abgr4444 => Abgr4444, + Rgba4444 => Rgba4444, + Bgra4444 => Bgra4444, + Xrgb1555 => Xrgb1555, + Xbgr1555 => Xbgr1555, + Rgbx5551 => Rgbx5551, + Bgrx5551 => Bgrx5551, + Argb1555 => Argb1555, + Abgr1555 => Abgr1555, + Rgba5551 => Rgba5551, + Bgra5551 => Bgra5551, + Rgb565 => Rgb565, + Bgr565 => Bgr565, + Rgb888 => Rgb888, + Bgr888 => Bgr888, + Xbgr8888 => Xbgr8888, + Rgbx8888 => Rgbx8888, + Bgrx8888 => Bgrx8888, + Abgr8888 => Abgr8888, + Rgba8888 => Rgba8888, + Bgra8888 => Bgra8888, + Xrgb2101010 => Xrgb2101010, + Xbgr2101010 => Xbgr2101010, + Rgbx1010102 => Rgbx1010102, + Bgrx1010102 => Bgrx1010102, + Argb2101010 => Argb2101010, + Abgr2101010 => Abgr2101010, + Rgba1010102 => Rgba1010102, + Bgra1010102 => Bgra1010102, + Yuyv => Yuyv, + Yvyu => Yvyu, + Uyvy => Uyvy, + Vyuy => Vyuy, + Ayuv => Ayuv, + Nv12 => Nv12, + Nv21 => Nv21, + Nv16 => Nv16, + Nv61 => Nv61, + Yuv410 => Yuv410, + Yvu410 => Yvu410, + Yuv411 => Yuv411, + Yvu411 => Yvu411, + Yuv420 => Yuv420, + Yvu420 => Yvu420, + Yuv422 => Yuv422, + Yvu422 => Yvu422, + Yuv444 => Yuv444, + Yvu444 => Yvu444, + R8 => R8, + R16 => R16, + Rg88 => Rg88, + Gr88 => Gr88, + Rg1616 => Rg1616, + Gr1616 => Gr1616, + Xrgb16161616f => Xrgb16161616f, + Xbgr16161616f => Xbgr16161616f, + Argb16161616f => Argb16161616f, + Abgr16161616f => Abgr16161616f, + Xyuv8888 => Xyuv8888, + Vuy888 => Vuy888, + Vuy101010 => Vuy101010, + Y210 => Y210, + Y212 => Y212, + Y216 => Y216, + Y410 => Y410, + Y412 => Y412, + Y416 => Y416, + Xvyu2101010 => Xvyu2101010, + Xvyu12_16161616 => Xvyu1216161616, + Xvyu16161616 => Xvyu16161616, + Y0l0 => Y0l0, + X0l0 => X0l0, + Y0l2 => Y0l2, + X0l2 => X0l2, + Yuv420_8bit => Yuv4208bit, + Yuv420_10bit => Yuv42010bit, + Xrgb8888_a8 => Xrgb8888A8, + Xbgr8888_a8 => Xbgr8888A8, + Rgbx8888_a8 => Rgbx8888A8, + Bgrx8888_a8 => Bgrx8888A8, + Rgb888_a8 => Rgb888A8, + Bgr888_a8 => Bgr888A8, + Rgb565_a8 => Rgb565A8, + Bgr565_a8 => Bgr565A8, + Nv24 => Nv24, + Nv42 => Nv42, + P210 => P210, + P010 => P010, + P012 => P012, + P016 => P016, + Axbxgxrx106106106106 => Axbxgxrx106106106106, + Nv15 => Nv15, + Q410 => Q410, + Q401 => Q401, + + // these currently have no drm variant + //Xrgb16161616 => Xrgb16161616, + //Xbgr16161616 => Xbgr16161616, + //Argb16161616 => Argb16161616, + //Abgr16161616 => Abgr16161616, +} + +/// Details of the contents of a buffer relative to its pool +#[derive(Copy, Clone, Debug)] +pub struct BufferData { + /// Offset of the start of the buffer relative to the beginning of the pool in bytes + pub offset: i32, + /// Width of the buffer in bytes + pub width: i32, + /// Height of the buffer in bytes + pub height: i32, + /// Stride of the buffer in bytes + pub stride: i32, + /// Format used by this buffer + pub format: wl_shm::Format, +} + +/// User data of WlShmPool +#[derive(Debug)] +pub struct ShmPoolUserData { + inner: Arc, +} + +type DestructionHook = dyn Fn(&mut dyn Any, &wl_buffer::WlBuffer) + Send + Sync; + +/// User data of shm WlBuffer +#[derive(Debug)] +pub struct ShmBufferUserData { + pub(crate) pool: Arc, + pub(crate) data: BufferData, + destruction_hooks: Mutex>>, +} + +impl ShmBufferUserData { + pub(crate) fn add_destruction_hook( + &self, + hook: impl Fn(&mut dyn Any, &wl_buffer::WlBuffer) + Send + Sync + 'static, + ) -> HookId { + let hook: Hook = Hook::new(Arc::new(hook)); + let id = hook.id.clone(); + self.destruction_hooks.lock().unwrap().push(hook); + id + } + + pub(crate) fn remove_destruction_hook(&self, hook_id: &HookId) { + let mut guard = self.destruction_hooks.lock().unwrap(); + if let Some(id) = guard.iter().position(|hook| hook.id == *hook_id) { + guard.remove(id); + } + } +} diff --git a/vendor/smithay/src/wayland/shm/pool.rs b/vendor/smithay/src/wayland/shm/pool.rs new file mode 100644 index 0000000..b52e731 --- /dev/null +++ b/vendor/smithay/src/wayland/shm/pool.rs @@ -0,0 +1,368 @@ +use std::{ + cell::Cell, + mem, + num::NonZeroUsize, + os::unix::io::{AsFd, BorrowedFd, OwnedFd}, + ptr, + sync::{ + LazyLock, OnceLock, RwLock, + mpsc::{Sender, channel}, + }, + thread, +}; + +use rustix::mm; +use tracing::{debug, instrument, trace}; + +// Dropping Pool is actually pretty slow. Unmapping the memory can take 1-2 ms, but the real +// offender is closing the file descriptor, which I've seen take up to 6 ms. It's waiting on some +// spinlock in the kernel. +// +// Blocking the main thread for 6 ms is quite bad. In fact, 6 ms is almost the entire time budget +// for a 165 Hz frame. To make matters worse, some clients will cause repeated creation and +// dropping of shm pools, like Firefox during a focus-out animation. This results in dropped +// frames. +// +// To work around this problem, we spawn a separate thread whose sole purpose is dropping stuff we +// send it through a channel. Conveniently, Pool is already Send, so there's no problem doing this. +static DROP_THIS: LazyLock> = LazyLock::new(|| { + let (tx, rx) = channel(); + thread::Builder::new() + .name("Shm dropping thread".to_owned()) + .spawn(move || { + while let Ok(x) = rx.recv() { + profiling::scope!("dropping Pool"); + drop(x); + } + }) + .unwrap(); + tx +}); + +thread_local!(static SIGBUS_GUARD: Cell<(*const MemMap, bool)> = const { Cell::new((ptr::null_mut(), false)) }); + +static OLD_SIGBUS_HANDLER: OnceLock = OnceLock::new(); + +#[derive(Debug)] +pub struct Pool { + inner: Option, +} + +#[derive(Debug)] +struct InnerPool { + map: RwLock, + fd: OwnedFd, +} + +// SAFETY: The memmap is owned by the pool and content is only accessible via a reference. +unsafe impl Send for InnerPool {} +// SAFETY: The memmap is guarded by a RwLock, meaning no writers may mutate the memmap when it is being read. +unsafe impl Sync for InnerPool {} + +pub enum ResizeError { + InvalidSize, + MremapFailed, +} + +impl InnerPool { + #[instrument(level = "trace", skip_all, name = "wayland_shm")] + pub fn new(fd: OwnedFd, size: NonZeroUsize) -> Result { + let memmap = match MemMap::new(fd.as_fd(), size) { + Ok(memmap) => memmap, + Err(_) => { + return Err(fd); + } + }; + trace!(fd = ?fd, size = ?size, "Creating new shm pool"); + Ok(InnerPool { + map: RwLock::new(memmap), + fd, + }) + } + + pub fn resize(&self, newsize: NonZeroUsize) -> Result<(), ResizeError> { + let mut guard = self.map.write().unwrap(); + let oldsize = guard.size(); + + if oldsize > usize::from(newsize) { + return Err(ResizeError::InvalidSize); + } + + trace!(fd = ?self.fd, oldsize = oldsize, newsize = ?newsize, "Resizing shm pool"); + guard.remap(self.fd.as_fd(), newsize).map_err(|()| { + debug!(fd = ?self.fd, oldsize = oldsize, newsize = ?newsize, "SHM pool resize failed"); + ResizeError::MremapFailed + }) + } + + pub fn size(&self) -> usize { + self.map.read().unwrap().size + } + + #[instrument(level = "trace", skip_all, name = "wayland_shm")] + pub fn with_data T>(&self, f: F) -> Result { + // Place the sigbus handler + unsafe { place_sigbus_handler() }; + + let pool_guard = self.map.read().unwrap(); + + trace!(fd = ?self.fd, "Buffer access on shm pool"); + + // Prepare the access + SIGBUS_GUARD.with(|guard| { + let (p, _) = guard.get(); + if !p.is_null() { + // Recursive call of this method is not supported + panic!("Recursive access to a SHM pool content is not supported."); + } + guard.set((&*pool_guard as *const MemMap, false)) + }); + + let t = f(pool_guard.ptr as *const _, pool_guard.size); + + // Cleanup Post-access + SIGBUS_GUARD.with(|guard| { + let (_, triggered) = guard.get(); + guard.set((ptr::null_mut(), false)); + if triggered { + debug!(fd = ?self.fd, "SIGBUS caught on access on shm pool"); + Err(()) + } else { + Ok(t) + } + }) + } + + #[instrument(level = "trace", skip_all, name = "wayland_shm")] + pub fn with_data_mut T>(&self, f: F) -> Result { + // Place the sigbus handler + unsafe { place_sigbus_handler() }; + + // This is actually a write access. + #[allow(clippy::readonly_write_lock)] + let pool_guard = self.map.write().unwrap(); + + trace!(fd = ?self.fd, "Mutable buffer access on shm pool"); + + // Prepare the access + SIGBUS_GUARD.with(|guard| { + let (p, _) = guard.get(); + if !p.is_null() { + // Recursive call of this method is not supported + panic!("Recursive access to a SHM pool content is not supported."); + } + guard.set((&*pool_guard as *const MemMap, false)) + }); + + let t = f(pool_guard.ptr, pool_guard.size); + + // Cleanup Post-access + SIGBUS_GUARD.with(|guard| { + let (_, triggered) = guard.get(); + guard.set((ptr::null_mut(), false)); + if triggered { + debug!(fd = ?self.fd, "SIGBUS caught on access on shm pool"); + Err(()) + } else { + Ok(t) + } + }) + } +} + +impl Pool { + pub fn new(fd: OwnedFd, size: NonZeroUsize) -> Result { + InnerPool::new(fd, size).map(|p| Self { inner: Some(p) }) + } + + pub fn resize(&self, newsize: NonZeroUsize) -> Result<(), ResizeError> { + self.inner.as_ref().unwrap().resize(newsize) + } + + pub fn size(&self) -> usize { + self.inner.as_ref().unwrap().size() + } + + pub fn with_data T>(&self, f: F) -> Result { + self.inner.as_ref().unwrap().with_data(f) + } + + pub fn with_data_mut T>(&self, f: F) -> Result { + self.inner.as_ref().unwrap().with_data_mut(f) + } +} + +impl Drop for Pool { + fn drop(&mut self) { + let _ = DROP_THIS.send(self.inner.take().unwrap()); + } +} + +#[derive(Debug)] +struct MemMap { + ptr: *mut u8, + size: usize, +} + +impl MemMap { + fn new(fd: BorrowedFd<'_>, size: NonZeroUsize) -> Result { + Ok(MemMap { + ptr: unsafe { map(fd, size) }?, + size: size.into(), + }) + } + + fn remap(&mut self, fd: BorrowedFd<'_>, newsize: NonZeroUsize) -> Result<(), ()> { + if self.ptr.is_null() { + return Err(()); + } + // memunmap cannot fail, as we are unmapping a pre-existing map + let _ = unsafe { unmap(self.ptr, self.size) }; + // remap the fd with the new size + match unsafe { map(fd, newsize) } { + Ok(ptr) => { + // update the parameters + self.ptr = ptr; + self.size = usize::from(newsize); + Ok(()) + } + Err(()) => { + // set ourselves in an empty state + self.ptr = ptr::null_mut(); + self.size = 0; + Err(()) + } + } + } + + fn size(&self) -> usize { + self.size + } + + fn contains(&self, ptr: *mut u8) -> bool { + ptr >= self.ptr && ptr < unsafe { self.ptr.add(self.size) } + } + + fn nullify(&self) -> Result<(), ()> { + unsafe { nullify_map(self.ptr, self.size) } + } +} + +impl Drop for MemMap { + fn drop(&mut self) { + if !self.ptr.is_null() { + let _ = unsafe { unmap(self.ptr, self.size) }; + } + } +} + +/// A simple wrapper with some default arguments for `nix::mman::mmap`. +unsafe fn map(fd: BorrowedFd<'_>, size: NonZeroUsize) -> Result<*mut u8, ()> { + let ret = unsafe { + mm::mmap( + ptr::null_mut(), + size.into(), + mm::ProtFlags::READ | mm::ProtFlags::WRITE, + mm::MapFlags::SHARED, + fd, + 0, + ) + }; + ret.map(|p| p as *mut u8).map_err(|_| ()) +} + +/// A simple wrapper for `nix::mman::munmap`. +#[profiling::function] +unsafe fn unmap(ptr: *mut u8, size: usize) -> Result<(), ()> { + let ret = unsafe { mm::munmap(ptr as *mut _, size) }; + ret.map_err(|_| ()) +} + +unsafe fn nullify_map(ptr: *mut u8, size: usize) -> Result<(), ()> { + let ret = unsafe { + mm::mmap_anonymous( + ptr as *mut std::ffi::c_void, + size, + mm::ProtFlags::READ | mm::ProtFlags::WRITE, + mm::MapFlags::PRIVATE | mm::MapFlags::FIXED, + ) + }; + ret.map(|_| ()).map_err(|_| ()) +} + +/// The sigbus handler will be placed only once +unsafe fn place_sigbus_handler() { + let _ = OLD_SIGBUS_HANDLER.get_or_init(|| { + // create our sigbus handler + unsafe { + // We use `mem::zeroed()` because regular struct init as well as struct update syntax require all fields to be public + // and libc does not guarantee that for all targets + let mut action: libc::sigaction = mem::zeroed(); + action.sa_sigaction = sigbus_handler as *const () as _; + action.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER; + + let mut old_action = mem::zeroed(); + if libc::sigaction(libc::SIGBUS, &action, &mut old_action) == -1 { + let e = rustix::io::Errno::from_raw_os_error(errno::errno().0); + panic!("sigaction failed for SIGBUS handler: {e:?}"); + } + + old_action + } + }); +} + +unsafe fn reraise_sigbus() { + // reset the old sigaction + unsafe { + libc::sigaction(libc::SIGBUS, OLD_SIGBUS_HANDLER.get().unwrap(), ptr::null_mut()); + libc::raise(libc::SIGBUS); + } +} + +extern "C" fn sigbus_handler(_signum: libc::c_int, info: *mut libc::siginfo_t, _context: *mut libc::c_void) { + let faulty_ptr = unsafe { siginfo_si_addr(info) } as *mut u8; + SIGBUS_GUARD.with(|guard| { + let (memmap, _) = guard.get(); + match unsafe { memmap.as_ref() }.map(|m| (m, m.contains(faulty_ptr))) { + Some((m, true)) => { + // we are in a faulty memory pool ! + // remember that it was faulty + guard.set((memmap, true)); + // nullify the pool + if m.nullify().is_err() { + // something terrible occurred ! + unsafe { reraise_sigbus() } + } + } + _ => { + // something else occurred, let's die honorably + unsafe { reraise_sigbus() } + } + } + }); +} + +/// This was shamelessly stolen from rustc's source +/// so I expect it to work whenever rust works +/// I guess it's good enough? +/// +/// SAFETY: +/// The returned pointer points to a struct. Make sure that you use it +/// appropriately. +#[cfg(any(target_os = "linux", target_os = "android"))] +unsafe fn siginfo_si_addr(info: *mut libc::siginfo_t) -> *mut libc::c_void { + #[repr(C)] + #[allow(non_camel_case_types)] + struct siginfo_t { + a: [libc::c_int; 3], // si_signo, si_errno, si_code + si_addr: *mut libc::c_void, + } + + unsafe { (*(info as *const siginfo_t)).si_addr } +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +unsafe fn siginfo_si_addr(info: *mut libc::siginfo_t) -> *mut libc::c_void { + unsafe { (*info).si_addr as _ } +} diff --git a/vendor/smithay/src/wayland/single_pixel_buffer/handlers.rs b/vendor/smithay/src/wayland/single_pixel_buffer/handlers.rs new file mode 100644 index 0000000..e912719 --- /dev/null +++ b/vendor/smithay/src/wayland/single_pixel_buffer/handlers.rs @@ -0,0 +1,79 @@ +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2, buffer::BufferHandler}; + +use super::SinglePixelBufferUserData; +use wayland_protocols::wp::single_pixel_buffer::v1::server::wp_single_pixel_buffer_manager_v1::{ + self, WpSinglePixelBufferManagerV1, +}; +use wayland_server::{ + DataInit, Dispatch, DisplayHandle, New, + protocol::wl_buffer::{self, WlBuffer}, +}; + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &wayland_server::Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _manager: &WpSinglePixelBufferManagerV1, + request: wp_single_pixel_buffer_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_single_pixel_buffer_manager_v1::Request::CreateU32RgbaBuffer { + id: buffer, + r, + g, + b, + a, + } => { + data_init.init(buffer, SinglePixelBufferUserData { r, g, b, a }); + } + wp_single_pixel_buffer_manager_v1::Request::Destroy => {} + _ => todo!(), + } + } +} + +impl Dispatch2 for SinglePixelBufferUserData +where + D: BufferHandler, +{ + fn request( + &self, + data: &mut D, + _client: &wayland_server::Client, + buffer: &wl_buffer::WlBuffer, + request: wl_buffer::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_buffer::Request::Destroy => { + data.buffer_destroyed(buffer); + } + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/single_pixel_buffer/mod.rs b/vendor/smithay/src/wayland/single_pixel_buffer/mod.rs new file mode 100644 index 0000000..737c132 --- /dev/null +++ b/vendor/smithay/src/wayland/single_pixel_buffer/mod.rs @@ -0,0 +1,144 @@ +//! Utilities for handling the `wp_single_pixel_buffer` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation, create [`SinglePixelBufferState`], store it in your `State` struct and +//! implement the required traits, as shown in this example: +//! +//! ``` +//! use smithay::wayland::{ +//! buffer::BufferHandler, +//! single_pixel_buffer::SinglePixelBufferState +//! }; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! // Create the single pixel buffer state: +//! let single_pixel_buffer_state = SinglePixelBufferState::new::( +//! &display.handle(), // the display +//! ); +//! +//! // Smithay's "SinglePixelBufferState" also requires the buffer management utilities, you need to implement +//! // "BufferHandler". +//! impl BufferHandler for State { +//! fn buffer_destroyed(&mut self, buffer: &wayland_server::protocol::wl_buffer::WlBuffer) { +//! // All renderers can handle buffer destruction at this point. Some parts of window management may +//! // also use this function. +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Access the single pixel buffer data +//! +//! The buffer data stores the color as RGBA values which can be retrieved by calling [`get_single_pixel_buffer`]. +//! +//! ```no_run +//! use smithay::wayland::single_pixel_buffer; +//! # let buffer: smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer = { todo!() }; +//! let single_pixel_buffer_data = single_pixel_buffer::get_single_pixel_buffer(&buffer).unwrap(); +//! ``` + +use wayland_protocols::wp::single_pixel_buffer::v1::server::wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1; +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, Resource, backend::GlobalId, protocol::wl_buffer::WlBuffer, +}; + +use crate::wayland::GlobalData; + +mod handlers; + +/// Delegate state of WpSinglePixelBuffer protocol +#[derive(Debug)] +pub struct SinglePixelBufferState { + global: GlobalId, +} + +impl SinglePixelBufferState { + /// Create a new [`WpSinglePixelBufferManagerV1`] global + // + /// The id provided by [`SinglePixelBufferState::global`] may be used to + /// remove or disable this global in the future. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: 'static, + { + let global = display.create_global::(1, GlobalData); + + Self { global } + } + + /// Returns the id of the [`WpSinglePixelBufferManagerV1`] global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +/// User data of `WlBuffer` backed by single pixel +#[derive(Debug)] +pub struct SinglePixelBufferUserData { + /// Value of the buffer's red channel + pub r: u32, + /// Value of the buffer's green channel + pub g: u32, + /// Value of the buffer's blue channel + pub b: u32, + /// Value of the buffer's alpha channel + pub a: u32, +} + +impl SinglePixelBufferUserData { + /// Check if pixel has alpha + pub fn has_alpha(&self) -> bool { + self.a != u32::MAX + } + + /// RGAB8888 color buffer + pub fn rgba8888(&self) -> [u8; 4] { + let divisor = u32::MAX / 255; + + [ + (self.r / divisor) as u8, + (self.g / divisor) as u8, + (self.b / divisor) as u8, + (self.a / divisor) as u8, + ] + } + + /// Color represented as rgba32f + /// + /// Ranges from `0f32` to `1f32` + pub fn rgba32f(&self) -> [f32; 4] { + let divisor = u32::MAX as f32; + + [ + self.r as f32 / divisor, + self.g as f32 / divisor, + self.b as f32 / divisor, + self.a as f32 / divisor, + ] + } +} + +/// Error that can occur when accessing an SinglePixelBuffer +#[derive(Debug, thiserror::Error)] +pub enum BufferAccessError { + /// This buffer is not managed by the SinglePixelBuffer handler + #[error("non-single-pixel buffer")] + NotManaged, +} + +/// Gets the data of a `SinglePixelBuffer` backed [`WlBuffer`]. +pub fn get_single_pixel_buffer(buffer: &WlBuffer) -> Result<&SinglePixelBufferUserData, BufferAccessError> { + buffer + .data::() + .ok_or(BufferAccessError::NotManaged) +} diff --git a/vendor/smithay/src/wayland/socket.rs b/vendor/smithay/src/wayland/socket.rs new file mode 100644 index 0000000..7f445fa --- /dev/null +++ b/vendor/smithay/src/wayland/socket.rs @@ -0,0 +1,135 @@ +//! Wayland listening socket. +//! +//! This module provides an [`EventSource`] that invokes a callback when a new client has connected to the +//! socket. This is one of the ways a Wayland compositor allows clients to discover the compositor. +//! +//! The callback provides a [`UnixStream`] that represents the client connection. You need to create the +//! client using this stream by calling [`DisplayHandle::insert_client`](wayland_server::DisplayHandle::insert_client). +//! +//! # Example usage +//! +//! ```no_run +//! # use std::sync::Arc; +//! use smithay::wayland::socket::ListeningSocketSource; +//! +//! // data passed into calloop +//! struct Example { +//! display: wayland_server::Display<()>, +//! } +//! +//! let event_loop = calloop::EventLoop::::try_new().unwrap(); +//! let mut display = wayland_server::Display::<()>::new().unwrap(); +//! +//! // Create a socket for clients to discover the compositor. +//! // +//! // This function will select the next open name for a listening socket. +//! let listening_socket = ListeningSocketSource::new_auto().unwrap(); +//! +//! event_loop.handle().insert_source(listening_socket, |client_stream, _, state| { +//! // Inside the callback, you should insert the client into the display. +//! // +//! // You may also associate some data with the client when inserting the client. +//! state.display.handle().insert_client(client_stream, Arc::new(ExampleClientData)); +//! }); +//! +//! # struct ExampleClientData; +//! # +//! # impl wayland_server::backend::ClientData for ExampleClientData { +//! # fn initialized(&self, _: wayland_server::backend::ClientId) {} +//! # fn disconnected( +//! # &self, +//! # _: wayland_server::backend::ClientId, +//! # _: wayland_server::backend::DisconnectReason +//! # ) {} +//! # } +//! ``` + +use std::{ffi::OsStr, io, os::unix::net::UnixStream}; + +use calloop::{ + EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory, generic::Generic, +}; +use tracing::{debug, info}; +use wayland_server::{BindError, ListeningSocket}; + +/// A Wayland listening socket event source. +/// +/// This implements [`EventSource`] and may be inserted into an event loop. +#[derive(Debug)] +pub struct ListeningSocketSource { + socket: Generic, +} + +impl ListeningSocketSource { + /// Creates a new listening socket, automatically choosing the next available `wayland` socket name. + pub fn new_auto() -> Result { + // Try socket numbers 1-32. Remember the upper bound of Range is exclusive. + // + // We don't try wayland-0 due since clients may connect to the wrong compositor. Clients these days + // should be connecting based off the WAYLAND_DISPLAY or WAYLAND_SOCKET environment variables. + let socket = ListeningSocket::bind_auto("wayland", 1..33)?; + + info!(name = ?socket.socket_name(), "Created new socket"); + + Ok(ListeningSocketSource { + socket: Generic::new(socket, Interest::READ, Mode::Level), + }) + } + + /// Creates a new listening socket with the specified name. + pub fn with_name(name: &str) -> Result { + let socket = ListeningSocket::bind(name)?; + info!(name = ?socket.socket_name(), "Created new socket"); + + Ok(ListeningSocketSource { + socket: Generic::new(socket, Interest::READ, Mode::Level), + }) + } + + /// Returns the name of the listening socket. + pub fn socket_name(&self) -> &OsStr { + self.socket.get_ref().socket_name().unwrap() + } +} + +impl EventSource for ListeningSocketSource { + /// A stream to the new client. + /// + /// You must register the client using the stream by calling + /// [`DisplayHandle::insert_client`](wayland_server::DisplayHandle::insert_client). + type Event = UnixStream; + type Metadata = (); + type Ret = (); + type Error = io::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> io::Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + self.socket.process_events(readiness, token, |_, socket| { + while let Some(client) = socket.accept()? { + debug!(socket = ?socket.socket_name(), client = ?client, "New client connected"); + callback(client, &mut ()); + } + + Ok(PostAction::Continue) + }) + } + + fn register(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.socket.register(poll, token_factory) + } + + fn reregister(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.socket.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.socket.unregister(poll) + } +} diff --git a/vendor/smithay/src/wayland/tablet_manager/mod.rs b/vendor/smithay/src/wayland/tablet_manager/mod.rs new file mode 100644 index 0000000..003c414 --- /dev/null +++ b/vendor/smithay/src/wayland/tablet_manager/mod.rs @@ -0,0 +1,202 @@ +//! Utilities for graphics tablet support +//! +//! This module provides you with utilities for handling the tablet manager globals and the +//! associated Wayland objects. +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! ``` +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! use smithay::input::tablet::{TabletSeatHandler, TabletSeatTrait}; +//! use smithay::backend::input::TabletToolDescriptor; +//! use smithay::wayland::tablet_manager::{TabletManagerState}; +//! use smithay::reexports::wayland_server::{Display, protocol::wl_surface::WlSurface}; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! # use smithay::reexports::wayland_server::Client; +//! +//! # struct State { seat_state: SeatState }; +//! # let mut display = Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! let mut seat_state = SeatState::::new(); +//! let tablet_state = TabletManagerState::new::(&display_handle); +//! // add the seat state and tablet manager state to your state. +//! // ... +//! +//! // create the seat +//! let seat = seat_state.new_wl_seat( +//! &display_handle, // the display +//! "seat-0", // the name of the seat, will be advertised to clients +//! ); +//! // create the associated tablet seat. +//! let tablet_seat = seat.tablet_seat(); +//! +//! // implement the required traits +//! impl SeatHandler for State { +//! type KeyboardFocus = WlSurface; +//! type PointerFocus = WlSurface; +//! type TouchFocus = WlSurface; +//! +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { +//! // handle focus changes, if you need to ... +//! } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { +//! // handle new images for the cursor ... +//! } +//! } +//! # impl PointerConstraintsHandler for State {} +//! +//! impl TabletSeatHandler for State { +//! type ToolFocus = WlSurface; +//! +//! fn tablet_tool_image(&mut self, tool: &TabletToolDescriptor, image: CursorImageStatus) { +//! // handle new image for the given tool. +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &WlSurface) {} +//! # } +//! ``` +//! +//! ### Run usage +//! +//! Once the seat is initialized, you can add tablet and tools to it. +//! +//! You can add these via methods of the [`TabletSeat`] struct: +//! [`TabletSeat::add_wp_tablet`] and [`TabletSeat::add_wp_tool`]. These methods return the same +//! handle their non-wayland counterpart do, but additionally expose ZwpTablet* objects to wayland +//! clients. + +use crate::{ + input::{ + Seat, SeatHandler, + tablet::{TabletSeat, TabletSeatHandler}, + }, + wayland::{Dispatch2, GlobalData, GlobalDispatch2, compositor::CompositorHandler, seat::WaylandFocus}, +}; +use wayland_protocols::wp::tablet::zv2::server::{ + zwp_tablet_manager_v2::{self, ZwpTabletManagerV2}, + zwp_tablet_seat_v2::ZwpTabletSeatV2, + zwp_tablet_tool_v2::ZwpTabletToolV2, + zwp_tablet_v2::ZwpTabletV2, +}; + +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, backend::GlobalId}; +const MANAGER_VERSION: u32 = 1; + +pub(crate) mod tablet; +mod tablet_seat; +pub(crate) mod tablet_tool; + +pub use tablet::TabletUserData; +pub use tablet_seat::TabletSeatUserData; +pub use tablet_tool::TabletToolUserData; + +/// State of wp tablet protocol +#[derive(Debug)] +pub struct TabletManagerState { + global: GlobalId, +} + +impl TabletManagerState { + /// Initialize a tablet manager global. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch>, + D: Dispatch>, + D: TabletSeatHandler, + D: 'static, + { + let global = display.create_global::(MANAGER_VERSION, GlobalData); + + Self { global } + } + + /// Get the id of ZwpTabletManagerV2 global + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch>, + D: TabletSeatHandler, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch>, + D: Dispatch, + D: Dispatch>, + D: SeatHandler + TabletSeatHandler + 'static, + D: CompositorHandler, + ::ToolFocus: WaylandFocus, +{ + fn request( + &self, + state: &mut D, + client: &Client, + _resource: &ZwpTabletManagerV2, + request: ::Request, + dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_tablet_manager_v2::Request::GetTabletSeat { tablet_seat, seat } => { + let seat = Seat::::from_resource(&seat).unwrap(); + + let user_data = seat.user_data(); + user_data.insert_if_missing(TabletSeat::::default); + + let handle = user_data.get::>().unwrap(); + let instance = data_init.init( + tablet_seat, + TabletSeatUserData { + handle: handle.downgrade(), + }, + ); + + handle.add_instance(state, dh, &instance, client); + } + zwp_tablet_manager_v2::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } + + fn destroyed( + &self, + _state: &mut D, + _client: wayland_server::backend::ClientId, + _resource: &ZwpTabletManagerV2, + ) { + } +} diff --git a/vendor/smithay/src/wayland/tablet_manager/tablet.rs b/vendor/smithay/src/wayland/tablet_manager/tablet.rs new file mode 100644 index 0000000..226ce2b --- /dev/null +++ b/vendor/smithay/src/wayland/tablet_manager/tablet.rs @@ -0,0 +1,182 @@ +use std::sync::{Arc, Mutex}; + +use wayland_protocols::wp::tablet::zv2::server::{ + zwp_tablet_seat_v2::ZwpTabletSeatV2, zwp_tablet_v2::ZwpTabletV2, +}; +use wayland_server::{ + Client, Dispatch, DisplayHandle, Resource, Weak, backend::ObjectId, protocol::wl_surface::WlSurface, +}; + +use crate::{ + input::tablet::{Tablet, TabletDescriptor, TabletRc, TabletSeat, TabletSeatHandler, WeakTablet}, + wayland::Dispatch2, +}; + +impl Tablet { + fn new_bound(descriptor: TabletDescriptor) -> Self { + Self { + arc: Arc::new(TabletRc { + descriptor, + #[cfg(feature = "wayland_frontend")] + wp_tablet: WpTabletHandle { + bound: true, + known_instances: Default::default(), + }, + }), + } + } +} + +impl TabletSeat { + /// Add a new tablet to a seat, and exposes it to wayland clients + /// + /// You can either add tablet on [DeviceAdded] event, or you can add tablet based + /// on tool event, then clients will not know about devices that are not being used. + /// + /// If the tablet was already known it removes it and recreate a new handle. Because + /// [`TabletToolHandle`] will keep a handle to the [`Tablet`] while in proximity, it may appears + /// to clients that the tablet hasn't been removed until the tool leave its proximity. + /// + /// [DeviceAdded]: crate::backend::input::InputEvent::DeviceAdded + /// [`TabletToolHandle`]: crate::input::tablet::tool::TabletToolHandle + pub fn add_wp_tablet(&self, dh: &DisplayHandle, tablet_desc: &TabletDescriptor) -> Tablet + where + D: Dispatch, + D: 'static, + { + let inner = &mut *self.arc.lock().unwrap(); + + let tablet = inner.add_tablet(tablet_desc, Tablet::new_bound); + let instances = &mut inner.instances; + + for seat in instances.iter() { + let Ok(seat) = seat.upgrade() else { + continue; + }; + + if let Ok(client) = dh.get_client(seat.id()) { + tablet + .arc + .wp_tablet + .new_instance::(&client, dh, &seat, tablet.clone(), tablet_desc); + } + } + + tablet + } +} + +/// User data of ZwpTabletV2 object +#[derive(Debug)] +pub struct TabletUserData { + tablet: WeakTablet, + seat_id: ObjectId, +} + +#[derive(Default, Debug)] +pub(crate) struct WpTabletHandle { + bound: bool, + known_instances: Mutex>>, +} + +impl WpTabletHandle { + pub(super) fn new_instance( + &self, + client: &Client, + dh: &DisplayHandle, + seat: &ZwpTabletSeatV2, + tablet: Tablet, + desc: &TabletDescriptor, + ) where + D: Dispatch, + D: 'static, + { + if !self.bound { + return; + } + + let wp_tablet = client + .create_resource::( + dh, + seat.version(), + TabletUserData { + tablet: tablet.downgrade(), + seat_id: seat.id(), + }, + ) + .unwrap(); + + seat.tablet_added(&wp_tablet); + + wp_tablet.name(desc.name.clone()); + + if let Some((id_product, id_vendor)) = desc.usb_id { + wp_tablet.id(id_product, id_vendor); + } + + if let Some(syspath) = desc.syspath.as_ref().and_then(|p| p.to_str()) { + wp_tablet.path(syspath.to_owned()); + } + + wp_tablet.done(); + + self.known_instances.lock().unwrap().push(wp_tablet.downgrade()); + } + + pub(crate) fn focused_tablet_for_seat( + &self, + surface: &WlSurface, + seat_id: &ObjectId, + ) -> Option { + self.known_instances.lock().unwrap().iter().find_map(|tablet| { + tablet.upgrade().ok().filter(|wp_tablet| { + Resource::id(wp_tablet).same_client_as(&surface.id()) + && &wp_tablet.data::().unwrap().seat_id == seat_id + }) + }) + } +} + +impl Drop for WpTabletHandle { + fn drop(&mut self) { + let mut guard = self.known_instances.lock().unwrap(); + + for wp_tablet in guard.drain(..) { + let Ok(wp_tablet) = wp_tablet.upgrade() else { + continue; + }; + + wp_tablet.removed(); + } + } +} + +impl Dispatch2 for TabletUserData +where + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &ZwpTabletV2, + _request: ::Request, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + } + + fn destroyed(&self, _state: &mut D, _client: wayland_server::backend::ClientId, wp_tablet: &ZwpTabletV2) { + let Some(tablet) = self.tablet.upgrade() else { + return; + }; + + tablet + .arc + .wp_tablet + .known_instances + .lock() + .unwrap() + .retain(|i| i.id() != Resource::id(wp_tablet)); + } +} diff --git a/vendor/smithay/src/wayland/tablet_manager/tablet_seat.rs b/vendor/smithay/src/wayland/tablet_manager/tablet_seat.rs new file mode 100644 index 0000000..2121531 --- /dev/null +++ b/vendor/smithay/src/wayland/tablet_manager/tablet_seat.rs @@ -0,0 +1,80 @@ +use wayland_protocols::wp::tablet::zv2::server::{ + zwp_tablet_seat_v2::ZwpTabletSeatV2, zwp_tablet_tool_v2::ZwpTabletToolV2, zwp_tablet_v2::ZwpTabletV2, +}; +use wayland_server::{Client, Dispatch, DisplayHandle, Resource}; + +use crate::{ + input::tablet::{TabletSeat, TabletSeatHandler, WeakTabletSeat}, + wayland::{ + Dispatch2, + compositor::CompositorHandler, + seat::WaylandFocus, + tablet_manager::{TabletToolUserData, tablet::TabletUserData}, + }, +}; + +impl TabletSeat { + pub(super) fn add_instance( + &self, + state: &mut D, + dh: &DisplayHandle, + seat: &ZwpTabletSeatV2, + client: &Client, + ) where + D: Dispatch, + D: Dispatch>, + D: CompositorHandler, + ::ToolFocus: WaylandFocus, + D: 'static, + { + let mut inner = self.arc.lock().unwrap(); + + for (desc, tablet) in inner.tablets.iter_mut() { + tablet + .arc + .wp_tablet + .new_instance::(client, dh, seat, tablet.clone(), desc); + } + + for (desc, tool) in inner.tools.iter_mut() { + tool.arc + .wp_tablet_tool + .new_instance::(state, client, dh, seat, tool.clone(), desc) + } + + inner.instances.push(seat.downgrade()) + } +} + +/// User data of ZwpTabletSeatV2 object +#[derive(Debug)] +pub struct TabletSeatUserData { + pub(super) handle: WeakTabletSeat, +} + +impl Dispatch2 for TabletSeatUserData +where + D: TabletSeatHandler + 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ZwpTabletSeatV2, + _request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + } + + fn destroyed(&self, _state: &mut D, _client: wayland_server::backend::ClientId, seat: &ZwpTabletSeatV2) { + if let Some(tablet_seat) = self.handle.upgrade() { + tablet_seat + .arc + .lock() + .unwrap() + .instances + .retain(|i| i.id() != seat.id()); + } + } +} diff --git a/vendor/smithay/src/wayland/tablet_manager/tablet_tool.rs b/vendor/smithay/src/wayland/tablet_manager/tablet_tool.rs new file mode 100644 index 0000000..28beeb0 --- /dev/null +++ b/vendor/smithay/src/wayland/tablet_manager/tablet_tool.rs @@ -0,0 +1,667 @@ +use std::sync::{Arc, Mutex, atomic::Ordering}; + +use portable_atomic::AtomicF64; +use wayland_protocols::wp::tablet::zv2::server::{ + zwp_tablet_seat_v2::ZwpTabletSeatV2, + zwp_tablet_tool_v2::{self, ZwpTabletToolV2}, +}; +use wayland_server::{ + Client, Dispatch, DisplayHandle, Resource, Weak, + backend::{ClientId, ObjectId}, + protocol::wl_surface::WlSurface, +}; + +use crate::{ + backend::input::{ButtonState, InputTime, TabletToolCapabilities, TabletToolDescriptor, TabletToolType}, + input::{ + pointer::{CursorImageAttributes, CursorImageStatus}, + tablet::{ + Tablet, TabletSeat, TabletSeatHandler, TabletSeatTrait, + tool::{ + self, AxisFrame, ButtonEvent, DownEvent, MotionEvent, TabletToolGrab, TabletToolHandle, + TabletToolInternal, TabletToolRc, TabletToolTarget, UpEvent, WeakTabletToolHandle, + }, + }, + }, + utils::{Client as ClientCoords, Clock, Monotonic, Point, Serial, iter::new_locked_obj_iter_from_vec}, + wayland::{ + Dispatch2, + compositor::{self, CompositorHandler}, + seat::{CURSOR_IMAGE_ROLE, WaylandFocus}, + }, +}; + +impl TabletToolHandle { + fn new_bound(descriptor: TabletToolDescriptor, default_grab: F) -> Self + where + F: Fn() -> Box> + Send + 'static, + { + Self { + arc: Arc::new(TabletToolRc { + descriptor, + inner: Mutex::new(TabletToolInternal::new(default_grab)), + wp_tablet_tool: WpTabletToolHandle { + bound: true, + last_proximity_in: Default::default(), + last_down: Default::default(), + known_instances: Default::default(), + }, + }), + } + } + + /// Attempt to retrieve a [`TabletToolHandle`] from an existing resource + pub fn from_resource(tool: &ZwpTabletToolV2) -> Option { + tool.data::>()?.handle.upgrade() + } + + /// Return the raw [`ZwpTabletToolV2`] instance for a particular [`Client`] + pub fn client_tools<'a>(&'a self, client: &Client) -> impl Iterator + 'a { + let guard = self.arc.wp_tablet_tool.known_instances.lock().unwrap(); + new_locked_obj_iter_from_vec(guard, client.id()) + } +} + +impl TabletSeat { + /// Add a new tool to a seat and exposes it to wayland clients. + /// + /// Tool are usually added on [TabletToolProximityEvent] event. + /// + /// Calling this method on a seat that already has the same tool will overwrite it, and will be + /// seen by clients as if the tool was removed and a new one was added. + /// + /// [TabletToolProximityEvent]: crate::backend::input::InputEvent::TabletToolProximity + pub fn add_wp_tool( + &self, + state: &mut D, + dh: &DisplayHandle, + tool_desc: &TabletToolDescriptor, + ) -> TabletToolHandle + where + D: Dispatch>, + D: CompositorHandler, + ::ToolFocus: WaylandFocus, + { + self.add_wp_tool_with_grab(state, dh, tool_desc, || Box::new(tool::grab::DefaultGrab)) + } + + /// Add a new tool to a seat and allows the use of a custom default [`TabletToolGrab`] + /// + /// The default ghrab is used in case no other grab is currently active. When using + /// [`TabletSeat::add_wp_tool`], it will use [`tool::DefaultGrab`] which will install + /// [`tool::DownGrab`] on a down event. [`tool::DownGrab`] makes sure all further event will use + /// the same target until an up or physical proximity out event. + /// + /// See [`TabletSeat::add_wp_tool`] for more information. + pub fn add_wp_tool_with_grab( + &self, + state: &mut D, + dh: &DisplayHandle, + tool_desc: &TabletToolDescriptor, + default_grab: F, + ) -> TabletToolHandle + where + D: Dispatch>, + D: CompositorHandler, + ::ToolFocus: WaylandFocus, + F: Fn() -> Box> + Send + 'static, + { + let inner = &mut self.arc.lock().unwrap(); + + let tool = inner.add_tool(tool_desc, default_grab, TabletToolHandle::new_bound); + let instances = &mut inner.instances; + + for seat in instances.iter() { + let Ok(seat) = seat.upgrade() else { + continue; + }; + + if let Ok(client) = dh.get_client(seat.id()) { + tool.arc + .wp_tablet_tool + .new_instance(state, &client, dh, &seat, tool.clone(), tool_desc); + } + } + + tool + } +} + +/// User data for ZwpTabletToolV2 object +#[derive(Debug)] +pub struct TabletToolUserData { + pub(crate) handle: WeakTabletToolHandle, + seat_id: ObjectId, + client_scale: Arc, +} + +#[derive(Default, Debug)] +pub(crate) struct WpTabletToolHandle { + pub(crate) bound: bool, + pub(crate) last_proximity_in: Mutex>, + pub(crate) last_down: Mutex>, + known_instances: Mutex>>, +} + +impl WpTabletToolHandle { + pub(super) fn new_instance( + &self, + state: &mut D, + client: &Client, + dh: &DisplayHandle, + seat: &ZwpTabletSeatV2, + handle: TabletToolHandle, + desc: &TabletToolDescriptor, + ) where + D: Dispatch>, + D: CompositorHandler, + D: TabletSeatHandler, + ::ToolFocus: WaylandFocus, + D: 'static, + { + if !self.bound { + return; + } + + let client_scale = state.client_compositor_state(client).clone_client_scale(); + let wp_tool = client + .create_resource::( + dh, + seat.version(), + TabletToolUserData { + handle: handle.downgrade(), + seat_id: seat.id(), + client_scale, + }, + ) + .unwrap(); + + seat.tool_added(&wp_tool); + + wp_tool._type(desc.tool_type.into()); + + let high: u32 = (desc.hardware_serial >> 32) as u32; + let low: u32 = desc.hardware_serial as u32; + wp_tool.hardware_serial(high, low); + + let high: u32 = (desc.hardware_id_wacom >> 32) as u32; + let low: u32 = desc.hardware_id_wacom as u32; + wp_tool.hardware_id_wacom(high, low); + + if desc.capabilities.contains(TabletToolCapabilities::PRESSURE) { + wp_tool.capability(zwp_tablet_tool_v2::Capability::Pressure); + } + + if desc.capabilities.contains(TabletToolCapabilities::DISTANCE) { + wp_tool.capability(zwp_tablet_tool_v2::Capability::Distance); + } + + if desc.capabilities.contains(TabletToolCapabilities::TILT) { + wp_tool.capability(zwp_tablet_tool_v2::Capability::Tilt); + } + + if desc.capabilities.contains(TabletToolCapabilities::SLIDER) { + wp_tool.capability(zwp_tablet_tool_v2::Capability::Slider); + } + + if desc.capabilities.contains(TabletToolCapabilities::ROTATION) { + wp_tool.capability(zwp_tablet_tool_v2::Capability::Rotation); + } + + if desc.capabilities.contains(TabletToolCapabilities::WHEEL) { + wp_tool.capability(zwp_tablet_tool_v2::Capability::Wheel); + } + + wp_tool.done(); + self.known_instances.lock().unwrap().push(wp_tool.downgrade()); + + let guard = handle.arc.inner.lock().unwrap(); + if let Some((focus, _location)) = &guard.focus { + if focus.same_client_as(&wp_tool.id()) { + if let Some(surface) = focus.wl_surface() { + let tablet = guard.tablet.as_ref().unwrap(); + if let Some(wp_tablet) = + tablet.arc.wp_tablet.focused_tablet_for_seat(&surface, &seat.id()) + { + let serial = self.last_proximity_in.lock().unwrap().unwrap(); + wp_tool.proximity_in(serial.into(), &wp_tablet, &surface); + + if let Some(serial) = *self.last_down.lock().unwrap() { + wp_tool.down(serial.into()); + } + + // TODO: send axis, motion, button. + + let time = Clock::::new().now().as_millis(); + wp_tool.frame(time); + } + } + } + } + } + + fn proximity_in( + &self, + surface: &WlSurface, + tablet: &Tablet, + serial: Serial, + ) { + *self.last_proximity_in.lock().unwrap() = Some(serial); + + self.for_each_focused_tool(surface, |wp_tool| { + let seat_id = &wp_tool.data::>().unwrap().seat_id; + + let Some(wp_tablet) = tablet.arc.wp_tablet.focused_tablet_for_seat(surface, seat_id) else { + return; + }; + + wp_tool.proximity_in(serial.0, &wp_tablet, surface); + }); + } + + fn proximity_out(&self, surface: &WlSurface) { + self.for_each_focused_tool(surface, |wp_tool| { + wp_tool.proximity_out(); + }); + + *self.last_proximity_in.lock().unwrap() = None; + } + + fn down(&self, surface: &WlSurface, event: &DownEvent) { + let serial = event.serial; + *self.last_down.lock().unwrap() = Some(serial); + + self.for_each_focused_tool(surface, |wp_tool| { + wp_tool.down(serial.0); + }); + } + + fn up(&self, surface: &WlSurface) { + self.for_each_focused_tool(surface, |wp_tool| { + wp_tool.up(); + }); + + *self.last_down.lock().unwrap() = None; + } + + fn motion(&self, surface: &WlSurface, event: &MotionEvent) { + self.for_each_focused_tool(surface, |wp_tool| { + let client_scale = wp_tool + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + + let Point { x, y, .. } = event.location.to_client(client_scale); + wp_tool.motion(x, y); + }) + } + + fn axis(&self, surface: &WlSurface, frame: AxisFrame) { + const NORMALIZE: f64 = 65535.0; + fn normalize(value: f64) -> u32 { + (value * NORMALIZE) as u32 + } + + self.for_each_focused_tool(surface, |wp_tool| { + let AxisFrame { + pressure, + distance, + tilt, + rotation, + slider, + wheel, + } = frame; + + if let Some(pressure) = pressure.map(normalize) { + wp_tool.pressure(pressure); + } + + if let Some(distance) = distance.map(normalize) { + wp_tool.distance(distance); + } + + if let Some((tilt_x, tilt_y)) = tilt { + wp_tool.tilt(tilt_x, tilt_y); + } + + if let Some(degrees) = rotation { + wp_tool.rotation(degrees); + } + + if let Some(slider) = slider.map(|s| (s * NORMALIZE) as i32) { + wp_tool.slider(slider); + } + + if let Some((degrees, clicks)) = wheel { + wp_tool.wheel(degrees, clicks); + } + }); + } + + fn button(&self, surface: &WlSurface, event: &ButtonEvent) { + self.for_each_focused_tool(surface, |wp_tool| { + let ButtonEvent { + serial, + button, + state, + .. + } = *event; + wp_tool.button(serial.0, button, state.into()); + }); + } + + fn frame(&self, surface: &WlSurface, time: InputTime) { + self.for_each_focused_tool(surface, |wp_tool| { + wp_tool.frame(time.millis()); + }); + } + + fn for_each_focused_tool(&self, surface: &WlSurface, mut f: impl FnMut(ZwpTabletToolV2)) { + let inner = self.known_instances.lock().unwrap(); + + for tool in &*inner { + let Ok(tool) = tool.upgrade() else { + continue; + }; + + if tool.id().same_client_as(&surface.id()) { + f(tool.clone()) + } + } + } +} + +impl Drop for WpTabletToolHandle { + fn drop(&mut self) { + let mut guard = self.known_instances.lock().unwrap(); + + for tool in guard.drain(..) { + let Ok(wp_tool) = tool.upgrade() else { continue }; + + wp_tool.removed(); + } + } +} + +impl From for zwp_tablet_tool_v2::Type { + #[inline] + fn from(from: TabletToolType) -> zwp_tablet_tool_v2::Type { + match from { + TabletToolType::Pen => zwp_tablet_tool_v2::Type::Pen, + TabletToolType::Eraser => zwp_tablet_tool_v2::Type::Eraser, + TabletToolType::Brush => zwp_tablet_tool_v2::Type::Brush, + TabletToolType::Pencil => zwp_tablet_tool_v2::Type::Pencil, + TabletToolType::Airbrush => zwp_tablet_tool_v2::Type::Airbrush, + TabletToolType::Mouse => zwp_tablet_tool_v2::Type::Mouse, + TabletToolType::Lens => zwp_tablet_tool_v2::Type::Lens, + _ => zwp_tablet_tool_v2::Type::Pen, + } + } +} + +impl From for zwp_tablet_tool_v2::ButtonState { + #[inline] + fn from(from: ButtonState) -> zwp_tablet_tool_v2::ButtonState { + match from { + ButtonState::Pressed => zwp_tablet_tool_v2::ButtonState::Pressed, + ButtonState::Released => zwp_tablet_tool_v2::ButtonState::Released, + } + } +} + +impl Dispatch2 for TabletToolUserData +where + D: TabletSeatHandler, + ::ToolFocus: WaylandFocus, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + tool: &ZwpTabletToolV2, + request: ::Request, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let Some(handle) = self.handle.upgrade() else { + return; + }; + + match request { + zwp_tablet_tool_v2::Request::SetCursor { + serial, + surface, + hotspot_x, + hotspot_y, + } => { + if !allow_setting_cursor(&handle, Serial(serial), &tool.id()) { + return; + } + + let cursor_image = match surface { + Some(surface) => { + if compositor::give_role(&surface, CURSOR_IMAGE_ROLE).is_err() + && compositor::get_role(&surface) != Some(CURSOR_IMAGE_ROLE) + { + tool.post_error( + zwp_tablet_tool_v2::Error::Role, + "Given wl_surface has another role.", + ); + return; + } + + compositor::with_states(&surface, |states| { + states.data_map.insert_if_missing_threadsafe(|| { + Mutex::new(CursorImageAttributes { + hotspot: (0, 0).into(), + }) + }); + + let client_scale = tool + .data::>() + .unwrap() + .client_scale + .load(Ordering::Acquire); + + let hotspot = Point::::from((hotspot_x, hotspot_y)) + .to_f64() + .to_logical(client_scale) + .to_i32_round(); + + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot = hotspot; + }); + + CursorImageStatus::Surface(surface) + } + None => CursorImageStatus::Hidden, + }; + + state.tablet_tool_image(handle.descriptor(), cursor_image); + } + zwp_tablet_tool_v2::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: ClientId, tool: &ZwpTabletToolV2) { + let Some(handle) = self.handle.upgrade() else { + return; + }; + + handle + .arc + .wp_tablet_tool + .known_instances + .lock() + .unwrap() + .retain(|i| i.id() != tool.id()) + } +} + +impl TabletToolTarget for WlSurface +where + D: TabletSeatHandler + 'static, +{ + fn proximity_in( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + tablet: &Tablet, + serial: Serial, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.proximity_in::(self, tablet, serial); + } + } + + fn proximity_out( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.proximity_out(self); + } + } + + fn down( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + event: &DownEvent, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.down(self, event); + } + } + + fn up( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + _event: &UpEvent, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.up(self); + } + } + + fn motion( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + event: &MotionEvent, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.motion::(self, event); + } + } + + fn axis( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + frame: AxisFrame, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.axis(self, frame); + } + } + + fn button( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + event: &ButtonEvent, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.button(self, event); + } + } + + fn frame( + &self, + seat: &crate::input::Seat, + _data: &mut D, + tool_descriptor: &TabletToolDescriptor, + time: InputTime, + ) { + let tablet_seat = seat.tablet_seat(); + + if let Some(tool) = tablet_seat.get_tool(tool_descriptor) { + tool.arc.wp_tablet_tool.frame(self, time); + } + } +} + +pub(crate) fn allow_setting_cursor( + handle: &TabletToolHandle, + serial: Serial, + object_id: &ObjectId, +) -> bool +where + D: TabletSeatHandler + 'static, + ::ToolFocus: WaylandFocus, +{ + // Allow client if there is a tool grab for that client. Like drag and drop. + if handle + .grab_start_data() + .and_then(|data| data.focus) + .is_some_and(|focus| focus.0.same_client_as(object_id)) + { + return true; + } + + if !handle + .arc + .wp_tablet_tool + .last_proximity_in + .lock() + .unwrap() + .is_some_and(|last_serial| last_serial == serial) + { + return false; // Ignore mismatches in serial + } + + // Only allow setting the cursor icon if the current tool focus is of the same + // client. + handle + .arc + .inner + .lock() + .unwrap() + .focus + .as_ref() + .is_some_and(|(focus, _)| focus.same_client_as(object_id)) +} diff --git a/vendor/smithay/src/wayland/text_input/mod.rs b/vendor/smithay/src/wayland/text_input/mod.rs new file mode 100644 index 0000000..6484033 --- /dev/null +++ b/vendor/smithay/src/wayland/text_input/mod.rs @@ -0,0 +1,180 @@ +//! Utilities for text input support +//! +//! This module provides you with utilities to handle text input surfaces, +//! it is usually used in conjunction with the input method module. +//! +//! Text input focus is automatically set to the same surface that has keyboard focus. +//! +//! ``` +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::text_input::{TextInputActivation, TextInputManagerState}; +//! use smithay::reexports::wayland_server::{Display, protocol::wl_surface::WlSurface}; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! # use smithay::reexports::wayland_server::Client; +//! +//! # struct State { seat_state: SeatState }; +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # let mut display = Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! let seat_state = SeatState::::new(); +//! +//! // implement the required traits +//! impl SeatHandler for State { +//! type KeyboardFocus = WlSurface; +//! type PointerFocus = WlSurface; +//! type TouchFocus = WlSurface; +//! fn seat_state(&mut self) -> &mut SeatState { +//! &mut self.seat_state +//! } +//! fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! } +//! # impl PointerConstraintsHandler for State {} +//! +//! impl TextInputActivation for State {} +//! +//! // Add the seat state to your state and create manager global +//! TextInputManagerState::new::(&display_handle); +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &WlSurface) {} +//! # } +//! ``` +//! + +use wayland_protocols::wp::text_input::zv3::server::{ + zwp_text_input_manager_v3::{self, ZwpTextInputManagerV3}, + zwp_text_input_v3::{ContentHint, ContentPurpose, ZwpTextInputV3}, +}; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, backend::GlobalId}; + +use crate::{ + input::{Seat, SeatHandler}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +pub use text_input_handle::TextInputHandle; +pub use text_input_handle::TextInputUserData; + +use super::input_method::InputMethodHandle; + +const MANAGER_VERSION: u32 = 1; + +mod text_input_handle; + +/// Hooks for text input activation +pub trait TextInputActivation { + /// Code to run after text input is activated + fn activated(&mut self, _content_type: Option<(ContentHint, ContentPurpose)>) {} + + /// Code to run after text input is deactivated + fn deactivated(&mut self) {} +} + +/// Extends [Seat] with text input functionality +pub trait TextInputSeat { + /// Get text input associated with this seat + fn text_input(&self) -> &TextInputHandle; +} + +impl TextInputSeat for Seat { + fn text_input(&self) -> &TextInputHandle { + let user_data = self.user_data(); + user_data.insert_if_missing(TextInputHandle::default); + user_data.get::().unwrap() + } +} + +/// State of wp text input protocol +#[derive(Debug)] +pub struct TextInputManagerState { + global: GlobalId, +} + +impl TextInputManagerState { + /// Initialize a text input manager global. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: 'static, + { + let global = display.create_global::(MANAGER_VERSION, GlobalData); + + Self { global } + } + + /// Get the id of ZwpTextInputManagerV3 global + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ZwpTextInputManagerV3, + request: zwp_text_input_manager_v3::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_text_input_manager_v3::Request::GetTextInput { id, seat } => { + let seat = Seat::::from_resource(&seat).unwrap(); + + let user_data = seat.user_data(); + user_data.insert_if_missing(TextInputHandle::default); + user_data.insert_if_missing(InputMethodHandle::default); + let handle = user_data.get::().unwrap(); + let input_method_handle = user_data.get::().unwrap(); + let instance = data_init.init( + id, + TextInputUserData { + handle: handle.clone(), + input_method_handle: input_method_handle.clone(), + }, + ); + handle.add_instance(&instance); + if input_method_handle.has_instance() { + handle.enter(); + } + } + zwp_text_input_manager_v3::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/text_input/text_input_handle.rs b/vendor/smithay/src/wayland/text_input/text_input_handle.rs new file mode 100644 index 0000000..efcf38e --- /dev/null +++ b/vendor/smithay/src/wayland/text_input/text_input_handle.rs @@ -0,0 +1,405 @@ +use std::mem; +use std::sync::{Arc, Mutex}; + +use tracing::debug; +use wayland_protocols::wp::text_input::zv3::server::zwp_text_input_v3::{ + self, ChangeCause, ContentHint, ContentPurpose, ZwpTextInputV3, +}; +use wayland_server::backend::{ClientId, ObjectId}; +use wayland_server::{Resource, protocol::wl_surface::WlSurface}; + +use crate::input::SeatHandler; +use crate::utils::{Logical, Rectangle}; +use crate::wayland::{Dispatch2, input_method::InputMethodHandle, text_input::TextInputActivation}; + +#[derive(Default, Debug)] +pub(crate) struct TextInput { + instances: Vec, + focus: Option, + active_text_input_id: Option, + compositor_input_method: bool, +} + +impl TextInput { + fn with_focused_client_all_text_inputs(&mut self, mut f: F) + where + F: FnMut(&ZwpTextInputV3, &WlSurface, u32), + { + if let Some(surface) = self.focus.as_ref().filter(|surface| surface.is_alive()) { + for text_input in self.instances.iter() { + let instance_id = text_input.instance.id(); + if instance_id.same_client_as(&surface.id()) { + f(&text_input.instance, surface, text_input.serial); + } + } + }; + } + + fn with_active_text_input(&mut self, mut f: F) + where + F: FnMut(&ZwpTextInputV3, &WlSurface, u32), + { + let active_id = match &self.active_text_input_id { + Some(active_text_input_id) => active_text_input_id, + None => return, + }; + + let surface = match self.focus.as_ref().filter(|surface| surface.is_alive()) { + Some(surface) => surface, + None => return, + }; + + let surface_id = surface.id(); + if let Some(text_input) = self + .instances + .iter() + .filter(|instance| instance.instance.id().same_client_as(&surface_id)) + .find(|instance| &instance.instance.id() == active_id) + { + f(&text_input.instance, surface, text_input.serial); + } + } +} + +/// Handle to text input instances +#[derive(Default, Debug, Clone)] +pub struct TextInputHandle { + pub(crate) inner: Arc>, +} + +impl TextInputHandle { + pub(super) fn add_instance(&self, instance: &ZwpTextInputV3) { + let mut inner = self.inner.lock().unwrap(); + inner.instances.push(Instance { + instance: instance.clone(), + serial: 0, + pending_state: Default::default(), + }); + } + + fn increment_serial(&self, text_input: &ZwpTextInputV3) { + if let Some(instance) = self + .inner + .lock() + .unwrap() + .instances + .iter_mut() + .find(|instance| instance.instance == *text_input) + { + instance.serial += 1 + } + } + + /// Return the currently focused surface. + pub fn focus(&self) -> Option { + self.inner.lock().unwrap().focus.clone() + } + + /// Advance the focus for the client to `surface`. + /// + /// This doesn't send any 'enter' or 'leave' events. + pub fn set_focus(&self, surface: Option) { + self.inner.lock().unwrap().focus = surface; + } + + /// Send `leave` on the text-input instance for the currently focused + /// surface. + pub fn leave(&self) { + let mut inner = self.inner.lock().unwrap(); + // Leaving clears the active text input. + inner.active_text_input_id = None; + // NOTE: we implement it in a symmetrical way with `enter`. + inner.with_focused_client_all_text_inputs(|text_input, focus, _| { + text_input.leave(focus); + }); + } + + /// Send `enter` on the text-input instance for the currently focused + /// surface. + pub fn enter(&self) { + let mut inner = self.inner.lock().unwrap(); + // NOTE: protocol states that if we have multiple text inputs enabled, `enter` must + // be send for each of them. + inner.with_focused_client_all_text_inputs(|text_input, focus, _| { + text_input.enter(focus); + }); + } + + /// Have the compositor act as the input method for this seat. + /// + /// While enabled, `enter` is delivered to the focused text-input even when no real + /// `zwp_input_method_v2` is bound, so the compositor can `commit_string` into it (e.g. for + /// remote-desktop text injection). Toggling this sends `enter`/`leave` for the current + /// focus immediately; subsequent focus changes are handled by the keyboard focus logic. + pub fn set_compositor_input_method(&self, active: bool) { + { + let mut inner = self.inner.lock().unwrap(); + if inner.compositor_input_method == active { + return; + } + inner.compositor_input_method = active; + } + if active { + self.enter(); + } else { + self.leave(); + } + } + + /// Whether the compositor is currently acting as the input method for this seat + /// (see [`set_compositor_input_method`](Self::set_compositor_input_method)). + pub fn compositor_input_method(&self) -> bool { + self.inner.lock().unwrap().compositor_input_method + } + + /// The `discard_state` is used when the input-method signaled that + /// the state should be discarded and wrong serial sent. + pub fn done(&self, discard_state: bool) { + let mut inner = self.inner.lock().unwrap(); + inner.with_active_text_input(|text_input, _, serial| { + if discard_state { + debug!("discarding text-input state due to serial"); + // Discarding is done by sending non-matching serial. + text_input.done(0); + } else { + text_input.done(serial); + } + }); + } + + /// Access the text-input instances for the currently focused surface. + pub fn with_focused_text_input(&self, mut f: F) + where + F: FnMut(&ZwpTextInputV3, &WlSurface), + { + let mut inner = self.inner.lock().unwrap(); + inner.with_focused_client_all_text_inputs(|ti, surface, _| { + f(ti, surface); + }); + } + + /// Access the active text-input instance for the currently focused surface. + pub fn with_active_text_input(&self, mut f: F) + where + F: FnMut(&ZwpTextInputV3, &WlSurface), + { + let mut inner = self.inner.lock().unwrap(); + inner.with_active_text_input(|ti, surface, _| { + f(ti, surface); + }); + } + + /// Call the callback with the serial of the active text_input or with the passed + /// `default` one when empty. + pub(crate) fn active_text_input_serial_or_default(&self, default: u32, mut callback: F) + where + F: FnMut(u32), + { + let mut inner = self.inner.lock().unwrap(); + let mut should_default = true; + inner.with_active_text_input(|_, _, serial| { + should_default = false; + callback(serial); + }); + if should_default { + callback(default) + } + } +} + +/// User data of ZwpTextInputV3 object +#[derive(Debug)] +pub struct TextInputUserData { + pub(super) handle: TextInputHandle, + pub(crate) input_method_handle: InputMethodHandle, +} + +impl Dispatch2 for TextInputUserData +where + D: SeatHandler, + D: TextInputActivation, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + resource: &ZwpTextInputV3, + request: zwp_text_input_v3::Request, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + // Always increment serial to not desync with clients. + if matches!(request, zwp_text_input_v3::Request::Commit) { + self.handle.increment_serial(resource); + } + + // Discard requests without any active input method instance, unless the compositor + // itself is acting as the input method for this seat (the text-injection escape hatch). + // In that case we still process enable/commit so the focused client becomes the active + // text input and the compositor can `commit_string` into it. + if !self.input_method_handle.has_instance() && !self.handle.compositor_input_method() { + debug!("discarding text-input request without IME running"); + return; + } + + let focus = match self.handle.focus() { + Some(focus) if focus.id().same_client_as(&resource.id()) => focus, + _ => { + debug!("discarding text-input request for unfocused client"); + return; + } + }; + + let mut guard = self.handle.inner.lock().unwrap(); + let pending_state = match guard.instances.iter_mut().find_map(|instance| { + if instance.instance == *resource { + Some(&mut instance.pending_state) + } else { + None + } + }) { + Some(pending_state) => pending_state, + None => { + debug!("got request for untracked text-input"); + return; + } + }; + + match request { + zwp_text_input_v3::Request::Enable => { + pending_state.enable = Some(true); + } + zwp_text_input_v3::Request::Disable => { + pending_state.enable = Some(false); + } + zwp_text_input_v3::Request::SetSurroundingText { text, cursor, anchor } => { + pending_state.surrounding_text = Some((text, cursor as u32, anchor as u32)); + } + zwp_text_input_v3::Request::SetTextChangeCause { cause } => { + // Guard against clients sending us unknown values from future versions. + let cause = cause.into_result().unwrap_or(ChangeCause::Other); + pending_state.text_change_cause = Some(cause); + } + zwp_text_input_v3::Request::SetContentType { hint, purpose } => { + // Guard against clients sending us unknown values from future versions. + let hint = ContentHint::from_bits_truncate(u32::from(hint)); + let purpose = purpose.into_result().unwrap_or(ContentPurpose::Normal); + pending_state.content_type = Some((hint, purpose)); + } + zwp_text_input_v3::Request::SetCursorRectangle { x, y, width, height } => { + pending_state.cursor_rectangle = Some(Rectangle::new((x, y).into(), (width, height).into())); + } + zwp_text_input_v3::Request::Commit => { + let mut new_state = mem::take(pending_state); + let _ = pending_state; + let active_text_input_id = &mut guard.active_text_input_id; + + if active_text_input_id.is_some() && *active_text_input_id != Some(resource.id()) { + debug!("discarding text_input request since we already have an active one"); + return; + } + + match new_state.enable { + Some(true) => { + *active_text_input_id = Some(resource.id()); + // Drop the guard before calling to other subsystem. + drop(guard); + self.input_method_handle.activate_input_method(state, &focus); + state.activated(new_state.content_type); + } + Some(false) => { + *active_text_input_id = None; + // Drop the guard before calling to other subsystem. + drop(guard); + self.input_method_handle.deactivate_input_method(state); + state.deactivated(); + return; + } + None => { + if *active_text_input_id != Some(resource.id()) { + debug!("discarding text_input requests before enabling it"); + return; + } + + // Drop the guard before calling to other subsystems later on. + drop(guard); + } + } + + if let Some((text, cursor, anchor)) = new_state.surrounding_text.take() { + self.input_method_handle.with_instance(move |input_method| { + input_method.object.surrounding_text(text, cursor, anchor) + }); + } + + if let Some(cause) = new_state.text_change_cause.take() { + self.input_method_handle.with_instance(move |input_method| { + input_method.object.text_change_cause(cause); + }); + } + + if let Some((hint, purpose)) = new_state.content_type.take() { + self.input_method_handle.with_instance(move |input_method| { + input_method.object.content_type(hint, purpose); + }); + } + + if let Some(rect) = new_state.cursor_rectangle.take() { + self.input_method_handle + .set_text_input_rectangle::(state, rect); + } + + self.input_method_handle.with_instance(|input_method| { + input_method.done(); + }); + } + zwp_text_input_v3::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, text_input: &ZwpTextInputV3) { + let destroyed_id = text_input.id(); + let deactivate_im = { + let mut inner = self.handle.inner.lock().unwrap(); + inner.instances.retain(|inst| inst.instance.id() != destroyed_id); + let destroyed_focused = inner + .focus + .as_ref() + .map(|focus| focus.id().same_client_as(&destroyed_id)) + .unwrap_or(true); + + // Deactivate IM when we either lost focus entirely or destroyed text-input for the + // currently focused client. + destroyed_focused + && !inner + .instances + .iter() + .any(|inst| inst.instance.id().same_client_as(&destroyed_id)) + }; + + if deactivate_im { + self.input_method_handle.deactivate_input_method(state); + state.deactivated(); + } + } +} + +#[derive(Debug)] +struct Instance { + instance: ZwpTextInputV3, + serial: u32, + pending_state: TextInputState, +} + +#[derive(Debug, Default)] +struct TextInputState { + enable: Option, + surrounding_text: Option<(String, u32, u32)>, + content_type: Option<(ContentHint, ContentPurpose)>, + cursor_rectangle: Option>, + text_change_cause: Option, +} diff --git a/vendor/smithay/src/wayland/viewporter/mod.rs b/vendor/smithay/src/wayland/viewporter/mod.rs new file mode 100644 index 0000000..a9e32c5 --- /dev/null +++ b/vendor/smithay/src/wayland/viewporter/mod.rs @@ -0,0 +1,422 @@ +//! Utilities for handling the `wp_viewporter` protocol +//! +//! ## How to use it +//! +//! ### Initialization +//! +//! To initialize this implementation, create [`ViewporterState`], store it in your `State` struct and +//! implement the required traits, as shown in this example: +//! +//! ``` +//! use smithay::wayland::viewporter::ViewporterState; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! # use smithay::reexports::wayland_server::{Client, protocol::wl_surface::WlSurface}; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! // Create the viewporter state: +//! let viewporter_state = ViewporterState::new::( +//! &display.handle(), // the display +//! ); +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &WlSurface) {} +//! # } +//! +//! // You're now ready to go! +//! ``` +//! +//! ### Use the viewport state +//! +//! The [`viewport state`](ViewportCachedState) is double-buffered and +//! can be accessed by using the [`with_states`] function +//! +//! ```no_compile +//! let viewport = with_states(surface, |states| { +//! states.cached_state.get::().current() +//! }); +//! ``` +//! +//! Before accessing the state you should call [`ensure_viewport_valid`] +//! to ensure the viewport is valid. +//! +//! Note: If you already hand over buffer management to smithay by using +//! [`on_commit_buffer_handler`](crate::backend::renderer::utils::on_commit_buffer_handler) +//! the implementation will already call [`ensure_viewport_valid`] for you. + +use std::sync::Mutex; + +use tracing::trace; +use wayland_protocols::wp::viewporter::server::{wp_viewport, wp_viewporter}; +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, Resource, Weak, backend::GlobalId, protocol::wl_surface, +}; + +use crate::{ + utils::{Client, Logical, Rectangle, Size}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, +}; + +use super::compositor::{self, Cacheable, CompositorHandler, SurfaceData, with_states}; + +/// State of the wp_viewporter Global +#[derive(Debug)] +pub struct ViewporterState { + global: GlobalId, +} + +pub(crate) type ViewporterSurfaceState = Mutex>; + +impl ViewporterState { + /// Create new [`wp_viewporter`] global. + /// + /// It returns the viewporter state, which you can drop to remove these global from + /// the event loop in the future. + pub fn new(display: &DisplayHandle) -> ViewporterState + where + D: GlobalDispatch + + Dispatch + + Dispatch + + 'static, + { + ViewporterState { + global: display.create_global::(1, GlobalData), + } + } + + /// Returns the viewporter global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, +{ + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _resource: &wp_viewporter::WpViewporter, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wp_viewporter::Request::GetViewport { id, surface } => { + let already_has_viewport = with_states(&surface, |states| { + states + .data_map + .get::() + .map(|v| v.lock().unwrap().is_some()) + .unwrap_or(false) + }); + + if already_has_viewport { + surface.post_error( + wp_viewporter::Error::ViewportExists, + "the surface already has a viewport object associated".to_string(), + ); + return; + } + + let viewport = data_init.init( + id, + ViewportState { + surface: surface.downgrade(), + }, + ); + let initial = with_states(&surface, |states| { + let inserted = states + .data_map + .insert_if_missing_threadsafe::(|| { + Mutex::new(Some(ViewportMarker(viewport.downgrade()))) + }); + + // if we did not insert the marker it will be None as + // checked in already_has_viewport and we have to update + // it now + if !inserted { + *states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() = Some(ViewportMarker(viewport.downgrade())); + } + + inserted + }); + + // only add the pre-commit hook once for the surface + if initial { + compositor::add_pre_commit_hook::(&surface, viewport_pre_commit_hook); + } + } + wp_viewporter::Request::Destroy => { + // All is already handled by our destructor + } + _ => unreachable!(), + } + } +} + +impl Dispatch2 for ViewportState +where + D: CompositorHandler, +{ + fn request( + &self, + state: &mut D, + client: &wayland_server::Client, + resource: &wp_viewport::WpViewport, + request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + wp_viewport::Request::Destroy => { + if let Ok(surface) = self.surface.upgrade() { + with_states(&surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .take(); + *states.cached_state.get::().pending() = + ViewportCachedState::default(); + }); + } + } + wp_viewport::Request::SetSource { x, y, width, height } => { + // If all of x, y, width and height are -1.0, the source rectangle is unset instead. + // Any other set of values where width or height are zero or negative, + // or x or y are negative, raise the bad_value protocol error. + let is_unset = x == -1.0 && y == -1.0 && width == -1.0 && height == -1.0; + let is_valid_src = x >= 0.0 && y >= 0.0 && width > 0.0 && height > 0.0; + + if !is_unset && !is_valid_src { + resource.post_error( + wp_viewport::Error::BadValue, + "negative or zero values in width or height or negative values in x or y".to_string(), + ); + return; + } + + // If the wl_surface associated with the wp_viewport is destroyed, + // all wp_viewport requests except 'destroy' raise the protocol error no_surface. + let Ok(surface) = self.surface.upgrade() else { + resource.post_error( + wp_viewport::Error::NoSurface, + "the wl_surface was destroyed".to_string(), + ); + return; + }; + + with_states(&surface, |states| { + let mut guard = states.cached_state.get::(); + let viewport_state = guard.pending(); + let src = if is_unset { + None + } else { + let src = Rectangle::new((x, y).into(), (width, height).into()); + trace!(surface = ?surface, src = ?src, "setting surface viewport src"); + Some(src) + }; + viewport_state.src = src; + }); + } + wp_viewport::Request::SetDestination { width, height } => { + // If width is -1 and height is -1, the destination size is unset instead. + // Any other pair of values for width and height that contains zero or + // negative values raises the bad_value protocol error. + let is_unset = width == -1 && height == -1; + let is_valid_size = width > 0 && height > 0; + + if !is_unset && !is_valid_size { + resource.post_error( + wp_viewport::Error::BadValue, + "negative or zero values in width or height".to_string(), + ); + return; + } + + // If the wl_surface associated with the wp_viewport is destroyed, + // all wp_viewport requests except 'destroy' raise the protocol error no_surface. + let Ok(surface) = self.surface.upgrade() else { + resource.post_error( + wp_viewport::Error::NoSurface, + "the wl_surface was destroyed".to_string(), + ); + return; + }; + + with_states(&surface, |states| { + let mut guard = states.cached_state.get::(); + let viewport_state = guard.pending(); + let size = if is_unset { + None + } else { + let client_scale = state.client_compositor_state(client).client_scale(); + let dst = Size::<_, Client>::from((width, height)) + .to_f64() + .to_logical(client_scale) + .to_i32_round(); + trace!(surface = ?surface, size = ?dst, "setting surface viewport destination size"); + Some(dst) + }; + viewport_state.dst = size; + }); + } + _ => unreachable!(), + } + } +} + +/// State of a single viewport attached to a surface +#[derive(Debug)] +pub struct ViewportState { + surface: Weak, +} + +pub(crate) struct ViewportMarker(Weak); + +fn viewport_pre_commit_hook( + _state: &mut D, + _dh: &DisplayHandle, + surface: &wl_surface::WlSurface, +) { + with_states(surface, |states| { + states + .data_map + .insert_if_missing_threadsafe::(|| Mutex::new(None)); + let viewport = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + if let Some(viewport) = &*viewport { + let mut guard = states.cached_state.get::(); + let viewport_state = guard.pending(); + + // If src_width or src_height are not integers and destination size is not set, + // the bad_size protocol error is raised when the surface state is applied. + let src_size = viewport_state.src.map(|src| src.size); + if viewport_state.dst.is_none() + && src_size != src_size.map(|s| Size::from((s.w as i32, s.h as i32)).to_f64()) + { + if let Ok(viewport) = viewport.0.upgrade() { + viewport.post_error( + wp_viewport::Error::BadSize, + "destination size is not integer".to_string(), + ); + } + } + } + }); +} + +/// Ensures that the viewport, if any, is valid accordingly to the protocol specification. +/// +/// If the viewport violates any protocol checks a protocol error will be raised and `false` +/// is returned. +pub fn ensure_viewport_valid(states: &SurfaceData, buffer_size: Size) -> bool { + states + .data_map + .insert_if_missing_threadsafe::(|| Mutex::new(None)); + let viewport = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + if let Some(viewport) = &*viewport { + let mut guard = states.cached_state.get::(); + let state = guard.current(); + + let buffer_rect = Rectangle::from_size(buffer_size.to_f64()); + let src = state.src.unwrap_or(buffer_rect); + let valid = buffer_rect.contains_rect(src); + if !valid { + if let Ok(viewport) = viewport.0.upgrade() { + viewport.post_error( + wp_viewport::Error::OutOfBuffer, + format!( + "source rectangle x={},y={},w={},h={} extends outside of the content area x={},y={},w={},h={}", + src.loc.x, src.loc.y, src.size.w, src.size.h, + buffer_rect.loc.x, buffer_rect.loc.y, buffer_rect.size.w, buffer_rect.size.h), + ); + } + } + valid + } else { + true + } +} + +/// Represents the double-buffered viewport +/// state of a [`WlSurface`](wl_surface::WlSurface) +#[derive(Debug, Default, Clone, Copy)] +pub struct ViewportCachedState { + /// Defines the source [`Rectangle`] of the [`WlSurface`](wl_surface::WlSurface) in [`Logical`] + /// coordinates used for cropping. + pub src: Option>, + /// Defines the destination [`Size`] of the [`WlSurface`](wl_surface::WlSurface) in [`Logical`] + /// coordinates used for scaling. + pub dst: Option>, +} + +impl ViewportCachedState { + /// Gets the actual size the [`WlSurface`](wl_surface::WlSurface) should have on screen in + /// [`Logical`] coordinates. + /// + /// This will return the destination size if set or the size of the source rectangle. + /// If both are unset `None` is returned. + pub fn size(&self) -> Option> { + self.dst.or_else(|| { + self.src + .map(|src| Size::from((src.size.w as i32, src.size.h as i32))) + }) + } +} + +impl Cacheable for ViewportCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + ViewportCachedState { + src: self.src, + dst: self.dst, + } + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + into.src = self.src; + into.dst = self.dst; + } +} diff --git a/vendor/smithay/src/wayland/virtual_keyboard/input.rs b/vendor/smithay/src/wayland/virtual_keyboard/input.rs new file mode 100644 index 0000000..218af51 --- /dev/null +++ b/vendor/smithay/src/wayland/virtual_keyboard/input.rs @@ -0,0 +1,202 @@ +//! [`InputBackend`] event types for virtual keyboard devices. + +use std::path::PathBuf; +use std::sync::Arc; + +use wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1; +use wayland_server::protocol::wl_seat::WlSeat; +use wayland_server::{Client, Resource}; + +use crate::backend::input::{self, InputBackend, InputTime, UnusedEvent}; +use crate::input::keyboard::KeyboardSource; + +use super::VirtualKeyboardData; + +/// Backend type identifying events from `zwp_virtual_keyboard_v1`. +/// +/// This backend is never constructed since virtual keyboards are created by the +/// client; only the associated event types are used. +#[derive(Debug)] +pub struct VirtualKeyboardBackend; + +impl InputBackend for VirtualKeyboardBackend { + type Device = VirtualKeyboardDevice; + type KeyboardKeyEvent = VirtualKeyboardKeyEvent; + + type PointerAxisEvent = UnusedEvent; + type PointerButtonEvent = UnusedEvent; + type PointerMotionEvent = UnusedEvent; + type PointerMotionAbsoluteEvent = UnusedEvent; + + type GestureSwipeBeginEvent = UnusedEvent; + type GestureSwipeUpdateEvent = UnusedEvent; + type GestureSwipeEndEvent = UnusedEvent; + type GesturePinchBeginEvent = UnusedEvent; + type GesturePinchUpdateEvent = UnusedEvent; + type GesturePinchEndEvent = UnusedEvent; + type GestureHoldBeginEvent = UnusedEvent; + type GestureHoldEndEvent = UnusedEvent; + + type TouchDownEvent = UnusedEvent; + type TouchUpEvent = UnusedEvent; + type TouchMotionEvent = UnusedEvent; + type TouchCancelEvent = UnusedEvent; + type TouchFrameEvent = UnusedEvent; + + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; + + type SwitchToggleEvent = UnusedEvent; + + type SpecialEvent = VirtualKeyboardSpecialEvent; +} + +/// A keyboard device created by a `zwp_virtual_keyboard_v1` client. +/// +/// The device is only actually added once the client has sent a keymap request, +/// which may or may not carry a keymap (see [`Self::keymap`]). +#[derive(Debug, Clone)] +pub struct VirtualKeyboardDevice { + pub(super) keyboard: ZwpVirtualKeyboardV1, + pub(super) data: Arc, +} + +impl PartialEq for VirtualKeyboardDevice { + fn eq(&self, other: &Self) -> bool { + self.keyboard == other.keyboard + } +} + +impl Eq for VirtualKeyboardDevice {} + +impl std::hash::Hash for VirtualKeyboardDevice { + fn hash(&self, state: &mut H) { + self.keyboard.hash(state) + } +} + +impl VirtualKeyboardDevice { + /// The seat this virtual keyboard was created for. + pub fn wl_seat(&self) -> &WlSeat { + &self.data.seat + } + + /// The client that created this virtual keyboard, if it's still alive. + pub fn client(&self) -> Option { + self.keyboard.client() + } + + /// The [`KeyboardSource`] for this device's key events. + /// + /// Pass this to `KeyboardHandle::input_from_source` so a key event sent for + /// a key already down isn't released prematurely (i.e., so it's released on + /// the last release rather than the first one). + pub fn source(&self) -> KeyboardSource { + self.data.source + } + + /// The client's keymap, as `XKB_KEYMAP_FORMAT_TEXT_V1`. + /// + /// This device's keycodes must be processed using the keymap, which isn't + /// usually the default one, so it needs to be activated (e.g. with + /// [`KeyboardHandle::set_keymap_from_string`](crate::input::keyboard::KeyboardHandle::set_keymap_from_string)) + /// before processing the device's key events. + /// + /// [`None`] when the client sent the `no_keymap` format, meaning its + /// keycodes are to be read with whatever keymap the seat already has, so + /// there is nothing to activate. Also [`None`] before the device is added. + pub fn keymap(&self) -> Option> { + self.data.keymap.lock().unwrap().clone() + } +} + +impl input::Device for VirtualKeyboardDevice { + fn id(&self) -> String { + format!("virtual-keyboard-{}", self.keyboard.id()) + } + + fn name(&self) -> String { + String::from("virtual keyboard") + } + + fn has_capability(&self, capability: input::DeviceCapability) -> bool { + matches!(capability, input::DeviceCapability::Keyboard) + } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option { + None + } +} + +/// A key event generated by a virtual keyboard. +#[derive(Debug)] +pub struct VirtualKeyboardKeyEvent { + pub(super) device: VirtualKeyboardDevice, + pub(super) time: u32, + pub(super) key: u32, + pub(super) state: input::KeyState, +} + +impl input::Event for VirtualKeyboardKeyEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> VirtualKeyboardDevice { + self.device.clone() + } +} + +impl input::KeyboardKeyEvent for VirtualKeyboardKeyEvent { + fn key_code(&self) -> input::Keycode { + // Keycodes from the protocol are offset by 8 from raw xkb keycodes, + // matching `wl_keyboard` and evdev. + input::Keycode::from(self.key.saturating_add(8)) + } + + fn state(&self) -> input::KeyState { + self.state + } + + fn count(&self) -> u32 { + 1 + } +} + +/// Virtual keyboard requests that have no generic [`InputEvent`] equivalent. +/// +/// [`InputEvent`]: crate::backend::input::InputEvent +#[derive(Debug)] +pub enum VirtualKeyboardSpecialEvent { + /// The client replaced the keymap of a device that was already added. + /// + /// [`VirtualKeyboardDevice::keymap`] now returns the new one. + KeymapChanged { + /// The virtual keyboard device + device: VirtualKeyboardDevice, + }, + /// The client set the modifier state directly, rather than through key + /// presses. + /// + /// The masks are relative to the device's keymap (or the seat's, if it sent + /// no keymap) and must be applied only while that keymap is active, using + /// [`XkbContext::set_modifier_mask`](crate::input::keyboard::XkbContext::set_modifier_mask). + Modifiers { + /// The virtual keyboard device + device: VirtualKeyboardDevice, + /// Depressed (pressed) modifier mask + mods_depressed: u32, + /// Latched modifier mask + mods_latched: u32, + /// Locked modifier mask + mods_locked: u32, + /// Locked layout group index + group: u32, + }, +} diff --git a/vendor/smithay/src/wayland/virtual_keyboard/mod.rs b/vendor/smithay/src/wayland/virtual_keyboard/mod.rs new file mode 100644 index 0000000..b40508c --- /dev/null +++ b/vendor/smithay/src/wayland/virtual_keyboard/mod.rs @@ -0,0 +1,412 @@ +//! Implementation of the `zwp_virtual_keyboard_v1` protocol. +//! +//! A virtual keyboard is exposed as a regular input device instead of being +//! wired straight to the seat. Requests are converted into +//! [`VirtualKeyboardBackend`] [`InputEvent`]s and passed to +//! [`VirtualKeyboardHandler::process_virtual_keyboard_event`] so synthetic +//! input is handled the same way as a real one (making compositor bindings, +//! idle handling, and focus all behave properly). +//! +//! The device shows up (as [`InputEvent::DeviceAdded`]) only after the initial +//! keymap is sent since the keycodes don't mean anything without one. The +//! device is removed when the client destroys the keyboard, after releasing any +//! held keys (see [`InputEvent::DeviceRemoved`]). +//! +//! Modifiers set directly through the `modifiers` request are not reset on +//! DeviceRemoved since the masks are absolute, and a virtual keyboard using the +//! seat's keymap shares its modifier state with the other devices. The +//! compositor is responsible for choosing if/how to reconcile the modifier +//! state on virtual keyboard removal (e.g., by tracking the bits set by it). +//! +//! Some clients (e.g., IMEs like fcitx) grab the keyboard and forward unhandled +//! keys through a virtual keyboard of their own. Those must not be passed back +//! to the grab, otherwise it may loop infinitely. +//! +//! ```ignore +//! let grab_client = seat.input_method().keyboard_grab_client(); +//! if grab_client.is_some() && grab_client == device.client() { +//! keyboard.input_forward_bypassing_grab(state, keycode, key_state, serial, time, true); +//! } else { +//! keyboard.input_from_source(device.source(), state, keycode, key_state, serial, time, filter); +//! } +//! ``` +//! +//! Since the keycodes belong to the client's keymap, not the seat, it must be +//! activated (see [`VirtualKeyboardDevice::keymap`]) before handling them. A +//! client may send the `no_keymap` format to use the existing seat keymap, in +//! which case it will be `None`. +//! +//! Requests with no [`InputEvent`] equivalent are [`InputEvent::Special`] (see +//! [`VirtualKeyboardSpecialEvent`]). +//! +//! ``` +//! use smithay::backend::input::InputEvent; +//! use smithay::wayland::virtual_keyboard::{ +//! VirtualKeyboardManagerState, VirtualKeyboardBackend, VirtualKeyboardHandler, +//! }; +//! # use smithay::reexports::wayland_server::Display; +//! +//! # struct State; +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # let mut display = Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! impl VirtualKeyboardHandler for State { +//! fn process_virtual_keyboard_event(&mut self, event: InputEvent) { +//! // process_input_event(event); +//! } +//! } +//! +//! // The function decides which clients may create keyboards. +//! VirtualKeyboardManagerState::new::(&display_handle, |_client| true); +//! ``` + +use std::os::unix::io::OwnedFd; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use tracing::warn; +use wayland_protocols_misc::zwp_virtual_keyboard_v1::server::{ + zwp_virtual_keyboard_manager_v1::{self, ZwpVirtualKeyboardManagerV1}, + zwp_virtual_keyboard_v1::{self, ZwpVirtualKeyboardV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId, + backend::GlobalId, protocol::wl_keyboard::KeymapFormat, protocol::wl_seat::WlSeat, +}; +use xkbcommon::xkb; + +use crate::backend::input::{InputEvent, InputTime, KeyState}; +use crate::input::keyboard::KeyboardSource; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +const MANAGER_VERSION: u32 = 1; + +mod input; + +pub use input::{ + VirtualKeyboardBackend, VirtualKeyboardDevice, VirtualKeyboardKeyEvent, VirtualKeyboardSpecialEvent, +}; + +/// Handler for virtual keyboard input events. +pub trait VirtualKeyboardHandler { + /// Handle an event from a virtual keyboard. + /// + /// Process it the same way as any other + /// [`InputBackend`](crate::backend::input::InputBackend). + fn process_virtual_keyboard_event(&mut self, event: InputEvent); +} + +/// State of wp misc virtual keyboard protocol +#[derive(Debug)] +pub struct VirtualKeyboardManagerState { + global: GlobalId, +} + +/// Data associated with a VirtualKeyboardManager global. +#[allow(missing_debug_implementations)] +pub struct VirtualKeyboardManagerGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +impl VirtualKeyboardManagerState { + /// Initialize a virtual keyboard manager global. + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: VirtualKeyboardHandler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = VirtualKeyboardManagerGlobalData { + filter: Box::new(filter), + }; + let global = display.create_global::(MANAGER_VERSION, data); + + Self { global } + } + + /// Get the id of ZwpVirtualKeyboardManagerV1 global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for VirtualKeyboardManagerGlobalData +where + D: Dispatch, + D: Dispatch, + D: VirtualKeyboardHandler, + D: 'static, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: VirtualKeyboardHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ZwpVirtualKeyboardManagerV1, + request: zwp_virtual_keyboard_manager_v1::Request, + _handle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_virtual_keyboard_manager_v1::Request::CreateVirtualKeyboard { seat, id } => { + data_init.init( + id, + VirtualKeyboardUserData { + data: Arc::new(VirtualKeyboardData { + seat, + has_keymap: AtomicBool::new(false), + keymap: Mutex::new(None), + pressed_keys: Mutex::new(Vec::new()), + source: KeyboardSource::new_auxiliary(), + }), + }, + ); + } + _ => unreachable!(), + } + } +} + +/// User data for ZwpVirtualKeyboardV1 +#[derive(Debug)] +pub struct VirtualKeyboardUserData { + data: Arc, +} + +#[derive(Debug)] +struct VirtualKeyboardData { + seat: WlSeat, + /// Whether a keymap request was accepted. Even if set, there may still be + /// no keymap if `no_keymap` was explicitly requested. + has_keymap: AtomicBool, + keymap: Mutex>>, + pressed_keys: Mutex>, + source: KeyboardSource, +} + +impl VirtualKeyboardUserData { + fn device(&self, keyboard: &ZwpVirtualKeyboardV1) -> VirtualKeyboardDevice { + VirtualKeyboardDevice { + keyboard: keyboard.clone(), + data: self.data.clone(), + } + } +} + +impl Dispatch2 for VirtualKeyboardUserData +where + D: VirtualKeyboardHandler + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + virtual_keyboard: &ZwpVirtualKeyboardV1, + request: zwp_virtual_keyboard_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_virtual_keyboard_v1::Request::Keymap { format, fd, size } => { + let keymap = if format == KeymapFormat::NoKeymap as u32 { + // Use the current seat keymap to process keycodes. + None + } else if format == KeymapFormat::XkbV1 as u32 { + match compile_keymap(fd, size as usize) { + Some(keymap) => Some(Arc::from(keymap)), + // Client will get a `no_keymap` error on the next `key` + // request if no other keymap requests have succeeded + // yet. + None => return, + } + } else { + // TODO: uncomment this once zwp_virtual_keyboard_v1 is updated + // virtual_keyboard.post_error( + // zwp_virtual_keyboard_v1::Error::InvalidKeymapFormat, + // "invalid keymap format", + // ); + warn!("Client sent an unsupported keymap format: {format}"); + return; + }; + + *self.data.keymap.lock().unwrap() = keymap; + let added = !self.data.has_keymap.swap(true, Ordering::Relaxed); + + let device = self.device(virtual_keyboard); + if added { + state.process_virtual_keyboard_event(InputEvent::DeviceAdded { device }); + } else { + state.process_virtual_keyboard_event(InputEvent::Special( + VirtualKeyboardSpecialEvent::KeymapChanged { device }, + )); + } + } + zwp_virtual_keyboard_v1::Request::Key { + time, + key, + state: key_state, + } => { + if !self.data.has_keymap.load(Ordering::Relaxed) { + virtual_keyboard.post_error( + zwp_virtual_keyboard_v1::Error::NoKeymap, + "`key` sent before keymap.", + ); + return; + } + + // In wlroots, this is passed through if it isn't 0 or 1, but + // smithay key states are enums, so we need to choose pressed or + // released (repeated isn't available until wl_keyboard v10). + let key_state = match key_state { + 0 => KeyState::Released, + // Repeated is sent zero or more times after Pressed and + // before Released. + // 2 => KeyState::Repeated, + _ => KeyState::Pressed, + }; + + // Tracked so `destroyed` can release held keys. + { + let mut pressed = self.data.pressed_keys.lock().unwrap(); + match key_state { + KeyState::Pressed => { + if !pressed.contains(&key) { + pressed.push(key); + } + } + KeyState::Released => pressed.retain(|pressed| *pressed != key), + } + } + + state.process_virtual_keyboard_event(InputEvent::Keyboard { + event: VirtualKeyboardKeyEvent { + device: self.device(virtual_keyboard), + time, + key, + state: key_state, + }, + }); + } + zwp_virtual_keyboard_v1::Request::Modifiers { + mods_depressed, + mods_latched, + mods_locked, + group, + } => { + if !self.data.has_keymap.load(Ordering::Relaxed) { + virtual_keyboard.post_error( + zwp_virtual_keyboard_v1::Error::NoKeymap, + "`modifiers` sent before keymap.", + ); + return; + } + + state.process_virtual_keyboard_event(InputEvent::Special( + VirtualKeyboardSpecialEvent::Modifiers { + device: self.device(virtual_keyboard), + mods_depressed, + mods_latched, + mods_locked, + group, + }, + )); + } + zwp_virtual_keyboard_v1::Request::Destroy => { + // no-op; the device is removed in `destroyed` + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, virtual_keyboard: &ZwpVirtualKeyboardV1) { + // The device only exists once a keymap request was accepted. + if !self.data.has_keymap.load(Ordering::Relaxed) { + return; + } + + let device = self.device(virtual_keyboard); + + // A client may destroy the keyboard, or die, with keys still down. + // Release them here like how libinput does with an unplugged keyboard. + let pressed = std::mem::take(&mut *self.data.pressed_keys.lock().unwrap()); + let time = InputTime::now().millis(); + for key in pressed.into_iter().rev() { + state.process_virtual_keyboard_event(InputEvent::Keyboard { + event: VirtualKeyboardKeyEvent { + device: device.clone(), + time, + key, + state: KeyState::Released, + }, + }); + } + + // Modifiers set through the `modifiers` request are deliberately not + // cleared since the masks are absolute, and a virtual keyboard using + // the seat's keymap (either by sending `no_keymap`, or the same one) + // shares the seat's xkb state, so an all-zero mask would clear + // modifiers set by other devices. A virtual keyboard with its own + // keymap doesn't have this problem, since the seat's xkb state is + // rebuilt when the compositor switches keymaps back. + // + // The compositor should reconcile the modifier state itself on + // DeviceRemoved (e.g., based on the bits the virtual keyboard actually + // changed). + state.process_virtual_keyboard_event(InputEvent::DeviceRemoved { device }); + } +} + +/// Compile a client-supplied `xkb_v1` keymap, normalizing it to +/// `XKB_KEYMAP_FORMAT_TEXT_V1`, and returning it if valid. +fn compile_keymap(fd: OwnedFd, size: usize) -> Option { + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + // SAFETY: we can map the keymap into the memory. + let keymap = match unsafe { + xkb::Keymap::new_from_fd( + &context, + fd, + size, + xkb::KEYMAP_FORMAT_TEXT_V1, + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) + } { + Ok(Some(keymap)) => keymap, + Ok(None) => { + warn!("Failed to compile virtual keyboard keymap"); + return None; + } + Err(err) => { + warn!("Failed to map virtual keyboard keymap: {err:?}"); + return None; + } + }; + + Some(keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1)) +} diff --git a/vendor/smithay/src/wayland/virtual_pointer/input.rs b/vendor/smithay/src/wayland/virtual_pointer/input.rs new file mode 100644 index 0000000..299f3e6 --- /dev/null +++ b/vendor/smithay/src/wayland/virtual_pointer/input.rs @@ -0,0 +1,302 @@ +//! [`InputBackend`] event types for virtual pointer devices. + +use std::path::PathBuf; +use std::sync::Arc; + +use wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1; +use wayland_server::Resource; +use wayland_server::protocol::wl_seat::WlSeat; + +use crate::backend::input::{self, Axis, AxisSource, InputBackend, InputTime, UnusedEvent}; +use crate::output::{Output, WeakOutput}; + +use super::VirtualPointerData; + +/// Backend type identifying events from `zwlr_virtual_pointer_v1`. +/// +/// This backend is never constructed since virtual pointers are created by the +/// client; only the associated event types are used. +#[derive(Debug)] +pub struct VirtualPointerBackend; + +impl InputBackend for VirtualPointerBackend { + type Device = VirtualPointerDevice; + + type PointerAxisEvent = VirtualPointerAxisEvent; + type PointerButtonEvent = VirtualPointerButtonEvent; + type PointerMotionEvent = VirtualPointerMotionEvent; + type PointerMotionAbsoluteEvent = VirtualPointerMotionAbsoluteEvent; + + type KeyboardKeyEvent = UnusedEvent; + + type GestureSwipeBeginEvent = UnusedEvent; + type GestureSwipeUpdateEvent = UnusedEvent; + type GestureSwipeEndEvent = UnusedEvent; + type GesturePinchBeginEvent = UnusedEvent; + type GesturePinchUpdateEvent = UnusedEvent; + type GesturePinchEndEvent = UnusedEvent; + type GestureHoldBeginEvent = UnusedEvent; + type GestureHoldEndEvent = UnusedEvent; + + type TouchDownEvent = UnusedEvent; + type TouchUpEvent = UnusedEvent; + type TouchMotionEvent = UnusedEvent; + type TouchCancelEvent = UnusedEvent; + type TouchFrameEvent = UnusedEvent; + + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; + + type SwitchToggleEvent = UnusedEvent; + + type SpecialEvent = UnusedEvent; +} + +/// A pointer device created by a `zwlr_virtual_pointer_v1` client. +#[derive(Debug, Clone)] +pub struct VirtualPointerDevice { + pub(super) pointer: ZwlrVirtualPointerV1, + pub(super) data: Arc, +} + +impl PartialEq for VirtualPointerDevice { + fn eq(&self, other: &Self) -> bool { + self.pointer == other.pointer + } +} + +impl Eq for VirtualPointerDevice {} + +impl std::hash::Hash for VirtualPointerDevice { + fn hash(&self, state: &mut H) { + self.pointer.hash(state) + } +} + +impl VirtualPointerDevice { + /// The seat this virtual pointer was created for, if any. + pub fn wl_seat(&self) -> Option<&WlSeat> { + self.data.seat.as_ref() + } + + /// Output requested by the client, if it still exists. + /// + /// Absolute motion from this device is mapped onto this output (not the + /// entire output layout). + /// + /// The output is stored as a weak reference so a long-lived client doesn't + /// keep a removed output alive. + pub fn output(&self) -> Option { + self.data.output.as_ref().and_then(WeakOutput::upgrade) + } +} + +impl input::Device for VirtualPointerDevice { + fn id(&self) -> String { + format!("virtual-pointer-{}", self.pointer.id()) + } + + fn name(&self) -> String { + String::from("virtual pointer") + } + + fn has_capability(&self, capability: input::DeviceCapability) -> bool { + matches!(capability, input::DeviceCapability::Pointer) + } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option { + None + } +} + +/// A relative motion event generated by a virtual pointer. +#[derive(Debug)] +pub struct VirtualPointerMotionEvent { + pub(super) device: VirtualPointerDevice, + pub(super) time: u32, + pub(super) dx: f64, + pub(super) dy: f64, +} + +impl input::Event for VirtualPointerMotionEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> VirtualPointerDevice { + self.device.clone() + } +} + +impl input::PointerMotionEvent for VirtualPointerMotionEvent { + fn delta_x(&self) -> f64 { + self.dx + } + + fn delta_y(&self) -> f64 { + self.dy + } + + fn delta_x_unaccel(&self) -> f64 { + self.dx + } + + fn delta_y_unaccel(&self) -> f64 { + self.dy + } +} + +/// An absolute motion event generated by a virtual pointer. +/// +/// The position is normalized from the client-chosen extents into `0.0..=1.0`. +#[derive(Debug)] +pub struct VirtualPointerMotionAbsoluteEvent { + pub(super) device: VirtualPointerDevice, + pub(super) time: u32, + pub(super) x: u32, + pub(super) y: u32, + pub(super) x_extent: u32, + pub(super) y_extent: u32, +} + +/// Position along an axis as a `0.0..=1.0` fraction of the client's extent. +/// +/// Although the protocol bounds the position by the extent, this doesn't stop +/// the client from sending something outside it, so clamp it here. +fn normalized(position: u32, extent: u32) -> f64 { + f64::from(position.min(extent)) / f64::from(extent) +} + +impl input::Event for VirtualPointerMotionAbsoluteEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> VirtualPointerDevice { + self.device.clone() + } +} + +impl input::AbsolutePositionEvent for VirtualPointerMotionAbsoluteEvent { + fn x(&self) -> f64 { + normalized(self.x, self.x_extent) + } + + fn y(&self) -> f64 { + normalized(self.y, self.y_extent) + } + + fn x_transformed(&self, width: i32) -> f64 { + f64::max(self.x() * f64::from(width), 0.0) + } + + fn y_transformed(&self, height: i32) -> f64 { + f64::max(self.y() * f64::from(height), 0.0) + } +} + +impl input::PointerMotionAbsoluteEvent for VirtualPointerMotionAbsoluteEvent {} + +/// A button event generated by a virtual pointer. +#[derive(Debug)] +pub struct VirtualPointerButtonEvent { + pub(super) device: VirtualPointerDevice, + pub(super) time: u32, + pub(super) button: u32, + pub(super) state: input::ButtonState, +} + +impl input::Event for VirtualPointerButtonEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.time) + } + + fn device(&self) -> VirtualPointerDevice { + self.device.clone() + } +} + +impl input::PointerButtonEvent for VirtualPointerButtonEvent { + fn button_code(&self) -> u32 { + self.button + } + + fn state(&self) -> input::ButtonState { + self.state + } +} + +/// Scroll state accumulated for one axis between two `frame` requests. +#[derive(Debug, Default, Clone, Copy)] +pub(super) struct PendingAxis { + pub(super) value: f64, + /// Discrete steps, if the client sent an `axis_discrete` request for this + /// axis. [`None`] when it only sent `axis`, which carries no step count. + pub(super) v120: Option, +} + +/// Scroll state accumulated between two `frame` requests. +#[derive(Debug, Default)] +pub(super) struct PendingFrame { + pub(super) time: u32, + pub(super) source: Option, + /// Pending state for the horizontal and vertical axes. + pub(super) axes: [Option; 2], +} + +pub(super) fn axis_index(axis: Axis) -> usize { + match axis { + Axis::Horizontal => 0, + Axis::Vertical => 1, + } +} + +/// An axis event generated by a virtual pointer, covering one `frame`. +#[derive(Debug)] +pub struct VirtualPointerAxisEvent { + pub(super) device: VirtualPointerDevice, + pub(super) frame: PendingFrame, +} + +impl input::Event for VirtualPointerAxisEvent { + fn time(&self) -> InputTime { + InputTime::from_millis(self.frame.time) + } + + fn device(&self) -> VirtualPointerDevice { + self.device.clone() + } +} + +impl input::PointerAxisEvent for VirtualPointerAxisEvent { + fn amount(&self, axis: Axis) -> Option { + self.frame.axes[axis_index(axis)].map(|pending| pending.value) + } + + fn amount_v120(&self, axis: Axis) -> Option { + if !matches!(self.source(), AxisSource::Wheel | AxisSource::WheelTilt) { + return None; + } + + // Note this can be `None` for a wheel source, even though the trait + // says otherwise. A client may send `axis` without `axis_discrete` for + // a wheel. Don't specify it unless it's non-zero so the compositor + // falls back to `amount()` if it's unknown. + self.frame.axes[axis_index(axis)].and_then(|pending| pending.v120.map(f64::from)) + } + + fn source(&self) -> AxisSource { + // wlroots defaults to wheel when the client doesn't send `axis_source`. + self.frame.source.unwrap_or(AxisSource::Wheel) + } + + fn relative_direction(&self, _axis: Axis) -> input::AxisRelativeDirection { + input::AxisRelativeDirection::Identical + } +} diff --git a/vendor/smithay/src/wayland/virtual_pointer/mod.rs b/vendor/smithay/src/wayland/virtual_pointer/mod.rs new file mode 100644 index 0000000..486bed3 --- /dev/null +++ b/vendor/smithay/src/wayland/virtual_pointer/mod.rs @@ -0,0 +1,387 @@ +//! Implementation of the `zwlr_virtual_pointer_v1` protocol. +//! +//! A virtual pointer is exposed as a regular input device. Requests are +//! converted into [`VirtualPointerBackend`] [`InputEvent`]s and passed to +//! [`VirtualPointerHandler::process_virtual_pointer_event`] so synthetic input +//! is handled the same way as a real +//! [`InputBackend`](crate::backend::input::InputBackend). +//! +//! The device exists for as long as the client's pointer object does. +//! +//! Motion and button requests are forwarded as they arrive. Axis requests are +//! buffered until `frame` (since a scroll needs the separately sent value, +//! steps, and source), and are emitted as a single [`InputEvent::PointerAxis`]. +//! +//! ``` +//! use smithay::backend::input::InputEvent; +//! use smithay::wayland::virtual_pointer::{ +//! VirtualPointerManagerState, VirtualPointerBackend, VirtualPointerHandler, +//! }; +//! # use smithay::reexports::wayland_server::Display; +//! +//! # struct State; +//! +//! smithay::delegate_dispatch2!(State); +//! +//! # let mut display = Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! +//! impl VirtualPointerHandler for State { +//! fn process_virtual_pointer_event(&mut self, event: InputEvent) { +//! // process_input_event(event); +//! } +//! } +//! +//! // The function decides which clients may create pointers. +//! VirtualPointerManagerState::new::(&display_handle, |_client| true); +//! ``` + +use std::sync::{Arc, Mutex}; + +use tracing::debug; +use wayland_protocols_wlr::virtual_pointer::v1::server::{ + zwlr_virtual_pointer_manager_v1::{self, ZwlrVirtualPointerManagerV1}, + zwlr_virtual_pointer_v1::{self, ZwlrVirtualPointerV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, backend::ClientId, + backend::GlobalId, protocol::wl_pointer, protocol::wl_seat::WlSeat, +}; + +use crate::backend::input::{Axis, AxisSource, ButtonState, InputEvent, InputTime}; +use crate::output::{Output, WeakOutput}; +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +const MANAGER_VERSION: u32 = 2; + +mod input; + +use input::{PendingFrame, axis_index}; +pub use input::{ + VirtualPointerAxisEvent, VirtualPointerBackend, VirtualPointerButtonEvent, VirtualPointerDevice, + VirtualPointerMotionAbsoluteEvent, VirtualPointerMotionEvent, +}; + +/// Handler for virtual pointer input events. +pub trait VirtualPointerHandler { + /// Handle an event from a virtual pointer. + /// + /// Process it the same way as any other + /// [`InputBackend`](crate::backend::input::InputBackend). + fn process_virtual_pointer_event(&mut self, event: InputEvent); +} + +/// State of wlr virtual pointer protocol +#[derive(Debug)] +pub struct VirtualPointerManagerState { + global: GlobalId, +} + +/// Data associated with a VirtualPointerManager global. +#[allow(missing_debug_implementations)] +pub struct VirtualPointerManagerGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +impl VirtualPointerManagerState { + /// Initialize a virtual pointer manager global. + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: VirtualPointerHandler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let data = VirtualPointerManagerGlobalData { + filter: Box::new(filter), + }; + let global = display.create_global::(MANAGER_VERSION, data); + + Self { global } + } + + /// Get the id of ZwlrVirtualPointerManagerV1 global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for VirtualPointerManagerGlobalData +where + D: Dispatch, + D: Dispatch, + D: VirtualPointerHandler, + D: 'static, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: VirtualPointerHandler, + D: 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ZwlrVirtualPointerManagerV1, + request: zwlr_virtual_pointer_manager_v1::Request, + _handle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + let (id, seat, output) = match request { + zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => (id, seat, None), + zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput { seat, output, id } => { + let output = output.as_ref().and_then(|output| { + let resolved = Output::from_resource(output).map(|output| output.downgrade()); + if resolved.is_none() { + debug!("Virtual pointer requested an unknown output, using the whole layout"); + } + resolved + }); + (id, seat, output) + } + zwlr_virtual_pointer_manager_v1::Request::Destroy => return, + _ => unreachable!(), + }; + + let data = Arc::new(VirtualPointerData { + seat, + output, + frame: Mutex::new(PendingFrame::default()), + pressed_buttons: Mutex::new(Vec::new()), + }); + let pointer = data_init.init(id, VirtualPointerUserData { data: data.clone() }); + + state.process_virtual_pointer_event(InputEvent::DeviceAdded { + device: VirtualPointerDevice { pointer, data }, + }); + } +} + +/// User data for ZwlrVirtualPointerV1 +#[derive(Debug)] +pub struct VirtualPointerUserData { + data: Arc, +} + +#[derive(Debug)] +struct VirtualPointerData { + seat: Option, + output: Option, + frame: Mutex, + pressed_buttons: Mutex>, +} + +impl VirtualPointerUserData { + fn device(&self, pointer: &ZwlrVirtualPointerV1) -> VirtualPointerDevice { + VirtualPointerDevice { + pointer: pointer.clone(), + data: self.data.clone(), + } + } +} + +fn parse_axis(virtual_pointer: &ZwlrVirtualPointerV1, axis: WEnum) -> Option { + match axis { + WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Some(Axis::Horizontal), + WEnum::Value(wl_pointer::Axis::VerticalScroll) => Some(Axis::Vertical), + _ => { + virtual_pointer.post_error(zwlr_virtual_pointer_v1::Error::InvalidAxis, "invalid axis"); + None + } + } +} + +impl Dispatch2 for VirtualPointerUserData +where + D: VirtualPointerHandler + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + virtual_pointer: &ZwlrVirtualPointerV1, + request: zwlr_virtual_pointer_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_virtual_pointer_v1::Request::Motion { time, dx, dy } => { + state.process_virtual_pointer_event(InputEvent::PointerMotion { + event: VirtualPointerMotionEvent { + device: self.device(virtual_pointer), + time, + dx, + dy, + }, + }); + } + zwlr_virtual_pointer_v1::Request::MotionAbsolute { + time, + x, + y, + x_extent, + y_extent, + } => { + if x_extent == 0 || y_extent == 0 { + debug!("Dropping virtual pointer absolute motion with zero extents"); + return; + } + + state.process_virtual_pointer_event(InputEvent::PointerMotionAbsolute { + event: VirtualPointerMotionAbsoluteEvent { + device: self.device(virtual_pointer), + time, + x, + y, + x_extent, + y_extent, + }, + }); + } + zwlr_virtual_pointer_v1::Request::Button { + time, + button, + state: button_state, + } => { + // The protocol defines this as an enum, but wlroots treats it + // as a C boolean (zero or nonzero), so any invalid value counts + // as pressed. + let button_state = match button_state { + WEnum::Value(wl_pointer::ButtonState::Released) => ButtonState::Released, + _ => ButtonState::Pressed, + }; + + // Tracked so `destroyed` can release held buttons. + { + let mut pressed = self.data.pressed_buttons.lock().unwrap(); + match button_state { + ButtonState::Pressed => { + if !pressed.contains(&button) { + pressed.push(button); + } + } + ButtonState::Released => pressed.retain(|pressed| *pressed != button), + } + } + + state.process_virtual_pointer_event(InputEvent::PointerButton { + event: VirtualPointerButtonEvent { + device: self.device(virtual_pointer), + time, + button, + state: button_state, + }, + }); + } + zwlr_virtual_pointer_v1::Request::Axis { time, axis, value } => { + let Some(axis) = parse_axis(virtual_pointer, axis) else { + return; + }; + + let mut frame = self.data.frame.lock().unwrap(); + frame.time = frame.time.max(time); + frame.axes[axis_index(axis)] + .get_or_insert_with(Default::default) + .value = value; + } + zwlr_virtual_pointer_v1::Request::AxisSource { axis_source } => { + let axis_source = match axis_source { + WEnum::Value(wl_pointer::AxisSource::Wheel) => AxisSource::Wheel, + WEnum::Value(wl_pointer::AxisSource::Finger) => AxisSource::Finger, + WEnum::Value(wl_pointer::AxisSource::Continuous) => AxisSource::Continuous, + WEnum::Value(wl_pointer::AxisSource::WheelTilt) => AxisSource::WheelTilt, + _ => { + virtual_pointer.post_error( + zwlr_virtual_pointer_v1::Error::InvalidAxisSource, + "invalid axis source", + ); + return; + } + }; + + self.data.frame.lock().unwrap().source = Some(axis_source); + } + zwlr_virtual_pointer_v1::Request::AxisStop { time, axis } => { + let Some(axis) = parse_axis(virtual_pointer, axis) else { + return; + }; + + let mut frame = self.data.frame.lock().unwrap(); + frame.time = frame.time.max(time); + // A zero value without any discrete steps is a stop. + frame.axes[axis_index(axis)] = Some(Default::default()); + } + zwlr_virtual_pointer_v1::Request::AxisDiscrete { + time, + axis, + value, + discrete, + } => { + let Some(axis) = parse_axis(virtual_pointer, axis) else { + return; + }; + + let mut frame = self.data.frame.lock().unwrap(); + frame.time = frame.time.max(time); + let pending = frame.axes[axis_index(axis)].get_or_insert_with(Default::default); + pending.value = value; + pending.v120 = Some(discrete.saturating_mul(120)); + } + zwlr_virtual_pointer_v1::Request::Frame => { + let frame = std::mem::take(&mut *self.data.frame.lock().unwrap()); + if frame.axes.iter().any(Option::is_some) { + state.process_virtual_pointer_event(InputEvent::PointerAxis { + event: VirtualPointerAxisEvent { + device: self.device(virtual_pointer), + frame, + }, + }); + } + } + zwlr_virtual_pointer_v1::Request::Destroy => { + // no-op; the device is removed in `destroyed` + } + _ => unreachable!(), + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, virtual_pointer: &ZwlrVirtualPointerV1) { + let device = self.device(virtual_pointer); + + // A client may destroy the mouse, or die, with buttons still down. + // Release them here like how libinput does with an unplugged mouse. + let pressed = std::mem::take(&mut *self.data.pressed_buttons.lock().unwrap()); + let time = InputTime::now().millis(); + for button in pressed.into_iter().rev() { + state.process_virtual_pointer_event(InputEvent::PointerButton { + event: VirtualPointerButtonEvent { + device: device.clone(), + time, + button, + state: ButtonState::Released, + }, + }); + } + + state.process_virtual_pointer_event(InputEvent::DeviceRemoved { device }); + } +} diff --git a/vendor/smithay/src/wayland/xdg_activation/dispatch.rs b/vendor/smithay/src/wayland/xdg_activation/dispatch.rs new file mode 100644 index 0000000..b968059 --- /dev/null +++ b/vendor/smithay/src/wayland/xdg_activation/dispatch.rs @@ -0,0 +1,169 @@ +use std::sync::{ + Mutex, + atomic::{AtomicBool, Ordering}, +}; + +use wayland_protocols::xdg::activation::v1::server::{xdg_activation_token_v1, xdg_activation_v1}; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, New, Resource}; + +use super::{ActivationTokenData, TokenBuilder, XdgActivationHandler, XdgActivationTokenData}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +impl Dispatch2 for GlobalData +where + D: Dispatch + + XdgActivationHandler + + 'static, +{ + fn request( + &self, + state: &mut D, + _: &Client, + _: &xdg_activation_v1::XdgActivationV1, + request: xdg_activation_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + xdg_activation_v1::Request::Destroy => {} + + xdg_activation_v1::Request::GetActivationToken { id } => { + data_init.init( + id, + ActivationTokenData { + constructed: AtomicBool::new(false), + build: Mutex::new(TokenBuilder { + serial: None, + app_id: None, + surface: None, + }), + token: Mutex::new(None), + }, + ); + } + + xdg_activation_v1::Request::Activate { token, surface } => { + let token = token.into(); + + let activation_state = state.activation_state(); + + if let Some(token_data) = activation_state.known_tokens.get(&token).cloned() { + state.request_activation(token, token_data, surface); + } + } + + _ => unreachable!(), + } + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch + + Dispatch + + XdgActivationHandler + + 'static, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for ActivationTokenData +where + D: XdgActivationHandler, +{ + fn request( + &self, + state: &mut D, + client: &Client, + token: &xdg_activation_token_v1::XdgActivationTokenV1, + request: xdg_activation_token_v1::Request, + _dh: &DisplayHandle, + _: &mut DataInit<'_, D>, + ) { + match request { + xdg_activation_token_v1::Request::SetSerial { serial, seat } => { + if self.constructed.load(Ordering::Relaxed) { + token.post_error( + xdg_activation_token_v1::Error::AlreadyUsed, + "The activation token has already been constructed", + ); + return; + } + + self.build.lock().unwrap().serial = Some((serial.into(), seat)); + } + + xdg_activation_token_v1::Request::SetAppId { app_id } => { + if self.constructed.load(Ordering::Relaxed) { + token.post_error( + xdg_activation_token_v1::Error::AlreadyUsed, + "The activation token has already been constructed", + ); + return; + } + + self.build.lock().unwrap().app_id = Some(app_id); + } + + xdg_activation_token_v1::Request::SetSurface { surface } => { + if self.constructed.load(Ordering::Relaxed) { + token.post_error( + xdg_activation_token_v1::Error::AlreadyUsed, + "The activation token has already been constructed", + ); + return; + } + + self.build.lock().unwrap().surface = Some(surface); + } + + xdg_activation_token_v1::Request::Commit => { + if self.constructed.load(Ordering::Relaxed) { + token.post_error( + xdg_activation_token_v1::Error::AlreadyUsed, + "The activation token has already been constructed", + ); + return; + } + + self.constructed.store(true, Ordering::Relaxed); + + let (activation_token, token_data) = { + let mut guard = self.build.lock().unwrap(); + + XdgActivationTokenData::new( + Some(client.id()), + guard.serial.take(), + guard.app_id.take(), + guard.surface.take(), + ) + }; + + let valid = state.token_created(activation_token.clone(), token_data.clone()); + + *self.token.lock().unwrap() = Some(activation_token.clone()); + if valid { + state + .activation_state() + .known_tokens + .insert(activation_token.clone(), token_data); + } + token.done(activation_token.to_string()); + } + + xdg_activation_token_v1::Request::Destroy => {} + + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/xdg_activation/mod.rs b/vendor/smithay/src/wayland/xdg_activation/mod.rs new file mode 100644 index 0000000..37c213c --- /dev/null +++ b/vendor/smithay/src/wayland/xdg_activation/mod.rs @@ -0,0 +1,287 @@ +//! Utilities for handling activation requests with the `xdg_activation` protocol +//! +//! ### Example +//! +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use wayland_server::{protocol::wl_surface::WlSurface, DisplayHandle}; +//! use smithay::{ +//! wayland::xdg_activation::{XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData} +//! }; +//! +//! pub struct State { +//! activation_state: XdgActivationState, +//! } +//! +//! impl XdgActivationHandler for State { +//! fn activation_state(&mut self) -> &mut XdgActivationState { +//! &mut self.activation_state +//! } +//! +//! fn request_activation( +//! &mut self, +//! token: XdgActivationToken, +//! token_data: XdgActivationTokenData, +//! surface: WlSurface +//! ) { +//! if token_data.timestamp.elapsed().as_secs() < 10 { +//! // Request surface activation +//! } +//! } +//! } +//! +//! // Delegate xdg activation handling for State to XdgActivationState. +//! smithay::delegate_dispatch2!(State); +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! let state = State { +//! activation_state: XdgActivationState::new::(&display_handle), +//! }; +//! +//! // Rest of the compositor goes here... +//! ``` + +use std::{ + collections::HashMap, + ops, + sync::{Arc, Mutex, atomic::AtomicBool}, + time::Instant, +}; + +use wayland_protocols::xdg::activation::v1::server::xdg_activation_v1; +use wayland_server::{ + Dispatch, DisplayHandle, GlobalDispatch, + backend::{ClientId, GlobalId}, + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, +}; + +use rand::distr::{Alphanumeric, SampleString}; + +use crate::utils::{Serial, user_data::UserDataMap}; +use crate::wayland::GlobalData; + +mod dispatch; + +/// Contains the unique string token of activation request +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct XdgActivationToken(String); + +impl XdgActivationToken { + fn new() -> Self { + Self(Alphanumeric.sample_string(&mut rand::rng(), 32)) + } + + /// Extracts a string slice containing the entire token. + #[inline] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl ops::Deref for XdgActivationToken { + type Target = str; + #[inline] + fn deref(&self) -> &str { + &self.0 + } +} + +impl From for XdgActivationToken { + #[inline] + fn from(s: String) -> Self { + Self(s) + } +} + +impl From for String { + #[inline] + fn from(s: XdgActivationToken) -> Self { + s.0 + } +} + +/// Activation data asosiated with the [`XdgActivationToken`] + +#[derive(Debug, Clone)] +pub struct XdgActivationTokenData { + /// Client that requested the token + pub client_id: Option, + /// Provides information about the seat and serial event that requested the token. + /// + /// The serial can come from an input or focus event. + /// For instance, if a click triggers the launch of a third-party client, + /// this field should contain serial and seat from the wl_pointer.button event. + /// + /// Some compositors might refuse to activate toplevels + /// when the token doesn't have a valid and recent enough event serial. + pub serial: Option<(Serial, WlSeat)>, + /// The requesting client can specify an app_id to associate the token being created with it. + pub app_id: Option, + /// The surface requesting the activation. + /// + /// Note, this is different from the surface that will be activated. + pub surface: Option, + /// Timestamp of the token + /// + /// You can use this to ignore tokens based on time. + /// For example you could ignore all tokens older than 5s. + pub timestamp: Instant, + /// Additional user data attached + pub user_data: Arc, +} + +impl XdgActivationTokenData { + fn new( + client_id: Option, + serial: Option<(Serial, WlSeat)>, + app_id: Option, + surface: Option, + ) -> (XdgActivationToken, XdgActivationTokenData) { + ( + XdgActivationToken::new(), + XdgActivationTokenData { + client_id, + serial, + app_id, + surface, + timestamp: Instant::now(), + user_data: Arc::new(UserDataMap::new()), + }, + ) + } +} + +impl Default for XdgActivationTokenData { + fn default() -> Self { + Self { + client_id: None, + serial: None, + app_id: None, + surface: None, + timestamp: Instant::now(), + user_data: Arc::new(UserDataMap::new()), + } + } +} + +/// Tracks the list of pending and current activation requests +#[derive(Debug)] +pub struct XdgActivationState { + global: GlobalId, + known_tokens: HashMap, +} + +impl XdgActivationState { + /// Creates a new xdg activation global. + /// + /// In order to use this abstraction, your `D` type needs to implement [`XdgActivationHandler`]. + pub fn new(display: &DisplayHandle) -> XdgActivationState + where + D: GlobalDispatch + + Dispatch + + XdgActivationHandler + + 'static, + { + let global = display.create_global::(1, GlobalData); + + XdgActivationState { + global, + known_tokens: HashMap::new(), + } + } + + /// Create a token without any client association, e.g. for spawning processes from the compositor. + /// + /// This will not invoke [`XdgActivationHandler::token_created`] like client-created tokens, + /// instead use the return arguments to handle any initialization of the data you might need and to copy the token. + pub fn create_external_token( + &mut self, + data: impl Into>, + ) -> (&XdgActivationToken, &XdgActivationTokenData) { + let token = XdgActivationToken::new(); + let data = data.into().unwrap_or_default(); + self.known_tokens.insert(token.clone(), data); + self.known_tokens.get_key_value(&token).unwrap() + } + + /// Iterate over all known tokens and their associated data + pub fn tokens(&self) -> impl Iterator { + self.known_tokens.iter() + } + + /// Access the data of a known token + pub fn data_for_token(&self, token: &XdgActivationToken) -> Option<&XdgActivationTokenData> { + self.known_tokens.get(token) + } + + /// Retain pending tokens + /// + /// You may want to remove super old tokens + /// that were never turned into activation request for some reason + pub fn retain_tokens(&mut self, mut f: F) + where + F: FnMut(&XdgActivationToken, &XdgActivationTokenData) -> bool, + { + self.known_tokens.retain(|k, v| f(k, v)) + } + + /// Removes an activation token from the internal storage. + /// + /// Returns `true` if the token was found and subsequently removed. + pub fn remove_token(&mut self, token: &XdgActivationToken) -> bool { + self.known_tokens.remove(token).is_some() + } + + /// Returns the xdg activation global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +/// A trait implemented to be notified of activation requests using the xdg activation protocol. +pub trait XdgActivationHandler { + /// Returns the activation state. + fn activation_state(&mut self) -> &mut XdgActivationState; + + /// A client has created a new token. + /// + /// If the token isn't considered valid, it can be immediately untracked by returning `false`. + /// The default implementation considers every token valid and will always return `true`. + /// + /// This method may also be used to attach user_data to the token. + fn token_created(&mut self, token: XdgActivationToken, data: XdgActivationTokenData) -> bool { + let _ = (token, data); + true + } + + /// A client has requested surface activation. + /// + /// The compositor may know which client requested this by checking the token data and may decide whether + /// or not to follow through with the activation if it's considered unwanted. + /// + /// The token remains in the pool and might be used to issue other requests + /// until the compositor decides to remove it using [`XdgActivationState::remove_token`] or [`XdgActivationState::retain_tokens`]. + fn request_activation( + &mut self, + token: XdgActivationToken, + token_data: XdgActivationTokenData, + surface: WlSurface, + ); +} + +/// Data associated with an activation token protocol object. +#[derive(Debug)] +pub struct ActivationTokenData { + constructed: AtomicBool, + build: Mutex, + token: Mutex>, +} + +#[derive(Debug)] +struct TokenBuilder { + serial: Option<(Serial, WlSeat)>, + app_id: Option, + surface: Option, +} diff --git a/vendor/smithay/src/wayland/xdg_foreign/handlers.rs b/vendor/smithay/src/wayland/xdg_foreign/handlers.rs new file mode 100644 index 0000000..ce7c000 --- /dev/null +++ b/vendor/smithay/src/wayland/xdg_foreign/handlers.rs @@ -0,0 +1,314 @@ +use std::collections::HashSet; + +use wayland_protocols::xdg::foreign::zv2::server::{ + zxdg_exported_v2::{self, ZxdgExportedV2}, + zxdg_exporter_v2::{self, ZxdgExporterV2}, + zxdg_imported_v2::{self, ZxdgImportedV2}, + zxdg_importer_v2::{self, ZxdgImporterV2}, +}; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, New, Resource, backend::ClientId}; + +use crate::wayland::{ + Dispatch2, GlobalData, GlobalDispatch2, compositor, + shell::{ + is_valid_parent, + xdg::{XDG_TOPLEVEL_ROLE, XdgShellHandler, XdgToplevelSurfaceData}, + }, +}; + +use super::{ExportedState, XdgExportedUserData, XdgForeignHandle, XdgForeignHandler, XdgImportedUserData}; + +// +// Export +// + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: XdgForeignHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + resource: &ZxdgExporterV2, + request: zxdg_exporter_v2::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zxdg_exporter_v2::Request::ExportToplevel { id, surface } => { + if compositor::get_role(&surface) != Some(XDG_TOPLEVEL_ROLE) { + resource.post_error( + zxdg_exporter_v2::Error::InvalidSurface, + "exported surface had an invalid role", + ); + return; + } + + let handle = XdgForeignHandle::new(); + let exported = data_init.init( + id, + XdgExportedUserData { + handle: handle.clone(), + }, + ); + exported.handle(handle.as_str().to_owned()); + + state.xdg_foreign_state().exported.insert( + handle, + ExportedState { + exported_surface: surface, + requested_child: None, + imported_by: HashSet::new(), + }, + ); + } + zxdg_exporter_v2::Request::Destroy => {} + _ => {} + } + } +} + +impl Dispatch2 for XdgExportedUserData +where + D: XdgForeignHandler + XdgShellHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &ZxdgExportedV2, + _request: zxdg_exported_v2::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } + + fn destroyed(&self, state: &mut D, _client: ClientId, _resource: &ZxdgExportedV2) { + // Revoke the previously exported surface. + // This invalidates any relationship the importer may have set up using the xdg_imported created given the handle sent via xdg_exported.handle. + invalidate_all_relationships(state, &self.handle); + state.xdg_foreign_state().exported.remove(&self.handle); + } +} + +// +// Import +// + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ZxdgImporterV2, + request: zxdg_importer_v2::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zxdg_importer_v2::Request::ImportToplevel { id, handle } => { + let exported = state + .xdg_foreign_state() + .exported + .iter_mut() + .find(|(key, _)| key.as_str() == handle.as_str()); + + let imported = data_init.init( + id, + XdgImportedUserData { + handle: XdgForeignHandle(handle), + }, + ); + + match exported { + Some((_, state)) => { + state.imported_by.insert(imported); + } + None => { + imported.destroyed(); + } + } + } + zxdg_importer_v2::Request::Destroy => {} + _ => {} + } + } +} + +impl Dispatch2 for XdgImportedUserData +where + D: XdgForeignHandler + XdgShellHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + resource: &ZxdgImportedV2, + request: zxdg_imported_v2::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zxdg_imported_v2::Request::SetParentOf { surface: child } => { + if let Some((_, exported_state)) = state + .xdg_foreign_state() + .exported + .iter_mut() + .find(|(key, _)| key.as_str() == self.handle.as_str()) + { + let parent = &exported_state.exported_surface; + + let mut invalid = false; + let mut changed = false; + compositor::with_states(&child, |states| { + if let Some(data) = states.data_map.get::() { + if is_valid_parent(&child, parent) { + let mut role = data.lock().unwrap(); + changed = role.parent.as_ref() != Some(parent); + role.parent = Some(parent.clone()); + } else { + invalid = true; + } + } + }); + + if invalid { + resource.post_error( + zxdg_imported_v2::Error::InvalidSurface, + "invalid parent relationship", + ); + return; + } + + exported_state.requested_child = Some((child.clone(), resource.clone())); + + if changed { + if let Some(toplevel) = state + .xdg_shell_state() + .toplevel_surfaces() + .iter() + .find(|toplevel| *toplevel.wl_surface() == child) + .cloned() + { + XdgShellHandler::parent_changed(state, toplevel); + } + } + } + } + zxdg_imported_v2::Request::Destroy => {} + _ => {} + } + } + + fn destroyed(&self, state: &mut D, _client: ClientId, resource: &ZxdgImportedV2) { + if let Some((_, exported_state)) = state + .xdg_foreign_state() + .exported + .iter_mut() + .find(|(key, _)| key.as_str() == self.handle.as_str()) + { + exported_state.imported_by.remove(resource); + } + + invalidate_relationship_for(state, &self.handle, Some(resource)); + } +} + +fn invalidate_all_relationships(state: &mut D, handle: &XdgForeignHandle) +where + D: XdgForeignHandler + XdgShellHandler, +{ + invalidate_relationship_for(state, handle, None); +} + +fn invalidate_relationship_for( + state: &mut D, + handle: &XdgForeignHandle, + invalidate_for: Option<&ZxdgImportedV2>, +) where + D: XdgForeignHandler + XdgShellHandler, +{ + let Some((_, exported_state)) = state + .xdg_foreign_state() + .exported + .iter_mut() + .find(|(key, _)| key.as_str() == handle.as_str()) + else { + return; + }; + + let Some((requested_child, requested_by)) = exported_state.requested_child.as_ref() else { + return; + }; + + if let Some(invalidate_for) = invalidate_for { + if invalidate_for != requested_by { + return; + } + } + + let mut changed = false; + compositor::with_states(requested_child, |states| { + let Some(data) = states.data_map.get::() else { + return; + }; + + let data = &mut *data.lock().unwrap(); + if data.parent.as_ref() == Some(&exported_state.exported_surface) { + data.parent = None; + changed = true; + } + }); + + let requested_child = requested_child.clone(); + exported_state.requested_child = None; + + if changed { + if let Some(toplevel) = state + .xdg_shell_state() + .toplevel_surfaces() + .iter() + .find(|toplevel| *toplevel.wl_surface() == requested_child) + .cloned() + { + XdgShellHandler::parent_changed(state, toplevel); + } + } +} diff --git a/vendor/smithay/src/wayland/xdg_foreign/mod.rs b/vendor/smithay/src/wayland/xdg_foreign/mod.rs new file mode 100644 index 0000000..9ac99ba --- /dev/null +++ b/vendor/smithay/src/wayland/xdg_foreign/mod.rs @@ -0,0 +1,132 @@ +//! Implementation `xdg_foreign` protocol +//! +//! ```rs +//! # extern crate wayland_server; +//! # +//! use wayland_server::{protocol::wl_surface::WlSurface, DisplayHandle}; +//! use smithay::{ +//! delegate_xdg_foreign, +//! wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState} +//! }; +//! +//! pub struct State { +//! xdg_foreign_state: XdgForeignState, +//! } +//! +//! impl XdgForeignHandler for State { +//! fn xdg_foreignHandler_state(&mut self) -> &mut XdgForeignState { +//! &mut self.xdg_foreign_state +//! } +//! } +//! +//! // Delegate xdg foreign handling for State to XdgForeignState. +//! delegate_xdg_foreign!(State); +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! let state = State { +//! xdg_foreign_state: XdgForeignState::new::(&display_handle), +//! }; +//! ``` + +use std::{ + collections::{HashMap, HashSet}, + ops::Deref, +}; + +use rand::distr::{Alphanumeric, SampleString}; +use wayland_protocols::xdg::foreign::zv2::server::{ + zxdg_exporter_v2::ZxdgExporterV2, zxdg_imported_v2::ZxdgImportedV2, zxdg_importer_v2::ZxdgImporterV2, +}; +use wayland_server::{DisplayHandle, GlobalDispatch, backend::GlobalId, protocol::wl_surface::WlSurface}; + +use crate::wayland::GlobalData; + +mod handlers; + +/// A trait implemented to be notified of activation requests using the xdg foreign protocol. +pub trait XdgForeignHandler: 'static { + /// Returns the xdg foreign state. + fn xdg_foreign_state(&mut self) -> &mut XdgForeignState; +} + +/// The handle contains the unique handle of exported surface. +/// It may be shared with any client, which then can use it to import the surface by calling xdg_importer.import_toplevel. +/// A handle may be used to import the surface multiple times. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct XdgForeignHandle(String); + +impl XdgForeignHandle { + fn new() -> Self { + Self(Alphanumeric.sample_string(&mut rand::rng(), 32)) + } + + fn as_str(&self) -> &str { + &self.0 + } +} + +impl Deref for XdgForeignHandle { + type Target = str; + fn deref(&self) -> &str { + &self.0 + } +} + +/// User data of xdg_exported +#[derive(Debug)] +pub struct XdgExportedUserData { + handle: XdgForeignHandle, +} + +/// User data of xdg_imported +#[derive(Debug)] +pub struct XdgImportedUserData { + handle: XdgForeignHandle, +} + +#[derive(Debug)] +struct ExportedState { + exported_surface: WlSurface, + requested_child: Option<(WlSurface, ZxdgImportedV2)>, + imported_by: HashSet, +} + +/// Tracks the list of exported surfaces +#[derive(Debug)] +pub struct XdgForeignState { + exported: HashMap, + exporter: GlobalId, + importer: GlobalId, +} + +impl XdgForeignState { + /// Creates a new xdg activation global. + /// + /// In order to use this abstraction, your `D` type needs to implement [`XdgForeignHandler`]. + pub fn new(display: &DisplayHandle) -> Self + where + D: XdgForeignHandler, + D: GlobalDispatch, + D: GlobalDispatch, + { + let exporter = display.create_global::(1, GlobalData); + let importer = display.create_global::(1, GlobalData); + + Self { + exported: HashMap::new(), + exporter, + importer, + } + } + + /// Returns the xdg_exporter global. + pub fn exporter_global(&self) -> GlobalId { + self.exporter.clone() + } + + /// Returns the xdg_importer global. + pub fn importer_global(&self) -> GlobalId { + self.importer.clone() + } +} diff --git a/vendor/smithay/src/wayland/xdg_system_bell.rs b/vendor/smithay/src/wayland/xdg_system_bell.rs new file mode 100644 index 0000000..5006003 --- /dev/null +++ b/vendor/smithay/src/wayland/xdg_system_bell.rs @@ -0,0 +1,98 @@ +//! XDG System Bell +//! +//! This protocol enables clients to ring the system bell. +//! +//! In order to advertise system bell global call [`XdgSystemBellState::new`]. +//! +//! ``` +//! use smithay::wayland::xdg_system_bell::{XdgSystemBellState, XdgSystemBellHandler}; +//! use wayland_server::protocol::wl_surface::WlSurface; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! XdgSystemBellState::new::( +//! &display.handle(), +//! ); +//! +//! // provide the necessary trait implementations +//! impl XdgSystemBellHandler for State { +//! fn ring(&mut self, surface: Option) { +//! println!("Ring got called"); +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use wayland_protocols::xdg::system_bell::v1::server::xdg_system_bell_v1::{self, XdgSystemBellV1}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, backend::GlobalId, + protocol::wl_surface::WlSurface, +}; + +use crate::wayland::{Dispatch2, GlobalData, GlobalDispatch2}; + +/// Handler for xdg ring request +pub trait XdgSystemBellHandler: 'static { + /// Ring the system bell + fn ring(&mut self, surface: Option); +} + +/// State of the xdg system bell +#[derive(Debug)] +pub struct XdgSystemBellState { + global_id: GlobalId, +} + +impl XdgSystemBellState { + /// Register new [XdgSystemBellV1] global + pub fn new(display: &DisplayHandle) -> Self + where + D: XdgSystemBellHandler + GlobalDispatch, + { + let global_id = display.create_global::(1, GlobalData); + Self { global_id } + } + + /// [XdgSystemBellV1] GlobalId getter + pub fn global(&self) -> GlobalId { + self.global_id.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData { + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + _resource: &XdgSystemBellV1, + request: xdg_system_bell_v1::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + xdg_system_bell_v1::Request::Ring { surface } => { + state.ring(surface); + } + xdg_system_bell_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/xdg_toplevel_icon.rs b/vendor/smithay/src/wayland/xdg_toplevel_icon.rs new file mode 100644 index 0000000..2a22b1e --- /dev/null +++ b/vendor/smithay/src/wayland/xdg_toplevel_icon.rs @@ -0,0 +1,408 @@ +//! XDG Toplevel Icon +//! +//! This protocol allows clients to set icons for their toplevel surfaces either via the XDG icon stock (using an icon name), or from pixel data. +//! +//! In order to advertise toplevel icon global call [XdgToplevelIconManager::new]. +//! Currently attached icon is available in double-buffered [ToplevelIconCachedState] +//! +//! ``` +//! use smithay::wayland::xdg_toplevel_icon::{XdgToplevelIconManager, XdgToplevelIconHandler}; +//! use wayland_protocols::xdg::shell::server::xdg_toplevel::XdgToplevel; +//! use wayland_server::protocol::wl_surface::WlSurface; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! XdgToplevelIconManager::new::( +//! &display.handle(), +//! ); +//! +//! impl XdgToplevelIconHandler for State { +//! fn set_icon(&mut self, toplevel: XdgToplevel, wl_surface: WlSurface) { +//! dbg!(wl_surface); +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::{ + collections::HashSet, + sync::{Arc, Mutex, OnceLock}, +}; + +use wayland_protocols::xdg::{ + shell::server::xdg_toplevel::XdgToplevel, + toplevel_icon::v1::server::{ + xdg_toplevel_icon_manager_v1::{self, XdgToplevelIconManagerV1}, + xdg_toplevel_icon_v1::{self, XdgToplevelIconV1}, + }, +}; + +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId}, + protocol::{wl_buffer::WlBuffer, wl_surface::WlSurface}, +}; + +use crate::{ + utils::HookId, + wayland::{ + Dispatch2, GlobalData, GlobalDispatch2, + compositor::{self, Cacheable}, + shell::xdg::XdgShellSurfaceUserData, + shm::ShmBufferUserData, + }, +}; + +/// Handler trait for xdg toplevel icon events. +pub trait XdgToplevelIconHandler: 'static { + /// Called when icon becomes pending, and awaits surface commit. + /// Icon is stored in wl_surface [ToplevelIconCachedState] + fn set_icon(&mut self, toplevel: XdgToplevel, wl_surface: WlSurface) { + let _ = toplevel; + let _ = wl_surface; + } +} + +/// Data associated with WlSurface +/// Represents the client pending state +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::xdg_toplevel_icon::ToplevelIconCachedState; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// let mut data = states.cached_state.get::(); +/// dbg!(data.current().icon_name()); +/// }); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ToplevelIconCachedState { + icon: Option, +} + +impl ToplevelIconCachedState { + fn data(&self) -> Option<&IconData> { + let icon = self.icon.as_ref()?; + let data = icon + .data::() + .unwrap() + .constructed + .get() + .unwrap(); + Some(data) + } + + /// Icon name getter + pub fn icon_name(&self) -> Option<&str> { + self.data()?.icon_name.as_deref() + } + + /// List of icon buffers and buffer scale + pub fn buffers(&self) -> &[(WlBuffer, i32)] { + let Some(data) = self.data() else { + return &[]; + }; + &data.buffers + } +} + +impl Cacheable for ToplevelIconCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + self.clone() + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +#[derive(Debug, Default)] +struct IconData { + icon_name: Option, + buffers: Vec<(WlBuffer, i32)>, +} + +/// User data of [XdgToplevelIconV1] +#[derive(Debug, Default)] +pub struct XdgToplevelIconUserData { + builder: Mutex, + constructed: OnceLock, + buffer_destruction_hooks: Mutex>, +} + +impl XdgToplevelIconUserData { + fn freeze(&self) { + if self.constructed.get().is_none() { + let data = std::mem::take(&mut *self.builder.lock().unwrap()); + self.constructed.set(data).unwrap(); + } + } + + fn is_immutable(&self) -> bool { + self.constructed.get().is_some() + } + + fn set_icon_name(&self, icon_name: String) { + debug_assert!(!self.is_immutable()); + self.builder.lock().unwrap().icon_name = Some(icon_name) + } + + fn register_buffer_destruction_hook( + &self, + buffer: WlBuffer, + shm: &ShmBufferUserData, + hook: impl Fn() + Send + Sync + 'static, + ) { + let buffer_destruction_hook = shm.add_destruction_hook(move |_, _| hook()); + self.buffer_destruction_hooks + .lock() + .unwrap() + .push((buffer, buffer_destruction_hook)); + } + + fn unregister_buffer_destruction_hook(&self, buffer: &WlBuffer) { + let mut guard = self.buffer_destruction_hooks.lock().unwrap(); + + if let Some((buffer, hook)) = guard + .iter() + .position(|(b, _)| b == buffer) + .map(|id| guard.remove(id)) + { + if let Some(shm) = buffer.data::() { + shm.remove_destruction_hook(&hook); + } + } + } + + fn unregister_all_hooks(&self) { + let mut guard = self.buffer_destruction_hooks.lock().unwrap(); + + for (buffer, hook) in guard.drain(..) { + if let Some(shm) = buffer.data::() { + shm.remove_destruction_hook(&hook); + } + } + } + + fn add_buffer(&self, buffer: WlBuffer, scale: i32, shm: &ShmBufferUserData) { + debug_assert!(!self.is_immutable()); + + let mut guard = self.builder.lock().unwrap(); + let list = &mut guard.buffers; + + for (existing_buffer, existing_scale) in list.iter_mut() { + let existing_data = &existing_buffer.data::().unwrap().data; + + let same_width = existing_data.width == shm.data.width; + let same_height = existing_data.height == shm.data.height; + let same_scale = *existing_scale == scale; + + // If this icon instance already has a buffer of the same size and scale from a previous 'add_buffer' request, + // data from the last request overrides the preexisting pixel data. + if same_width && same_height && same_scale { + // The existing buffer will not be used, so let's assume we don't care about it's + // destruction + self.unregister_buffer_destruction_hook(existing_buffer); + *existing_buffer = buffer; + return; + } + } + + list.push((buffer, scale)); + } +} + +#[derive(Debug, Default)] +struct ManagerGlobalData { + sizes: HashSet, +} + +/// User data of [XdgToplevelIconManagerV1] global +#[derive(Debug)] +pub struct XdgToplevelIconManagerUserData(Arc>); + +/// Delegate type for handling xdg toplevel icon events. +#[derive(Debug)] +pub struct XdgToplevelIconManager { + global: GlobalId, + data: Arc>, +} + +impl XdgToplevelIconManager { + /// Creates a new delegate type for handling xdg toplevel icon events. + pub fn new(display: &DisplayHandle) -> Self + where + D: XdgToplevelIconHandler + GlobalDispatch, + { + let data = Arc::new(Mutex::new(ManagerGlobalData::default())); + let global = display + .create_global::(1, XdgToplevelIconManagerUserData(data.clone())); + XdgToplevelIconManager { global, data } + } + + /// Request given icon size + /// + /// - `size` the edge size of the square icon in surface-local coordinates + pub fn add_icon_size(&mut self, size: i32) { + self.data.lock().unwrap().sizes.insert(size); + } + + /// Replaces previously-set icon sizes with those in `sizes` + /// + /// The new icon sizes will only be sent to newly-bound globals, not to existing binds. + /// + /// - `sizes` an iterator over `i32` icon size values + pub fn replace_icon_sizes(&mut self, sizes: impl IntoIterator) { + let mut guard = self.data.lock().unwrap(); + guard.sizes.clear(); + guard.sizes.extend(sizes); + } + + /// Returns the [XdgToplevelIconManagerV1] global id. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 + for XdgToplevelIconManagerUserData +where + D: Dispatch, +{ + fn bind( + &self, + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + let manager = data_init.init(resource, GlobalData); + + for size in self.0.lock().unwrap().sizes.iter() { + manager.icon_size(*size); + } + manager.done(); + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, +{ + fn request( + &self, + state: &mut D, + _: &Client, + _resource: &XdgToplevelIconManagerV1, + request: xdg_toplevel_icon_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + use xdg_toplevel_icon_manager_v1::Request; + + match request { + Request::CreateIcon { id } => { + data_init.init(id, XdgToplevelIconUserData::default()); + } + Request::SetIcon { toplevel, icon } => { + let data = toplevel.data::().unwrap(); + let wl_surface = data.wl_surface.clone(); + + compositor::with_states(&data.wl_surface, |states| { + let mut state = states.cached_state.get::(); + let pending = state.pending(); + match icon { + Some(icon) => { + let icon_data = icon.data::().unwrap(); + icon_data.freeze(); + pending.icon = Some(icon); + } + None => { + *pending = Default::default(); + } + } + }); + + state.set_icon(toplevel, wl_surface); + } + Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for XdgToplevelIconUserData { + fn request( + &self, + _state: &mut D, + _: &Client, + icon: &XdgToplevelIconV1, + request: xdg_toplevel_icon_v1::Request, + _: &DisplayHandle, + _: &mut DataInit<'_, D>, + ) { + use xdg_toplevel_icon_v1::Request; + + match request { + Request::SetName { icon_name } => { + if self.is_immutable() { + icon.post_error( + xdg_toplevel_icon_v1::Error::Immutable, + "Request made after the icon has been assigned to a toplevel via 'set_icon'" + .to_string(), + ); + } + + self.set_icon_name(icon_name); + } + Request::AddBuffer { buffer, scale } => { + if self.is_immutable() { + icon.post_error( + xdg_toplevel_icon_v1::Error::Immutable, + "Request made after the icon has been assigned to a toplevel via 'set_icon'" + .to_string(), + ); + } + + let Some(shm) = buffer.data::() else { + icon.post_error( + xdg_toplevel_icon_v1::Error::InvalidBuffer, + "The wl_buffer must be backed by wl_shm".to_string(), + ); + return; + }; + + if shm.data.width != shm.data.height { + icon.post_error( + xdg_toplevel_icon_v1::Error::InvalidBuffer, + "The wl_buffer must be a square".to_string(), + ); + return; + }; + + // Let's listen for buffer destruction event to catch no_buffer protocol error + // This hook has to be unregistered once the icon is destroyed + self.register_buffer_destruction_hook(buffer.clone(), shm, { + let icon = icon.clone(); + move || { + icon.post_error( + xdg_toplevel_icon_v1::Error::NoBuffer, + "The provided buffer has been destroyed before the toplevel icon", + ) + } + }); + self.add_buffer(buffer.clone(), scale, shm); + } + Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed(&self, _state: &mut D, _client: ClientId, _resource: &XdgToplevelIconV1) { + self.unregister_all_hooks(); + } +} diff --git a/vendor/smithay/src/wayland/xdg_toplevel_tag.rs b/vendor/smithay/src/wayland/xdg_toplevel_tag.rs new file mode 100644 index 0000000..4e12c43 --- /dev/null +++ b/vendor/smithay/src/wayland/xdg_toplevel_tag.rs @@ -0,0 +1,181 @@ +//! XDG Toplevel Tag +//! +//! This protocol allows clients to set a tag and description of a toplevel. +//! +//! In order to advertise toplevel tag global call [XdgToplevelTagManager::new]. +//! Currently attached tag is available either via [XdgToplevelTagHandler] or in [XdgToplevelTagSurfaceData] +//! +//! ``` +//! use smithay::wayland::xdg_toplevel_tag::{XdgToplevelTagManager, XdgToplevelTagHandler}; +//! use wayland_protocols::xdg::shell::server::xdg_toplevel::XdgToplevel; +//! use wayland_server::protocol::wl_surface::WlSurface; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! +//! XdgToplevelTagManager::new::( +//! &display.handle(), +//! ); +//! +//! impl XdgToplevelTagHandler for State { +//! fn set_tag(&mut self, toplevel: XdgToplevel, tag: String) { +//! dbg!(tag); +//! } +//! +//! fn set_description(&mut self, toplevel: XdgToplevel, description: String) { +//! dbg!(description); +//! } +//! } +//! +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::sync::{Arc, Mutex}; + +use wayland_protocols::xdg::{ + shell::server::xdg_toplevel::XdgToplevel, + toplevel_tag::v1::server::xdg_toplevel_tag_manager_v1::{self, XdgToplevelTagManagerV1}, +}; + +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::GlobalId, +}; + +use crate::wayland::{ + Dispatch2, GlobalData, GlobalDispatch2, compositor, shell::xdg::XdgShellSurfaceUserData, +}; + +/// Data associated with WlSurface +/// Represents the client pending state +/// +/// ```no_run +/// use smithay::wayland::compositor; +/// use smithay::wayland::xdg_toplevel_tag::XdgToplevelTagSurfaceData; +/// +/// # let wl_surface = todo!(); +/// compositor::with_states(&wl_surface, |states| { +/// let mut data = states.data_map.get::().unwrap(); +/// dbg!(data.tag()); +/// dbg!(data.description()); +/// }); +/// ``` +#[derive(Default, Debug)] +pub struct XdgToplevelTagSurfaceData { + tag: Mutex>>, + description: Mutex>>, +} + +impl XdgToplevelTagSurfaceData { + /// Get tag of a toplevel. + /// + /// The tag may be shown to the user in UI, + /// so it's preferable for it to be human readable, + /// but it must be suitable for configuration files and should not be translated. + /// + /// Example of a tag would be "main window", "settings", "e-mail composer". + pub fn tag(&self) -> Option> { + self.tag.lock().unwrap().clone() + } + + /// Get description of a toplevel. + /// + /// This description may be shown to the user in UI or read by a screen reader for accessibility purposes. + pub fn description(&self) -> Option> { + self.description.lock().unwrap().clone() + } +} + +/// Handler trait for xdg toplevel tag events. +pub trait XdgToplevelTagHandler: 'static { + /// Toplevel tag was set/updated. + #[allow(unused)] + fn set_tag(&mut self, toplevel: XdgToplevel, tag: String) {} + + /// Toplevel description was set/updated. + #[allow(unused)] + fn set_description(&mut self, toplevel: XdgToplevel, description: String) {} +} + +/// Delegate type for handling xdg toplevel tag events. +#[derive(Debug)] +pub struct XdgToplevelTagManager { + global: GlobalId, +} + +impl XdgToplevelTagManager { + /// Creates a new delegate type for handling xdg toplevel tag events. + pub fn new(display: &DisplayHandle) -> Self + where + D: XdgToplevelTagHandler + GlobalDispatch, + { + let global = display.create_global::(1, GlobalData); + XdgToplevelTagManager { global } + } + + /// Returns the [XdgToplevelTagManagerV1] global id. + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } +} + +impl Dispatch2 for GlobalData { + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &XdgToplevelTagManagerV1, + request: xdg_toplevel_tag_manager_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + use xdg_toplevel_tag_manager_v1::Request; + + match request { + Request::SetToplevelTag { toplevel, tag } => { + let data = toplevel.data::().unwrap(); + + compositor::with_states(&data.wl_surface, |states| { + let data = states + .data_map + .get_or_insert_threadsafe(XdgToplevelTagSurfaceData::default); + *data.tag.lock().unwrap() = Some(tag.clone().into()); + }); + + state.set_tag(toplevel, tag); + } + Request::SetToplevelDescription { + toplevel, + description, + } => { + let data = toplevel.data::().unwrap(); + + compositor::with_states(&data.wl_surface, |states| { + let data = states + .data_map + .get_or_insert_threadsafe(XdgToplevelTagSurfaceData::default); + *data.description.lock().unwrap() = Some(description.clone().into()); + }); + + state.set_description(toplevel, description); + } + Request::Destroy => {} + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/xwayland_keyboard_grab.rs b/vendor/smithay/src/wayland/xwayland_keyboard_grab.rs new file mode 100644 index 0000000..8720a89 --- /dev/null +++ b/vendor/smithay/src/wayland/xwayland_keyboard_grab.rs @@ -0,0 +1,248 @@ +//! Utilities for handling the xwayland keyboard grab protocol +//! +//! ``` +//! use smithay::wayland::xwayland_keyboard_grab::{ +//! XWaylandKeyboardGrabHandler, +//! XWaylandKeyboardGrabState +//! }; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the keyboard grab state: +//! XWaylandKeyboardGrabState::new::( +//! &display.handle(), // the display +//! ); +//! # +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! # use smithay::input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}; +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! # impl SeatHandler for State { +//! # type KeyboardFocus = WlSurface; +//! # type PointerFocus = WlSurface; +//! # type TouchFocus = WlSurface; +//! # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } +//! # } +//! # impl PointerConstraintsHandler for State {} +//! +//! impl XWaylandKeyboardGrabHandler for State { +//! fn keyboard_focus_for_xsurface(&self, _: &WlSurface) -> Option { +//! // Return a `SeatHandler::KeyboardFocus` that the grab can use to set focus to the +//! // XWayland surface corresponding to this `WlSurface`. +//! todo!() +//! } +//! } +//! +//! // implement Dispatch for the keyboard grab types +//! smithay::delegate_dispatch2!(State); +//! ``` + +use wayland_protocols::xwayland::keyboard_grab::zv1::server::{ + zwp_xwayland_keyboard_grab_manager_v1::{self, ZwpXwaylandKeyboardGrabManagerV1}, + zwp_xwayland_keyboard_grab_v1::{self, ZwpXwaylandKeyboardGrabV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::GlobalId, + protocol::wl_surface, +}; + +use crate::{ + backend::input::{InputTime, KeyState, Keycode}, + input::{ + Seat, SeatHandler, + keyboard::{self, KeyboardGrab, KeyboardInnerHandle}, + }, + utils::{SERIAL_COUNTER, Serial}, + wayland::{Dispatch2, GlobalData, GlobalDispatch2}, + xwayland::XWaylandClientData, +}; + +const MANAGER_VERSION: u32 = 1; + +/// Handler for xwayland keyboard grab protocol +pub trait XWaylandKeyboardGrabHandler: SeatHandler { + /// XWayland client has requested a keyboard grab for `surface` on `seat` + /// + /// The default implementation calls `KeyboardHandle::set_grab` if `seat` + /// has a keyboard. + fn grab(&mut self, _surface: wl_surface::WlSurface, seat: Seat, grab: XWaylandKeyboardGrab) { + if let Some(keyboard) = seat.get_keyboard() { + keyboard.set_grab(self, grab, SERIAL_COUNTER.next_serial()); + } + } + + /// Defines what `KeyboardFocus` an `XWaylandKeyboardGrab` can use to focus the + /// `X11Surface` corresponding to `surface`. + /// + /// If this returns `None`, no grab is created. + fn keyboard_focus_for_xsurface(&self, surface: &wl_surface::WlSurface) -> Option; +} + +/// A grab created by xwayland keyboard grab protocol. +/// +/// This implements `KeyboardGrab`, and deactivates the grab when the corresponding +/// `ZwpXwaylandKeyboardGrabV1` is destroyed by the client. +#[derive(Debug)] +pub struct XWaylandKeyboardGrab { + grab: ZwpXwaylandKeyboardGrabV1, + start_data: keyboard::GrabStartData, +} + +impl XWaylandKeyboardGrab { + /// Get the `zwp_xwayland_keyboard_grab_v1` object that created the grab + pub fn grab(&self) -> &ZwpXwaylandKeyboardGrabV1 { + &self.grab + } +} + +impl Clone for XWaylandKeyboardGrab { + fn clone(&self) -> Self { + Self { + grab: self.grab.clone(), + start_data: self.start_data.clone(), + } + } +} + +impl KeyboardGrab for XWaylandKeyboardGrab { + fn input( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + keycode: Keycode, + state: KeyState, + modifiers: Option, + serial: Serial, + time: InputTime, + ) { + handle.set_focus(data, self.start_data.focus.clone(), serial); + + if !self.grab.is_alive() { + handle.unset_grab(self, data, serial, false); + } + + handle.input(data, keycode, state, modifiers, serial, time) + } + + fn set_focus( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + focus: Option<::KeyboardFocus>, + serial: Serial, + ) { + if !self.grab.is_alive() { + handle.unset_grab(self, data, serial, false); + handle.set_focus(data, focus, serial); + } + } + + fn start_data(&self) -> &keyboard::GrabStartData { + &self.start_data + } + + fn unset(&mut self, _data: &mut D) {} +} + +/// State of the xwayland keyboard grab global +#[derive(Debug)] +pub struct XWaylandKeyboardGrabState { + global: GlobalId, +} + +impl XWaylandKeyboardGrabState { + /// Register new [ZwpXwaylandKeyboardGrabManagerV1] global + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: 'static, + { + let global = + display.create_global::(MANAGER_VERSION, GlobalData); + + Self { global } + } + + /// [ZwpXwaylandKeyboardGrabV1] GlobalId getter + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch + 'static, +{ + fn bind( + &self, + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + client.get_data::().is_some() + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch + 'static, + D: XWaylandKeyboardGrabHandler, +{ + fn request( + &self, + state: &mut D, + _client: &wayland_server::Client, + _grab_manager: &ZwpXwaylandKeyboardGrabManagerV1, + request: zwp_xwayland_keyboard_grab_manager_v1::Request, + _dh: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_xwayland_keyboard_grab_manager_v1::Request::GrabKeyboard { id, surface, seat } => { + let grab = data_init.init(id, GlobalData); + if let Some(focus) = state.keyboard_focus_for_xsurface(&surface) { + let grab = XWaylandKeyboardGrab { + grab, + start_data: keyboard::GrabStartData { focus: Some(focus) }, + }; + let seat = Seat::from_resource(&seat).unwrap(); + state.grab(surface, seat, grab); + } + } + zwp_xwayland_keyboard_grab_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch2 for GlobalData { + fn request( + &self, + _state: &mut D, + _client: &wayland_server::Client, + _grab: &ZwpXwaylandKeyboardGrabV1, + request: zwp_xwayland_keyboard_grab_v1::Request, + _dh: &DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + match request { + zwp_xwayland_keyboard_grab_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} diff --git a/vendor/smithay/src/wayland/xwayland_shell.rs b/vendor/smithay/src/wayland/xwayland_shell.rs new file mode 100644 index 0000000..314bd2f --- /dev/null +++ b/vendor/smithay/src/wayland/xwayland_shell.rs @@ -0,0 +1,377 @@ +//! Helpers for handling the xwayland shell protocol +//! +//! # Example +//! +//! ``` +//! use smithay::wayland::xwayland_shell::{ +//! XWaylandShellHandler, +//! XWaylandShellState, +//! }; +//! use smithay::xwayland::xwm::{XwmId, X11Surface}; +//! +//! # struct State; +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! // Create the global: +//! XWaylandShellState::new::( +//! &display.handle(), +//! ); +//! # +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::backend::input::{InputTime, KeyState}; +//! # use smithay::input::{ +//! # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, +//! # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, +//! # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, +//! # GestureHoldBeginEvent, GestureHoldEndEvent}, +//! # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, +//! # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget, FrameMarker}, +//! # }; +//! # use smithay::utils::{IsAlive, Serial}; +//! # +//! # #[derive(Debug, Clone, PartialEq)] +//! # struct Target; +//! # impl IsAlive for Target { +//! # fn alive(&self) -> bool { true } +//! # } +//! # impl PointerTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) {} +//! # fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) {} +//! # fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) {} +//! # fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) {} +//! # fn frame(&self, seat: &Seat, data: &mut State) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: InputTime) {} +//! # fn gesture_swipe_begin(&self, seat: &Seat, data: &mut State, event: &GestureSwipeBeginEvent) {} +//! # fn gesture_swipe_update(&self, seat: &Seat, data: &mut State, event: &GestureSwipeUpdateEvent) {} +//! # fn gesture_swipe_end(&self, seat: &Seat, data: &mut State, event: &GestureSwipeEndEvent) {} +//! # fn gesture_pinch_begin(&self, seat: &Seat, data: &mut State, event: &GesturePinchBeginEvent) {} +//! # fn gesture_pinch_update(&self, seat: &Seat, data: &mut State, event: &GesturePinchUpdateEvent) {} +//! # fn gesture_pinch_end(&self, seat: &Seat, data: &mut State, event: &GesturePinchEndEvent) {} +//! # fn gesture_hold_begin(&self, seat: &Seat, data: &mut State, event: &GestureHoldBeginEvent) {} +//! # fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) {} +//! # } +//! # impl KeyboardTarget for Target { +//! # fn enter(&self, seat: &Seat, data: &mut State, keys: Vec>, serial: Serial) {} +//! # fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) {} +//! # fn key( +//! # &self, +//! # seat: &Seat, +//! # data: &mut State, +//! # key: KeysymHandle<'_>, +//! # state: KeyState, +//! # serial: Serial, +//! # time: InputTime, +//! # ) {} +//! # fn modifiers(&self, seat: &Seat, data: &mut State, modifiers: ModifiersState, serial: Serial) {} +//! # } +//! # impl TouchTarget for Target { +//! # fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) {} +//! # fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) {} +//! # fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) {} +//! # fn frame(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn cancel(&self, seat: &Seat, data: &mut State, marker: FrameMarker) {} +//! # fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) {} +//! # fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) {} +//! # fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { unimplemented!() } +//! # } +//! # impl SeatHandler for State { +//! # type KeyboardFocus = Target; +//! # type PointerFocus = Target; +//! # type TouchFocus = Target; +//! # fn seat_state(&mut self) -> &mut SeatState { unreachable!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) {} +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) {} +//! # } +//! +//! impl XWaylandShellHandler for State { +//! fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState { +//! // Return a reference to the state we created earlier. +//! todo!() +//! } +//! +//! fn surface_associated(&mut self, _xwm_id: XwmId, _surface: WlSurface, _window: X11Surface) { +//! // Called when XWayland has associated an X11 window with a wl_surface. +//! todo!() +//! } +//! } +//! +//! # use smithay::wayland::selection::SelectionTarget; +//! # use smithay::xwayland::{XWayland, XWaylandEvent, X11Wm, XwmHandler, xwm::{ResizeEdge, Reorder}}; +//! # use smithay::utils::{Rectangle, Logical}; +//! # use std::os::unix::io::OwnedFd; +//! # use std::process::Stdio; +//! +//! impl XwmHandler for State { +//! fn xwm_state(&mut self, xwm: XwmId) -> &mut X11Wm { +//! // ... +//! # unreachable!() +//! } +//! fn new_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn new_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn map_window_request(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn mapped_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn unmapped_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn destroyed_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn configure_request(&mut self, xwm: XwmId, window: X11Surface, x: Option, y: Option, w: Option, h: Option, reorder: Option) { /* ... */ } +//! fn configure_notify(&mut self, xwm: XwmId, window: X11Surface, geometry: Rectangle, above: Option) { /* ... */ } +//! fn resize_request(&mut self, xwm: XwmId, window: X11Surface, button: u32, resize_edge: ResizeEdge) { /* ... */ } +//! fn move_request(&mut self, xwm: XwmId, window: X11Surface, button: u32) { /* ... */ } +//! fn send_selection(&mut self, xwm: XwmId, selection: SelectionTarget, mime_type: String, fd: OwnedFd) { /* ... */ } +//! } +//! +//! // implement Dispatch for your state. +//! smithay::delegate_dispatch2!(State); +//! ``` + +use std::collections::HashMap; + +use tracing::{debug, warn}; +use wayland_protocols::xwayland::shell::v1::server::{ + xwayland_shell_v1::{self, XwaylandShellV1}, + xwayland_surface_v1::{self, XwaylandSurfaceV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::GlobalId, + protocol::wl_surface::{self, WlSurface}, +}; + +use crate::{ + input::SeatHandler, + wayland::{Dispatch2, GlobalData, GlobalDispatch2, compositor}, + xwayland::{X11Surface, XWaylandClientData, XwmHandler, xwm::XwmId}, +}; + +/// The role for an xwayland-associated surface. +pub const XWAYLAND_SHELL_ROLE: &str = "xwayland_shell"; + +const VERSION: u32 = 1; + +/// The global for the xwayland shell protocol. +#[derive(Debug, Clone)] +pub struct XWaylandShellState { + global: GlobalId, + by_serial: HashMap, +} + +impl XWaylandShellState { + /// Registers a new [XwaylandShellV1] global. Only XWayland clients will be + /// able to bind it. + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: 'static, + { + let global = display.create_global::(VERSION, GlobalData); + Self { + global, + by_serial: HashMap::new(), + } + } + + /// Retrieve a handle for the [XwaylandShellV1] global. + pub fn global(&self) -> GlobalId { + self.global.clone() + } + + /// Retrieves the surface for a given serial. + pub fn surface_for_serial(&self, serial: u64) -> Option { + self.by_serial.get(&serial).cloned() + } +} + +/// Userdata for an xwayland shell surface. +#[derive(Debug, Clone)] +pub struct XWaylandSurfaceUserData { + pub(crate) wl_surface: wl_surface::WlSurface, +} + +/// Handler for the xwayland shell protocol. +pub trait XWaylandShellHandler { + /// Retrieves the global state. + fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState; + + /// An X11 window has been associated with a wayland surface. This doesn't + /// take effect until the wl_surface is committed. + fn surface_associated(&mut self, xwm: XwmId, wl_surface: wl_surface::WlSurface, surface: X11Surface) { + let _ = (xwm, wl_surface, surface); + } +} + +/// Represents a pending X11 serial, used to associate X11 windows with wayland +/// surfaces. +#[derive(Debug, Default, Clone, Copy)] +pub struct XWaylandShellCachedState { + /// The serial of the matching X11 window. + pub serial: Option, +} + +impl compositor::Cacheable for XWaylandShellCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +impl GlobalDispatch2 for GlobalData +where + D: Dispatch, + D: 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, GlobalData); + } + + fn can_view(&self, client: &Client) -> bool { + client.get_data::().is_some() + } +} + +impl Dispatch2 for GlobalData +where + D: Dispatch, + D: XWaylandShellHandler + XwmHandler + SeatHandler, + D: 'static, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + resource: &XwaylandShellV1, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + xwayland_shell_v1::Request::GetXwaylandSurface { id, surface } => { + if compositor::give_role(&surface, XWAYLAND_SHELL_ROLE).is_err() { + resource.post_error(xwayland_shell_v1::Error::Role, "Surface already has a role."); + return; + } + + compositor::add_pre_commit_hook::(&surface, serial_commit_hook); + + data_init.init(id, XWaylandSurfaceUserData { wl_surface: surface }); + // We call the handler callback once the serial is set. + } + xwayland_shell_v1::Request::Destroy => { + // The child objects created via this interface are unaffected. + } + _ => unreachable!(), + } + } +} + +impl Dispatch2 for XWaylandSurfaceUserData +where + D: XWaylandShellHandler, + D: 'static, + D: XwmHandler, +{ + fn request( + &self, + _state: &mut D, + _client: &Client, + _resource: &XwaylandSurfaceV1, + request: ::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + // In order to match a WlSurface to an X11 window, we need to match a Serial sent both to the X11 window and the WlSurface. + // This can happen in any order so we need to store the Serial until we have both. + xwayland_surface_v1::Request::SetSerial { serial_lo, serial_hi } => { + let serial = u64::from(serial_lo) | (u64::from(serial_hi) << 32); + + // Set the serial on the pending state of surface + compositor::with_states(&self.wl_surface, |states| { + states + .cached_state + .get::() + .pending() + .serial = Some(serial); + }); + } + xwayland_surface_v1::Request::Destroy => { + // Any already existing associations are unaffected. + } + _ => unreachable!(), + } + } +} + +fn serial_commit_hook( + state: &mut D, + _dh: &DisplayHandle, + surface: &WlSurface, +) { + if let Some(serial) = compositor::with_states(surface, |states| { + states + .cached_state + .get::() + .pending() + .serial + }) { + if let Some(client) = surface.client() { + // We only care about surfaces created by XWayland. + if let Some(xwm_id) = client + .get_data::() + .and_then(|data| data.user_data().get::()) + { + let xwm = XwmHandler::xwm_state(state, *xwm_id); + + // This handles the case that the serial was set on the X11 + // window before surface. To handle the other case, we look for + // a matching surface when the WL_SURFACE_SERIAL atom is sent. + if let Some(window) = xwm.unpaired_surfaces.remove(&serial) { + if let Some(xsurface) = xwm + .windows + .iter() + .find(|x| x.window_id() == window || x.mapped_window_id() == Some(window)) + .cloned() + { + debug!( + window = xsurface.window_id(), + wl_surface = ?surface.id().protocol_id(), + "associated X11 window to wl_surface in commit hook", + ); + + xsurface.set_wl_surface(state, Some(surface.clone())); + + XWaylandShellHandler::surface_associated(state, *xwm_id, surface.clone(), xsurface); + } else { + warn!( + window, + wl_surface = ?surface.id().protocol_id(), + "Unknown X11 window associated to wl_surface in commit hook" + ) + } + } else { + // this is necessary for the atom-handler to look up the matching surface + XWaylandShellHandler::xwayland_shell_state(state) + .by_serial + .insert(serial, surface.clone()); + + compositor::add_destruction_hook::(surface, move |state, _| { + XWaylandShellHandler::xwayland_shell_state(state) + .by_serial + .remove(&serial); + }); + } + } + } + } +} diff --git a/vendor/smithay/src/xwayland/mod.rs b/vendor/smithay/src/xwayland/mod.rs new file mode 100644 index 0000000..7173b67 --- /dev/null +++ b/vendor/smithay/src/xwayland/mod.rs @@ -0,0 +1,21 @@ +// Allow in this module because of existing usage +#![allow(clippy::uninlined_format_args)] +//! XWayland utilities +//! +//! This module contains helpers to manage XWayland from your compositor, in order +//! to support running X11 apps. +//! +//! The starting point is the [`XWayland`] struct, which represents the +//! running XWayland instance. Dropping it will shutdown XWayland. +//! +//! You need to provide an implementation of a X11 Window Manager for XWayland to +//! function properly. You'll need to treat XWayland (and all its X11 apps) as one +//! special client, and play the role of an X11 Window Manager. +//! +//! Smithay does not provide any helper for doing that yet, but it is planned. +mod x11_sockets; +mod xserver; +pub mod xwm; + +pub use self::xserver::{XWayland, XWaylandClientData, XWaylandEvent}; +pub use self::xwm::{X11Surface, X11Wm, XwmHandler}; diff --git a/vendor/smithay/src/xwayland/x11_sockets.rs b/vendor/smithay/src/xwayland/x11_sockets.rs new file mode 100644 index 0000000..ddbd91b --- /dev/null +++ b/vendor/smithay/src/xwayland/x11_sockets.rs @@ -0,0 +1,184 @@ +use std::{ + io::{Read, Write}, + os::unix::net::UnixStream, +}; + +use tracing::{debug, info, warn}; + +use rustix::{io::Errno, net::SocketAddrUnix}; + +/// Find a free X11 display slot and setup +pub(crate) fn prepare_x11_sockets( + display: Option, + open_abstract_socket: bool, +) -> Result<(X11Lock, Vec), std::io::Error> { + match display { + Some(d) => { + if let Ok(lock) = X11Lock::grab(d) { + // we got a lockfile, try and create the socket + match open_x11_sockets_for_display(d, open_abstract_socket) { + Ok(sockets) => return Ok((lock, sockets)), + Err(err) => return Err(std::io::Error::from(err)), + }; + } + } + None => { + for d in 0..33 { + // if fails, try the next one + if let Ok(lock) = X11Lock::grab(d) { + // we got a lockfile, try and create the socket + match open_x11_sockets_for_display(d, open_abstract_socket) { + Ok(sockets) => return Ok((lock, sockets)), + Err(err) => warn!(display = d, "Failed to create sockets: {err}"), + } + } + } + // If we reach here, all values from 0 to 32 failed + // we need to stop trying at some point + } + } + + Err(std::io::Error::new( + std::io::ErrorKind::AddrInUse, + "Could not find a free socket for the XServer.", + )) +} + +#[derive(Debug)] +pub(crate) struct X11Lock { + display: u32, +} + +impl X11Lock { + /// Try to grab a lockfile for given X display number + fn grab(number: u32) -> Result { + debug!(display = number, "Attempting to acquire an X11 display lock"); + let filename = format!("/tmp/.X{number}-lock"); + let lockfile = ::std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&filename); + match lockfile { + Ok(mut file) => { + // we got it, write our PID in it and we're good + let ret = file.write_fmt(format_args!( + "{:>10}\n", + rustix::process::Pid::as_raw(Some(rustix::process::getpid())) + )); + if ret.is_err() { + // write to the file failed ? we abandon + ::std::mem::drop(file); + let _ = ::std::fs::remove_file(&filename); + Err(()) + } else { + debug!(display = number, "X11 lock acquired"); + // we got the lockfile and wrote our pid to it, all is good + Ok(X11Lock { display: number }) + } + } + Err(_) => { + debug!(display = number, "Failed to acquire lock"); + // we could not open the file, now we try to read it + // and if it contains the pid of a process that no longer + // exist (so if a previous x server claimed it and did not + // exit gracefully and remove it), we claim it + // if we can't open it, give up + let mut file = ::std::fs::File::open(&filename).map_err(|_| ())?; + let mut spid = [0u8; 11]; + file.read_exact(&mut spid).map_err(|_| ())?; + ::std::mem::drop(file); + let pid = rustix::process::Pid::from_raw( + ::std::str::from_utf8(&spid) + .map_err(|_| ())? + .trim() + .parse::() + .map_err(|_| ())?, + ) + .ok_or(())?; + if let Err(Errno::SRCH) = rustix::process::test_kill_process(pid) { + // no process whose pid equals the contents of the lockfile exists + // remove the lockfile and try grabbing it again + if let Ok(()) = ::std::fs::remove_file(filename) { + debug!( + display = number, + "Lock was blocked by a defunct X11 server, trying again" + ); + return X11Lock::grab(number); + } else { + // we could not remove the lockfile, abort + return Err(()); + } + } + // if we reach here, this lockfile exists and is probably in use, give up + Err(()) + } + } + } + + pub(crate) fn display_number(&self) -> u32 { + self.display + } +} + +impl Drop for X11Lock { + fn drop(&mut self) { + info!("Cleaning up X11 lock."); + // Cleanup all the X11 files + if let Err(e) = ::std::fs::remove_file(format!("/tmp/.X11-unix/X{}", self.display)) { + warn!(error = ?e, "Failed to remove X11 socket"); + } + if let Err(e) = ::std::fs::remove_file(format!("/tmp/.X{}-lock", self.display)) { + warn!(error = ?e, "Failed to remove X11 lockfile"); + } + } +} + +/// Open the two unix sockets an X server listens on +/// +/// Should only be done after the associated lockfile is acquired! +#[cfg(target_os = "linux")] +fn open_x11_sockets_for_display( + display: u32, + open_abstract_socket: bool, +) -> rustix::io::Result> { + let path = format!("/tmp/.X11-unix/X{display}"); + let _ = ::std::fs::remove_file(&path); + // We know this path is not too long, these unwrap cannot fail + let fs_addr = SocketAddrUnix::new(path.as_bytes()).unwrap(); + let mut sockets = vec![open_socket(fs_addr)?]; + if open_abstract_socket { + let abs_addr = SocketAddrUnix::new_abstract_name(path.as_bytes()).unwrap(); + sockets.push(open_socket(abs_addr)?); + } + Ok(sockets) +} + +/// Open the two unix sockets an X server listens on +/// +/// Should only be done after the associated lockfile is acquired! +#[cfg(not(target_os = "linux"))] +fn open_x11_sockets_for_display( + display: u32, + _open_abstract_socket: bool, +) -> rustix::io::Result> { + let path = format!("/tmp/.X11-unix/X{}", display); + let _ = ::std::fs::remove_file(&path); + // We know this path is not too long, these unwrap cannot fail + let fs_addr = SocketAddrUnix::new(path.as_bytes()).unwrap(); + Ok(vec![open_socket(fs_addr)?]) +} + +/// Open an unix socket for listening and bind it to given path +fn open_socket(addr: SocketAddrUnix) -> rustix::io::Result { + // create an unix stream socket + let fd = rustix::net::socket_with( + rustix::net::AddressFamily::UNIX, + rustix::net::SocketType::STREAM, + rustix::net::SocketFlags::CLOEXEC, + None, + )?; + // bind it to requested address + rustix::net::bind(&fd, &addr)?; + rustix::net::listen(&fd, 1)?; + Ok(UnixStream::from(fd)) +} diff --git a/vendor/smithay/src/xwayland/xserver.rs b/vendor/smithay/src/xwayland/xserver.rs new file mode 100644 index 0000000..4d2ef4c --- /dev/null +++ b/vendor/smithay/src/xwayland/xserver.rs @@ -0,0 +1,412 @@ +use std::{ + env, + ffi::OsStr, + os::fd::{BorrowedFd, OwnedFd}, + os::unix::{ + io::{AsRawFd, RawFd}, + net::UnixStream, + process::CommandExt, + }, + process::{Child, Command}, + sync::{Arc, Mutex}, + thread, +}; + +use tracing::{error, info, trace}; +use wayland_server::backend::{ClientData, ClientId, DisconnectReason}; +use wayland_server::{Client, DisplayHandle}; + +use crate::{utils::user_data::UserDataMap, wayland::compositor::CompositorClientState}; + +use super::x11_sockets::{X11Lock, prepare_x11_sockets}; + +/// A handle to a running XWayland process. Using XWayland as an xserver for +/// X11-based clients requires two connections: one wayland socket, where +/// XWayland creates surfaces for its clients, and one X11 socket, where a +/// compositor's implementation of an X11 window manager connects and handles +/// X11 events. +/// +/// ```text +/// ┌───────────────────┐ +/// │ │ +/// │ │ X11 Clients +/// │ Xwayland │ ◄───── +/// │ │ +/// │ │ +/// └──┬──────────────▲─┘ +/// │ │ +/// Wayland Socket│ │ X11 Socket +/// │ │ +/// │ │ +/// ┌────────────▼──────────────┼────────────┐ +/// │ Compositor │ │ +/// │ ┌─┴───────┐ │ +/// │ │ XWM │ │ +/// │ │ │ │ +/// │ └─────────┘ │ +/// │ │ +/// └────────────────────────────────────────┘ +/// ``` +/// +/// This struct handles integrating the XWayland process itself into the event +/// loop, but a [separate X11 window manager implementation](crate::xwayland::xwm::X11Wm) +/// is needed as well to support X11 clients. +/// +/// To shut down the instance, dropping this handle is generally sufficient, +/// along with the X11 connection that was passed to the window manager +/// implementation, if any. The process will die once the connections to it +/// are closed. +#[derive(Debug)] +pub struct XWayland { + inner: Arc>, + source: calloop::generic::Generic>, + + // So we can disconnect the client on drop. + dh: DisplayHandle, + client: Client, +} + +/// Events generated by an [XWayland] instance. +#[derive(Debug)] +pub enum XWaylandEvent { + /// The XWayland server is ready + Ready { + /// A privileged X11 connection to XWayland. + x11_socket: UnixStream, + + /// The display number the XWayland server is using. + /// + /// This can be useful to set the `DISPLAY` variable manually when + /// spawning processes that may use XWayland. + display_number: u32, + }, + + /// The XWayland server exited unexpectedly during startup. + Error, +} + +impl XWayland { + /// Spawns an XWayland server instance. `Xwayland` must be on the `PATH` and + /// executable. + /// + /// ## Arguments + /// + /// - `display` - if provided, only the given display number will be tested. + /// If you wish smithay to choose a display for you, pass `None`. + /// - `envs` - Allows additionally environment variables to be set when + /// launching XWayland. + /// - `extra_args` - Additional command-line arguments to pass to Xwayland + /// (e.g. `-enable-ei-portal`). + /// - `open_abstract_socket` - Open an abstract socket as well as filesystem + /// sockets (only on available on Linux). + /// - `stdout, stderr` - Allows redirecting stdout and stderr of the + /// XWayland process. XWayland output is rarely useful, so `Stdio::null()` + /// is a good choice if you're not sure. + /// - `user_data` - Allows mutating the `XWaylandClientData::user_data`-map + /// before the client is added to the wayland display. Useful for + /// initializing state for global filters. + /// + /// Returns a handle to the XWayland instance and the + /// [Client](wayland_server::Client) representing the XWayland server. The + /// handle can be inserted in your event loop, and If everything goes well, + /// you'll eventually receive an `XWaylandEvent::Ready`, indicating that + /// it's time to start the X11 window manager. + #[allow(clippy::too_many_arguments)] + pub fn spawn( + dh: &DisplayHandle, + display: impl Into>, + envs: I, + extra_args: AI, + open_abstract_socket: bool, + stdout: impl Into, + stderr: impl Into, + user_data: F, + ) -> std::io::Result<(Self, Client)> + where + I: IntoIterator, + K: AsRef, + V: AsRef, + AI: IntoIterator, + A: AsRef, + F: FnOnce(&UserDataMap), + { + let (x_wm_x11, x_wm_me) = UnixStream::pair()?; + let (wl_x11, wl_me) = UnixStream::pair()?; + + let (lock, listen_sockets) = prepare_x11_sockets(display.into(), open_abstract_socket)?; + let display_number = lock.display_number(); + + // XWayland writes the the display number and a newline to this pipe when it's ready. + let (displayfd_recv, displayfd_send) = + rustix::pipe::pipe_with(rustix::pipe::PipeFlags::NONBLOCK | rustix::pipe::PipeFlags::CLOEXEC)?; + + let mut command = Command::new("Xwayland"); + + command + .stdout(stdout) + .stderr(stderr) + .arg(format!(":{display_number}")) + .arg("-verbose") + .arg("-rootless") + .arg("-terminate") + .arg("-wm") + .arg(x_wm_x11.as_raw_fd().to_string()) + .arg("-displayfd") + .arg(displayfd_send.as_raw_fd().to_string()); + + for socket in &listen_sockets { + command.arg("-listenfd").arg(socket.as_raw_fd().to_string()); + } + + for arg in extra_args { + command.arg(arg); + } + + // Setup the environment; clear everything except PATH and XDG_RUNTIME_DIR. + command.env_clear(); + for (key, value) in env::vars_os() { + if key.to_str() == Some("PATH") || key.to_str() == Some("XDG_RUNTIME_DIR") { + command.env(key, value); + continue; + } + } + + command.env("WAYLAND_SOCKET", format!("{}", wl_x11.as_raw_fd())); + command.envs(envs); + + unsafe { + let wayland_socket_fd = wl_x11.as_raw_fd(); + let wm_socket_fd = x_wm_x11.as_raw_fd(); + let pipe_fd = displayfd_send.as_raw_fd(); + let socket_fds: Vec<_> = listen_sockets.iter().map(|socket| socket.as_raw_fd()).collect(); + + command.pre_exec(move || { + // unset the CLOEXEC flag from the sockets we need to pass + // to xwayland. + unset_cloexec(wayland_socket_fd)?; + unset_cloexec(wm_socket_fd)?; + unset_cloexec(pipe_fd)?; + for &socket in socket_fds.iter() { + unset_cloexec(socket)?; + } + + Ok(()) + }); + } + + info!("spawning XWayland instance"); + + let child = command.spawn()?; + + // SAFETY: RawFd's AsRawFd impl is infallible. + let wrapper = unsafe { calloop::generic::FdWrapper::new(displayfd_recv.as_raw_fd()) }; + let source = calloop::generic::Generic::new(wrapper, calloop::Interest::READ, calloop::Mode::Level); + let inner = Instance { + display_lock: lock, + display_fd: displayfd_recv, + x11_socket: Some(x_wm_me), + }; + + let data_map = UserDataMap::new(); + user_data(&data_map); + + // Insert the client into the display handle. The order is important + // here; XWayland never starts up at all unless it can roundtrip with + // wayland. + let inner = Arc::new(Mutex::new(inner)); + let mut dh = dh.clone(); + let client = dh.insert_client( + wl_me, + Arc::new(XWaylandClientData { + #[cfg(feature = "wayland_frontend")] + compositor_state: CompositorClientState::default(), + data_map, + child: Mutex::new(Some(child)), + }), + )?; + + Ok(( + Self { + inner, + source, + dh, + client: client.clone(), + }, + client, + )) + } + + /// Returns the X11 display used by the instance, suitable for setting the + /// `DISPLAY` environment variable. + pub fn display_number(&self) -> u32 { + self.inner.lock().unwrap().display_lock.display_number() + } + + /// Returns a file descriptor which can be polled for readiness. When the fd + /// is readable, the XWayland server's readiness can be checked with + /// [take_socket](Self::take_socket). + pub fn poll_fd(&self) -> BorrowedFd<'_> { + let guard = self.inner.lock().unwrap(); + + // SAFETY: we never mutate display_fd, and it lives as long as the instance. + unsafe { BorrowedFd::borrow_raw(guard.display_fd.as_raw_fd()) } + } + + /// Checks if the XWayland instance is ready. If XWayland has fully started, + /// this will return the X11 socket connected to the running instance. + /// + /// Calling `take_socket` successfully transfers ownership of the connection + /// to the caller. After returning `Some` the first time, it will always + /// return `None`. + /// + /// An `Err` result is only returned if the XWayland instance has exited + /// unexpectedly. + /// + /// This is a low-level method. Using the instance as an event source is the + /// recommended way to interact with it. + pub fn take_socket(&mut self) -> std::io::Result> { + self.inner.lock().unwrap().take_socket() + } +} + +#[derive(Debug)] +struct Instance { + display_lock: X11Lock, + x11_socket: Option, + display_fd: OwnedFd, +} + +impl calloop::EventSource for XWayland { + type Event = XWaylandEvent; + type Metadata = (); + type Ret = (); + type Error = std::io::Error; + + #[profiling::function] + fn process_events( + &mut self, + readiness: calloop::Readiness, + token: calloop::Token, + mut callback: F, + ) -> std::io::Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let mut guard = self.inner.lock().unwrap(); + + self.source.process_events(readiness, token, |_, _| { + let x11_socket = match guard.take_socket() { + Ok(Some(sockets)) => sockets, + Ok(None) => return Ok(calloop::PostAction::Continue), + Err(_) => { + callback(XWaylandEvent::Error, &mut ()); + return Ok(calloop::PostAction::Disable); + } + }; + + callback( + XWaylandEvent::Ready { + x11_socket, + display_number: guard.display_lock.display_number(), + }, + &mut (), + ); + + Ok(calloop::PostAction::Disable) + }) + } + + fn register( + &mut self, + poll: &mut calloop::Poll, + factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.source.register(poll, factory) + } + + fn reregister( + &mut self, + poll: &mut calloop::Poll, + factory: &mut calloop::TokenFactory, + ) -> calloop::Result<()> { + self.source.reregister(poll, factory) + } + + fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> { + self.source.unregister(poll) + } +} + +impl Drop for XWayland { + fn drop(&mut self) { + self.dh + .backend_handle() + .kill_client(self.client.id(), DisconnectReason::ConnectionClosed); + } +} + +impl Instance { + fn take_socket(&mut self) -> std::io::Result> { + trace!("checking for XWayland readiness"); + + if self.x11_socket.is_none() { + return Ok(None); + } + + let mut buf = [0; 64]; + loop { + let res = rustix::io::read(&self.display_fd, &mut buf); + trace!(?res, "read from XWayland displayfd"); + + match res { + Ok(0) => return Ok(None), + Ok(len) if (buf[..len]).contains(&b'\n') => return Ok(self.x11_socket.take()), + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => return Ok(None), + Err(err) => return Err(err.into()), + _ => continue, + } + } + } +} + +/// Inner `ClientData`-type of an xwayland client. +#[derive(Debug)] +pub struct XWaylandClientData { + /// client state of the [`crate::wayland::compositor`] module + #[cfg(feature = "wayland_frontend")] + pub compositor_state: CompositorClientState, + data_map: UserDataMap, + child: Mutex>, +} + +impl ClientData for XWaylandClientData { + fn disconnected(&self, _client_id: ClientId, reason: DisconnectReason) { + if let DisconnectReason::ProtocolError(err) = reason { + error!("Xwayland disconnected: {}", err); + } + + let mut child = self.child.lock().unwrap().take().unwrap(); + thread::spawn(move || { + if let Ok(status) = child.wait() { + if !status.success() { + error!("Xwayland terminated: {}", status); + } + } + }); + } +} + +impl XWaylandClientData { + /// Access user_data map for a xwayland client + pub fn user_data(&self) -> &UserDataMap { + &self.data_map + } +} + +/// Removes the `O_CLOEXEC` flag from a `RawFd`, causing it to leak when we +/// `exec` XWayland +unsafe fn unset_cloexec(fd: RawFd) -> std::io::Result<()> { + let fd = unsafe { BorrowedFd::borrow_raw(fd) }; + rustix::io::fcntl_setfd(fd, rustix::io::FdFlags::empty())?; + Ok(()) +} diff --git a/vendor/smithay/src/xwayland/xwm/dnd.rs b/vendor/smithay/src/xwayland/xwm/dnd.rs new file mode 100644 index 0000000..3e1a1d0 --- /dev/null +++ b/vendor/smithay/src/xwayland/xwm/dnd.rs @@ -0,0 +1,1222 @@ +use std::{ + any::Any, + cmp, + collections::HashMap, + fmt, + os::fd::OwnedFd, + sync::{ + Arc, Mutex, Weak, + atomic::{AtomicBool, Ordering}, + }, +}; + +use calloop::LoopHandle; +use portable_atomic::AtomicF64; +use rustix::fs::OFlags; +use smallvec::SmallVec; +use tracing::{debug, trace, warn}; +#[cfg(feature = "wayland_frontend")] +use wayland_server::DisplayHandle; +use x11rb::{ + CURRENT_TIME, + connection::Connection as _, + errors::{ConnectionError, ReplyOrIdError}, + protocol::{ + xfixes::SelectionNotifyEvent, + xproto::{ + Atom, AtomEnum, ClientMessageData, ClientMessageEvent, ConfigureWindowAux, ConnectionExt, + CreateWindowAux, EventMask, PropMode, Screen, StackMode, Window as X11Window, WindowClass, + }, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, +}; + +use crate::{ + backend::input::InputTime, + input::{ + Seat, SeatHandler, + dnd::{DnDGrab, DndAction, DndFocus, DndGrabHandler, OfferData, Source, SourceMetadata}, + pointer::Focus, + }, + utils::{IsAlive, Logical, Point, Serial}, + xwayland::{ + X11Surface, XwmHandler, + xwm::{Atoms, OwnedX11Window, XwmId, atom_from_mime, mime_from_atom, selection::XWmSelection}, + }, +}; + +// TODO: We should have a central source for this +const BTN_LEFT: u32 = 0x110; +const DND_VERSION: u32 = 5; +const MIN_DND_VERSION: u32 = 2; + +#[derive(Debug)] +pub struct XWmDnd { + pub selection: XWmSelection, + pub active_offer: Option, + pub active_drag: Option, + pub xdnd_active: Arc, +} + +impl XWmDnd { + pub fn new(conn: &Arc, screen: &Screen, atoms: &Atoms) -> Result { + let selection = XWmSelection::new(conn, screen, atoms, atoms.XdndSelection)?; + + conn.change_property32( + PropMode::REPLACE, + *selection.window, + atoms.XdndAware, + AtomEnum::ATOM, + &[DND_VERSION], + )?; + conn.flush()?; + + Ok(XWmDnd { + selection, + active_offer: None, + active_drag: None, + xdnd_active: Arc::new(AtomicBool::new(false)), + }) + } + + pub fn update_screen(&self, screen: &Screen) -> Result<(), ReplyOrIdError> { + if let Some(active_drag) = self.active_drag.as_ref() { + let mut drag_state = active_drag.state.lock().unwrap(); + + if drag_state.mapped { + self.selection.conn.configure_window( + *active_drag.target, + &ConfigureWindowAux::new() + .width(screen.width_in_pixels as u32) + .height(screen.height_in_pixels as u32), + )?; + } else { + drag_state.pending_configure = Some((screen.width_in_pixels, screen.height_in_pixels)); + } + } + + Ok(()) + } + + pub fn window_destroyed(&mut self, window: &X11Window, loop_handle: &LoopHandle<'_, D>) -> bool { + let mut res = self.selection.window_destroyed(window, loop_handle); + if let Some(active_drag) = self.active_drag.as_ref() { + if active_drag.owner == *window + || active_drag + .state + .lock() + .unwrap() + .source + .as_ref() + .is_some_and(|w| w == window) + { + self.active_drag.take(); + self.xdnd_active.store(false, Ordering::Release); + res = true; + } + } + res + } + + pub fn has_window(&self, window: &X11Window) -> bool { + if let Some(drag) = self.active_drag.as_ref() { + if drag.target == *window { + return true; + } + } + + self.selection.has_window(window) + } + + pub fn xfixes_selection_notify( + dh: &DisplayHandle, + data: &mut D, + id: XwmId, + event: SelectionNotifyEvent, + ) -> Result<(), ReplyOrIdError> + where + D: XwmHandler + SeatHandler + DndGrabHandler + 'static, + ::PointerFocus: DndFocus, + ::TouchFocus: DndFocus, + { + let xwm = data.xwm_state(id); + + xwm.dnd.selection.owner = event.owner; + if xwm.dnd.selection.owner == *xwm.dnd.selection.window { + xwm.dnd.selection.timestamp = event.timestamp; + return Ok(()); + } + + if xwm.dnd.active_offer.is_some() { + // rough X11 client tries to take over the selection, take it back. + xwm.conn + .set_selection_owner(*xwm.dnd.selection.window, xwm.atoms.XdndSelection, CURRENT_TIME)?; + return Ok(()); + } + + if let Some(active_drag) = xwm.dnd.active_drag.as_ref() { + if active_drag.owner == xwm.dnd.selection.owner + && active_drag.state.lock().unwrap().x11 != X11State::Finished + { + warn!("Got another selection notify for an already active grab. Ignoring..."); + return Ok(()); + } else { + // new drag, ours is presumably outdated/stale now + debug!("Dropping stale Xwm drag"); + + let active_drag = xwm.dnd.active_drag.take().unwrap(); + active_drag.state.lock().unwrap().x11 = X11State::Finished; + xwm.dnd.xdnd_active.store(false, Ordering::Release); + } + } + + if event.owner == x11rb::NONE { + trace!("XDND selection went away"); + xwm.dnd.active_drag.take(); + xwm.dnd.xdnd_active.store(false, Ordering::Release); + return Ok(()); + } + + trace!("New XDND selection from {}", event.owner); + // figure out, if we have a grab + let ptr_grab = data + .seat_state() + .seats + .iter() + .flat_map(|seat| { + seat.get_pointer().and_then(|ptr| { + ptr.with_grab(|serial, grab| (seat.clone(), serial, grab.start_data().clone())) + .filter(|(_, _, start_data)| start_data.button == BTN_LEFT) + }) + }) + .max_by(|(_, s1, _), (_, s2, _)| s1.partial_cmp(s2).unwrap_or(cmp::Ordering::Equal)); + + let touch_grab = data + .seat_state() + .seats + .iter() + .flat_map(|seat| { + seat.get_touch().and_then(|touch| { + touch.with_grab(|serial, grab| (seat.clone(), serial, grab.start_data().clone())) + }) + }) + .max_by(|(_, s1, _), (_, s2, _)| s1.partial_cmp(s2).unwrap_or(cmp::Ordering::Equal)); + + if ptr_grab.is_none() && touch_grab.is_none() { + return Ok(()); + } + + // create our drop proxy + let xwm = data.xwm_state(id); + let window = xwm.conn.generate_id()?; + xwm.conn.create_window( + xwm.screen.root_depth, + window, + xwm.screen.root, + 0, + 0, + xwm.screen.width_in_pixels, + xwm.screen.height_in_pixels, + 0, + WindowClass::INPUT_OUTPUT, + xwm.screen.root_visual, + &CreateWindowAux::new().event_mask(EventMask::PROPERTY_CHANGE), + )?; + xwm.conn.change_property32( + PropMode::REPLACE, + window, + xwm.atoms.XdndAware, + AtomEnum::ATOM, + &[DND_VERSION], + )?; + xwm.conn.change_property8( + PropMode::REPLACE, + window, + xwm.atoms.WM_NAME, + xwm.atoms.UTF8_STRING, + "Smithay XDND proxy".as_bytes(), + )?; + xwm.conn.map_window(window)?; + xwm.conn + .configure_window(window, &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE))?; + xwm.conn.flush()?; + + let window = OwnedX11Window::new(window, &xwm.conn); + + // create our dnd source + let state = Arc::new(Mutex::new(XwmSourceState { + mapped: true, + x11: X11State::Active, + wayland: WlState::Active, + source: None, + pending_configure: None, + metadata: SourceMetadata { + mime_types: vec![], + dnd_actions: SmallVec::new_const(), + }, + active_action: DndAction::None, + last_timestamp: None, + version: 0, + })); + let source = XwmDndSource { + xwm: id, + conn: Arc::downgrade(&xwm.conn), + atoms: xwm.atoms, + target: window.clone(), + state: state.clone(), + pending_transfers: xwm.dnd.selection.pending_transfers.clone(), + }; + xwm.dnd.active_drag = Some(XwmActiveDrag { + target: window, + owner: xwm.dnd.selection.owner, + state, + pending_transfers: xwm.dnd.selection.pending_transfers.clone(), + }); + xwm.dnd.xdnd_active.store(true, Ordering::Release); + + // create the DndGrab + match (ptr_grab, touch_grab) { + (Some((seat, s1, start_data)), Some((_, s2, _))) if s1 >= s2 => { + // pointer_grab + let grab = DnDGrab::new_pointer(dh, start_data, source, seat.clone()); + seat.get_pointer().unwrap().set_grab(data, grab, s1, Focus::Keep); + } + (Some((seat, serial, start_data)), None) => { + // pointer grab + let grab = DnDGrab::new_pointer(dh, start_data, source, seat.clone()); + seat.get_pointer() + .unwrap() + .set_grab(data, grab, serial, Focus::Keep); + } + (_, Some((seat, serial, start_data))) => { + // touch_grab + let grab = DnDGrab::new_touch(dh, start_data, source, seat.clone()); + seat.get_touch().unwrap().set_grab(data, grab, serial); + } + (None, None) => unreachable!(), + }; + + Ok(()) + } + + pub fn handle_status( + &mut self, + window: &X11Surface, + status_msg: ClientMessageData, + atoms: &Atoms, + client_scale: Arc, + ) { + if let Some(offer) = self.active_offer.as_ref() { + let data = status_msg.as_data32(); + let mut offer_state = offer.state.lock().unwrap(); + + if offer_state.target == data[0] { + let mut pos_update = false; + offer_state.client_accepts = (data[1] & 1) != 0; + + if !offer_state.dropped { + let old_action = offer_state.preferred_action; + let new_action = DndAction::from_x(data[4], atoms); + if offer_state.supported_actions.contains(&new_action) { + offer_state.preferred_action = new_action; + } else if offer_state.supported_actions.contains(&DndAction::Copy) { + offer_state.preferred_action = DndAction::Copy; + } else { + offer_state.preferred_action = DndAction::None; + } + + offer.source.choose_action(offer_state.preferred_action); + if old_action != offer_state.preferred_action { + pos_update = true; + } + } + + // TODO: rectangle in data[2] and data[3] for optimizations + + offer_state.pos_pending = false; + if let Some(pos) = offer_state + .pos_cached + .take() + .or(pos_update.then_some(offer_state.last_pos)) + { + let location = (window.last_configure().loc + pos.to_i32_round()) + .to_client_precise_round::<_, i32>(client_scale.load(Ordering::Acquire)); + + let data = [ + *self.selection.window, + 0, + ((location.x << 16) | location.y) as u32, + CURRENT_TIME, + offer_state.preferred_action.to_x(atoms), + ]; + + if let Err(err) = self.selection.conn.send_event( + false, + window.window_id(), + EventMask::NO_EVENT, + ClientMessageEvent::new(32, window.window_id(), atoms.XdndPosition, data), + ) { + warn!("Failed to send DND_POSITION event: {:?}", err); + } + let _ = self.selection.conn.flush(); + offer_state.pos_pending = true; + } else if offer_state.dropped { + // finish drop + + offer.source.drop_performed(); + + let data = [*self.selection.window, 0, CURRENT_TIME, 0, 0]; + if let Err(err) = self.selection.conn.send_event( + false, + window.window_id(), + EventMask::NO_EVENT, + ClientMessageEvent::new(32, window.window_id(), self.selection.atoms.XdndDrop, data), + ) { + warn!("Failed to send DND_DROP event: {:?}", err); + } + let _ = self.selection.conn.flush(); + } + } + } + } + + pub fn handle_finished(&mut self, status_msg: ClientMessageData) { + if let Some(offer) = self.active_offer.as_ref() { + let offer_state = offer.state.lock().unwrap(); + let data = status_msg.as_data32(); + trace!("Got XDND finished msg: {:?}", data); + + if data[0] != offer_state.target { + return; + } + + if !offer_state.dropped { + return; + } + + offer.source.finished(); + std::mem::drop(offer_state); + self.active_offer.take(); + } + } + + pub fn transfer(&mut self, mime_type: impl AsRef, send_fd: OwnedFd) { + if let Some(offer) = self.active_offer.as_ref() { + offer.source.send(mime_type.as_ref(), send_fd); + } + } + + pub fn handle_enter(&mut self, enter_msg: ClientMessageData) -> Result<(), ReplyOrIdError> { + if let Some(drag) = self.active_drag.as_mut() { + let mut drag_state = drag.state.lock().unwrap(); + let data = enter_msg.as_data32(); + + trace!("Got XDND enter msg: {:?}", data); + if drag_state.source.is_some_and(|w| w != data[0]) { + debug!( + "Received XdndEnter from unknown source (got: {}, state: {:?}), ignoring..", + data[0], drag_state.source + ); + return Ok(()); + } + drag_state.source = Some(data[0]); + let source = data[0]; + + let version = data[1] >> 24; + if version > DND_VERSION { + warn!("New unsupported XDND version: {}", data[1]); + return Ok(()); + } + + // get types + let mimes = if (data[1] & 1) == 0 { + (2..5) + .flat_map(|i| { + mime_from_atom(data[i], &self.selection.conn, &self.selection.atoms).transpose() + }) + .collect::, _>>()? + } else { + let reply = self + .selection + .conn + .get_property( + false, + source, + self.selection.atoms.XdndTypeList, + AtomEnum::ANY, + 0, + 0x1fffffff, + )? + .reply()?; + let Some(values) = reply.value32() else { + return Ok(()); + }; + + values + .flat_map(|atom| { + mime_from_atom(atom, &self.selection.conn, &self.selection.atoms).transpose() + }) + .collect::, _>>()? + }; + + drag_state.x11 = X11State::Active; + drag_state.metadata.mime_types = mimes; + drag_state.version = version; + } + + Ok(()) + } + + pub fn handle_position(&mut self, pos_msg: ClientMessageData) -> Result<(), ReplyOrIdError> { + if let Some(drag) = self.active_drag.as_ref() { + let mut drag_state = drag.state.lock().unwrap(); + + let data = pos_msg.as_data32(); + if drag_state.source.is_none_or(|w| w != data[0]) { + debug!( + "Received XdndPosition from unknown source (got: {}, expected: {:?}), ignoring..", + data[0], drag_state.source + ); + return Ok(()); + } + let source = data[0]; + + let mut actions = SmallVec::from_elem(DndAction::Copy, 1); // copy is always supported on XDND + if drag_state.version > 1 { + #[allow(clippy::single_match)] + match DndAction::from_x(data[4], &self.selection.atoms) { + DndAction::Move => actions.push(DndAction::Move), + _ => {} // We don't support Ask or Private atm, + } + } + + drag_state.last_timestamp = Some(data[3]); + drag_state.metadata.dnd_actions = actions; + + let mut flags = 1 << 1; + if !matches!(&drag_state.wayland, WlState::Cancelled) + && drag_state.active_action != DndAction::None + { + flags |= 1 << 0; + } + let data = [ + *drag.target, + flags, + 0, + 0, + drag_state.active_action.to_x(&self.selection.atoms), + ]; + self.selection.conn.send_event( + false, + source, + EventMask::NO_EVENT, + ClientMessageEvent::new(32, source, self.selection.atoms.XdndStatus, data), + )?; + self.selection.conn.flush()?; + + if matches!(&drag_state.wayland, &WlState::GotFd(_, _)) { + let mut wl_state = WlState::Active; + std::mem::swap(&mut drag_state.wayland, &mut wl_state); + + let WlState::GotFd(mime_type, fd) = wl_state else { + unreachable!() + }; + + let Some(atom) = atom_from_mime(&mime_type, &self.selection.conn, &self.selection.atoms)? + else { + warn!("Unable to determine atom for mime_type ({})", mime_type); + return Ok(()); + }; + + drag.pending_transfers + .lock() + .unwrap() + .insert(*drag.target, (drag.target.clone(), fd)); + + self.selection.conn.convert_selection( + drag.owner, + self.selection.atoms.XdndSelection, + atom, + self.selection.atoms._WL_SELECTION, + drag_state.last_timestamp.unwrap(), + )?; + self.selection.conn.flush()?; + } + } + Ok(()) + } + + pub fn handle_leave(&mut self, leave_msg: ClientMessageData) -> Result<(), ReplyOrIdError> { + if let Some(drag) = self.active_drag.as_mut() { + let mut state = drag.state.lock().unwrap(); + + let data = leave_msg.as_data32(); + trace!("Got XDND leave msg: {:?}", data); + + if state.source.is_none_or(|w| w != data[0]) { + debug!( + "Received XdndLeave from unknown source (got: {}, expected: {:?}), ignoring..", + data[0], state.source + ); + return Ok(()); + } + let _ = state.source.take(); + + state.x11 = X11State::Left; + + if matches!(&state.wayland, &WlState::GotFd(_, _) | &WlState::Dropped) { + state.wayland = WlState::Finished; + } + if matches!(&state.wayland, &WlState::Cancelled | &WlState::Finished) { + std::mem::drop(state); + self.active_drag.take(); + self.xdnd_active.store(false, Ordering::Release); + } + } + Ok(()) + } + + pub fn handle_drop(&mut self, drop_msg: ClientMessageData) -> Result<(), ReplyOrIdError> { + if let Some(drag) = self.active_drag.take() { + let mut state = drag.state.lock().unwrap(); + self.xdnd_active.store(false, Ordering::Release); + + let data = drop_msg.as_data32(); + trace!("Got XDND drop msg: {:?}", data); + + if state.source.is_none_or(|w| w != data[0]) { + debug!( + "Received XdndDrop from unknown source (got: {}, expected: {:?}), ignoring..", + data[0], state.source + ); + return Ok(()); + } + let source = data[0]; + + let conn = &self.selection.conn; + let atoms = &self.selection.atoms; + + let failed = || -> Result<(), ReplyOrIdError> { + drag.pending_transfers.lock().unwrap().remove(&drag.target); + + let data = [*drag.target, 0, 0, 0, 0]; + conn.send_event( + false, + source, + EventMask::NO_EVENT, + ClientMessageEvent::new(32, source, atoms.XdndFinished, data), + )?; + conn.flush()?; + + Ok(()) + }; + + if matches!(&state.wayland, &WlState::Cancelled | &WlState::Finished) { + failed()?; + return Ok(()); + } + + if state.version >= 1 { + state.last_timestamp = Some(data[2]); + } + + if matches!(&state.wayland, &WlState::GotFd(_, _)) { + let mut wl_state = WlState::Dropped; + std::mem::swap(&mut state.wayland, &mut wl_state); + + let WlState::GotFd(mime_type, fd) = wl_state else { + unreachable!() + }; + + drag.pending_transfers + .lock() + .unwrap() + .insert(*drag.target, (drag.target.clone(), fd)); + + let Some(atom) = atom_from_mime(&mime_type, &self.selection.conn, &self.selection.atoms)? + else { + warn!("Unable to determine atom for mime_type ({})", mime_type); + + failed()?; + return Ok(()); + }; + + conn.convert_selection( + drag.owner, + atoms.XdndSelection, + atom, + atoms._WL_SELECTION, + state.last_timestamp.unwrap_or(x11rb::CURRENT_TIME), + )?; + conn.flush()?; + } + + state.x11 = X11State::Dropped; + } + Ok(()) + } +} + +impl DndAction { + fn from_x(atom: Atom, atoms: &Atoms) -> DndAction { + match atom { + x if x == atoms.XdndActionCopy => DndAction::Copy, + x if x == atoms.XdndActionMove => DndAction::Move, + x if x == atoms.XdndActionAsk => DndAction::Ask, + _ => DndAction::None, + } + } + + fn to_x(self, atoms: &Atoms) -> Atom { + match self { + DndAction::Copy => atoms.XdndActionCopy, + DndAction::Move => atoms.XdndActionMove, + DndAction::Ask => atoms.XdndActionAsk, + DndAction::None => AtomEnum::NONE.into(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum X11State { + Active, + Left, + Dropped, + Finished, +} + +#[derive(Debug)] +enum WlState { + Active, + Cancelled, + Dropped, + GotFd(String, OwnedFd), + Finished, +} + +#[derive(Debug)] +struct XwmSourceState { + x11: X11State, + wayland: WlState, + source: Option, + + mapped: bool, + pending_configure: Option<(u16, u16)>, + + active_action: DndAction, + last_timestamp: Option, + version: u32, + + metadata: SourceMetadata, +} + +#[derive(Debug)] +pub struct XwmActiveDrag { + target: OwnedX11Window, + owner: X11Window, + + state: Arc>, + pending_transfers: Arc>>, +} + +#[derive(Debug)] +pub struct XwmDndSource { + xwm: XwmId, + conn: Weak, + atoms: Atoms, + + target: OwnedX11Window, + + state: Arc>, + pending_transfers: Arc>>, +} + +impl Drop for XwmDndSource { + fn drop(&mut self) { + if let Some(conn) = self.conn.upgrade() { + let mut state = self.state.lock().unwrap(); + state.wayland = WlState::Finished; + if state.mapped { + let _ = conn.unmap_window(*self.target); + let _ = conn.flush(); + } + } + } +} + +impl IsAlive for XwmDndSource { + fn alive(&self) -> bool { + let state = self.state.lock().unwrap(); + !matches!(&state.wayland, &WlState::Cancelled) + || (matches!(&state.wayland, &WlState::Finished) && state.x11 != X11State::Finished) + } +} + +impl Source for XwmDndSource { + fn metadata(&self) -> Option { + Some(self.state.lock().unwrap().metadata.clone()) + } + + fn choose_action(&self, action: DndAction) { + self.state.lock().unwrap().active_action = action; + } + + fn send(&self, mime_type: &str, fd: OwnedFd) { + let mut state = self.state.lock().unwrap(); + if !state.metadata.mime_types.iter().any(|m| m == mime_type) { + return; + } + + if let Err(err) = rustix::fs::fcntl_setfl(&fd, OFlags::WRONLY | OFlags::NONBLOCK) { + warn!(?err, "Failed to restrict file descriptor"); + } + + if let Some(conn) = self.conn.upgrade() { + trace!("XDND Transfer from xwayland -> wayland"); + + if let Some(timestamp) = state.last_timestamp { + let Ok(Some(atom)) = atom_from_mime(mime_type, &conn, &self.atoms) else { + warn!("Unable to determine atom for mime_type ({})", mime_type); + return; + }; + + self.pending_transfers + .lock() + .unwrap() + .insert(*self.target, (self.target.clone(), fd)); + + if let Err(err) = conn.convert_selection( + *self.target, + self.atoms.XdndSelection, + atom, + self.atoms._WL_SELECTION, + timestamp, + ) { + warn!("Failed to request selection for Dnd drop: {:?}", err); + self.pending_transfers.lock().unwrap().remove(&self.target); + return; + } + } else { + state.wayland = WlState::GotFd(mime_type.to_string(), fd); + } + + let _ = conn.flush(); + } + } + + fn drop_performed(&self) { + self.state.lock().unwrap().wayland = WlState::Dropped; + } + + fn cancel(&self) { + let mut state = self.state.lock().unwrap(); + state.wayland = WlState::Cancelled; + + if state.x11 == X11State::Dropped { + if let (Some(conn), Some(source)) = (self.conn.upgrade(), state.source) { + trace!("XDND cancelled, but already dropped, sending XDNDFinished"); + + let data = [*self.target, 0, 0, 0, 0]; + if let Err(err) = conn.send_event( + false, + source, + EventMask::NO_EVENT, + ClientMessageEvent::new(32, source, self.atoms.XdndFinished, data), + ) { + warn!("Failed to send XdndFinished: {:?}", err); + } + let _ = conn.flush(); + } + state.x11 = X11State::Finished; + } + } + + fn finished(&self) { + let mut state = self.state.lock().unwrap(); + + if state.x11 == X11State::Dropped { + if let (Some(conn), Some(source)) = (self.conn.upgrade(), state.source) { + trace!("XDND done, sending XDNDFinished"); + + let data = [ + *self.target, + (1 << 0), + state.active_action.to_x(&self.atoms), + 0, + 0, + ]; + if let Err(err) = conn.send_event( + false, + source, + EventMask::NO_EVENT, + ClientMessageEvent::new(32, source, self.atoms.XdndFinished, data), + ) { + warn!("Failed to send XdndFinished: {:?}", err); + } + let _ = conn.flush(); + } + state.x11 = X11State::Finished; + } + + state.wayland = WlState::Finished; + } + + fn is_client_local(&self, target: &dyn Any) -> bool { + target.downcast_ref::().is_some_and(|id| *id == self.xwm) + } +} + +#[derive(Debug)] +struct XwmOfferState { + target: X11Window, + proxy: Option, + + preferred_action: DndAction, + supported_actions: SmallVec<[DndAction; 3]>, + + pos_pending: bool, + last_pos: Point, + pos_cached: Option>, + + client_accepts: bool, + active: bool, + dropped: bool, +} + +pub struct XwmActiveOffer { + state: Arc>, + source: Arc, +} + +impl fmt::Debug for XwmActiveOffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("XwmActiveOffer") + .field("state", &self.state) + .finish_non_exhaustive() + } +} + +/// Type implementing [`OfferData`](crate::input::dnd::OfferData) for XDND based DnD sources +#[derive(Debug)] +pub struct XwmOfferData { + state: Arc>, + source: Arc, +} + +fn get_proxy_window( + window: X11Window, + conn: &impl ConnectionExt, + atoms: &Atoms, +) -> Result, ConnectionError> { + if let Some(prop) = conn + .get_property(false, window, atoms.XdndProxy, AtomEnum::WINDOW, 0, 1)? + .reply_unchecked()? + { + if let Some(proxy) = prop.value32().and_then(|mut val| val.next()) { + if let Some(prop) = conn + .get_property(false, proxy, atoms.XdndProxy, AtomEnum::WINDOW, 0, 1)? + .reply_unchecked()? + { + if let Some(verify) = prop.value32().and_then(|mut val| val.next()) { + if proxy == verify { + return Ok(Some(proxy)); + } + } + } + } + } + + Ok(None) +} + +impl DndFocus for X11Surface { + type OfferData + = XwmOfferData + where + S: Source; + + fn enter( + &self, + data: &mut D, + _dh: &DisplayHandle, + source: Arc, + seat: &Seat, + location: Point, + _serial: &Serial, + ) -> Option> { + let xwm_id = self.xwm_id()?; + let xwm = data.xwm_state(xwm_id); + + if source.is_client_local(&xwm_id) { + if let Some(active_drag) = xwm.dnd.active_drag.as_mut() { + trace!("XDND grab entered X11Surface, unmapping proxy"); + let mut drag_state = active_drag.state.lock().unwrap(); + if drag_state.mapped { + xwm.conn + .unmap_window(*active_drag.target) + .inspect_err(|err| warn!("Unable to unmap proxy dnd window: {}", err)) + .ok()?; + let _ = xwm.conn.flush(); + drag_state.last_timestamp = None; + drag_state.mapped = false; + } + } + + None + } else { + trace!("Wayland dnd grab entered Xwayland surface, taking over XDND_SELECTION"); + if xwm.dnd.selection.owner != *xwm.dnd.selection.window { + xwm.conn + .set_selection_owner(*xwm.dnd.selection.window, xwm.atoms.XdndSelection, CURRENT_TIME) + .inspect_err(|err| warn!("Failed to take X11 DND_SELECTION: {:?}", err)) + .ok()?; + } + + let proxy = get_proxy_window(self.window_id(), &xwm.conn, &self.atoms).ok()?; + let proxy_or_window = proxy.unwrap_or(self.window_id()); + + let prop = xwm + .conn + .get_property(false, proxy_or_window, self.atoms.XdndAware, AtomEnum::ANY, 0, 1) + .ok()? + .reply() + .ok()?; + if prop.type_ != AtomEnum::ATOM.into() { + return None; + } + let client_ver = prop.value32()?.next()?; + if client_ver < MIN_DND_VERSION { + return None; + } + let version = client_ver.min(DND_VERSION); + + let metadata = source.metadata()?; + let mime_types = metadata + .mime_types + .iter() + .filter_map(|mime| atom_from_mime(mime, &xwm.conn, &xwm.atoms).transpose()) + .collect::, _>>() + .inspect_err(|err| warn!("Failed to convert mime types: {:?}", err)) + .ok()?; + + let mut enter_data = [ + *xwm.dnd.selection.window, + version << 24, + AtomEnum::NONE.into(), + AtomEnum::NONE.into(), + AtomEnum::NONE.into(), + ]; + for (i, mime_type) in mime_types.iter().take(3).enumerate() { + enter_data[i + 2] = *mime_type; + } + + if mime_types.len() > 3 { + enter_data[1] |= 1; + xwm.conn + .change_property32( + PropMode::REPLACE, + *xwm.dnd.selection.window, + self.atoms.XdndTypeList, + AtomEnum::ATOM, + &mime_types, + ) + .inspect_err(|err| warn!("Failed to update DND_TYPE_LIST: {:?}", err)) + .ok()?; + } else { + xwm.conn + .delete_property(*xwm.dnd.selection.window, self.atoms.XdndTypeList) + .inspect_err(|err| warn!("Failed to update DND_TYPE_LIST: {:?}", err)) + .ok()?; + } + xwm.dnd.selection.mime_types = metadata.mime_types; + + trace!("Sending XdndEnter: {:?}", enter_data); + xwm.conn + .send_event( + false, + proxy_or_window, + EventMask::NO_EVENT, + ClientMessageEvent::new(32, self.window_id(), self.atoms.XdndEnter, enter_data), + ) + .inspect_err(|err| warn!("Failed to send DND_ENTER event: {:?}", err)) + .ok()?; + + let mut offer = XwmOfferData { + state: Arc::new(Mutex::new(XwmOfferState { + target: self.window_id(), + proxy, + + preferred_action: if metadata.dnd_actions.contains(&DndAction::Copy) { + DndAction::Copy + } else { + DndAction::None + }, + supported_actions: metadata.dnd_actions, + + last_pos: location, + pos_pending: false, + pos_cached: None, + + client_accepts: false, + active: true, + dropped: false, + })), + source, + }; + xwm.dnd.active_offer = Some(XwmActiveOffer { + state: offer.state.clone(), + source: offer.source.clone(), + }); + + DndFocus::motion(self, data, Some(&mut offer), seat, location, InputTime::now()); + Some(offer) + } + } + + fn motion( + &self, + data: &mut D, + offer: Option<&mut XwmOfferData>, + _seat: &Seat, + location: Point, + _time: InputTime, + ) { + let Some(offer) = offer else { return }; + let mut state = offer.state.lock().unwrap(); + if state.dropped { + return; + } + + let preferred = { + if state.pos_pending { + state.pos_cached = Some(location); + return; + } + state.pos_pending = true; + state.preferred_action + }; + + let Some(xwm_id) = self.xwm_id() else { return }; + let xwm = data.xwm_state(xwm_id); + let location = (self.last_configure().loc + location.to_i32_round()) + .to_client_precise_round::<_, i32>(xwm.client_scale.load(Ordering::Acquire)); + + let data = [ + *xwm.dnd.selection.window, + 0, + ((location.x << 16) | location.y) as u32, + CURRENT_TIME, + preferred.to_x(&xwm.atoms), + ]; + + if let Err(err) = xwm.conn.send_event( + false, + state.proxy.unwrap_or(self.window_id()), + EventMask::NO_EVENT, + ClientMessageEvent::new(32, self.window_id(), self.atoms.XdndPosition, data), + ) { + warn!("Failed to send DND_POSITION event: {:?}", err); + } + let _ = xwm.conn.flush(); + } + + fn leave(&self, data: &mut D, offer: Option<&mut XwmOfferData>, _seat: &Seat) { + let Some(xwm_id) = self.xwm_id() else { return }; + let Some(offer) = offer else { + // remap the proxy + trace!("XDND grab left X11Surface, remapping proxy"); + + let xwm = data.xwm_state(xwm_id); + if let Some(active_drag) = xwm.dnd.active_drag.as_mut() { + let mut drag_state = active_drag.state.lock().unwrap(); + if !drag_state.mapped { + if let Err(err) = xwm.conn.map_window(*active_drag.target) { + warn!("Unable to map proxy dnd window: {}", err); + return; + } + + let mut configure = ConfigureWindowAux::new().stack_mode(StackMode::ABOVE); + if let Some((w, h)) = drag_state.pending_configure.take() { + configure = configure.width(Some(w as u32)).height(Some(h as u32)); + } + if let Err(err) = xwm.conn.configure_window(*active_drag.target, &configure) { + warn!("Unable to configure proxy dnd window: {}", err); + return; + } + drag_state.mapped = true; + } + } + + return; + }; + + let state = offer.state.lock().unwrap(); + if !state.dropped { + let xwm = data.xwm_state(xwm_id); + let data = [*xwm.dnd.selection.window, 0, 0, 0, 0]; + trace!("Sending XdndLeave: {:?}", data); + + if let Err(err) = xwm.conn.send_event( + false, + state.proxy.unwrap_or(self.window_id()), + EventMask::NO_EVENT, + ClientMessageEvent::new(32, self.window_id(), self.atoms.XdndLeave, data), + ) { + warn!("Failed to send DND_LEAVE event: {:?}", err); + } + let _ = xwm.conn.flush(); + + xwm.dnd.active_offer = None; + } + } + + fn drop(&self, data: &mut D, offer: Option<&mut XwmOfferData>, _seat: &Seat) { + let Some(xwm_id) = self.xwm_id() else { return }; + let xwm = data.xwm_state(xwm_id); + + let Some(offer) = offer else { + // the x11 source was presumably dropped on an x11 window, so we are done here. + if let Some(active_drag) = xwm.dnd.active_drag.take() { + active_drag.state.lock().unwrap().x11 = X11State::Finished; + xwm.dnd.xdnd_active.store(false, Ordering::Release); + } + return; + }; + + let mut state = offer.state.lock().unwrap(); + state.dropped = true; + if state.pos_pending { + return; + } + + offer.source.drop_performed(); + + let data = [*xwm.dnd.selection.window, 0, CURRENT_TIME, 0, 0]; + trace!("Sending XdndDrop: {:?}", data); + if let Err(err) = xwm.conn.send_event( + false, + state.proxy.unwrap_or(self.window_id()), + EventMask::NO_EVENT, + ClientMessageEvent::new(32, self.window_id(), self.atoms.XdndDrop, data), + ) { + warn!("Failed to send DND_DROP event: {:?}", err); + } + let _ = xwm.conn.flush(); + } +} + +impl OfferData for XwmOfferData { + fn disable(&self) { + self.state.lock().unwrap().active = false; + } + + fn drop(&self) { + self.state.lock().unwrap().dropped = true; + } + + fn validated(&self) -> bool { + let state = self.state.lock().unwrap(); + state.active && state.preferred_action != DndAction::None && state.client_accepts + } +} diff --git a/vendor/smithay/src/xwayland/xwm/mod.rs b/vendor/smithay/src/xwayland/xwm/mod.rs new file mode 100644 index 0000000..e744669 --- /dev/null +++ b/vendor/smithay/src/xwayland/xwm/mod.rs @@ -0,0 +1,2889 @@ +//! +//! Xwayland Window Manager module +//! +//! Provides an [`X11Wm`] type, which will register itself as a window manager +//! for a previously spawned Xwayland instances, allowing +//! backwards-compatibility by seamlessly integrating X11 windows into a wayland +//! compositor. +//! +//! To use this functionality you must first spawn an +//! [`XWayland`](super::XWayland) instance to attach a [`X11Wm`] to. +//! +//! # Associating an [`X11Surface`] with a [`WlSurface`][wayland_server::protocol::wl_surface::WlSurface] +//! +//! When an X11 window is created, XWayland will associate it with a +//! `wl_surface` that it creates via the wayland connection. This happens in two +//! steps: +//! +//! - Via the [xwayland shell](crate::wayland::xwayland_shell) protocol, a +//! serial number is set on the surface, and it is given the +//! "xwayland_shell" role. +//! - Via the X11 connection, a matching `WL_SURFACE_SERIAL` atom is set on the +//! X11 window, which can be queried with +//! [`X11Surface::wl_surface_serial()`]. +//! +//! Note that these two steps can happen in any order. +//! +//! # Example +//! +//! ```no_run +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! # use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState}; +//! # use smithay::wayland::selection::{SelectionTarget, SelectionHandler, data_device::{DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler}}; +//! # use smithay::xwayland::{XWayland, XWaylandEvent, X11Wm, X11Surface, XwmHandler, xwm::{XwmId, ResizeEdge, Reorder}}; +//! # use smithay::utils::{Rectangle, Logical}; +//! # use std::os::unix::io::OwnedFd; +//! # use std::process::Stdio; +//! # +//! struct State { /* ... */ } +//! # impl XWaylandShellHandler for State { +//! # fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState { +//! # unreachable!() +//! # } +//! # } +//! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +//! # use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +//! # use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus, dnd::DndGrabHandler}; +//! # use smithay::backend::input::KeyState; +//! # use smithay::input::{ +//! # pointer::{PointerTarget, AxisFrame, MotionEvent, ButtonEvent, RelativeMotionEvent, +//! # GestureSwipeBeginEvent, GestureSwipeUpdateEvent, GestureSwipeEndEvent, +//! # GesturePinchBeginEvent, GesturePinchUpdateEvent, GesturePinchEndEvent, +//! # GestureHoldBeginEvent, GestureHoldEndEvent}, +//! # keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, +//! # touch::{DownEvent, UpEvent, MotionEvent as TouchMotionEvent, ShapeEvent, OrientationEvent, TouchTarget}, +//! # }; +//! # use smithay::utils::{IsAlive, Serial}; +//! # +//! # impl CompositorHandler for State { +//! # fn compositor_state(&mut self) -> &mut CompositorState { unimplemented!() } +//! # fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState { unimplemented!() } +//! # fn commit(&mut self, surface: &wayland_server::protocol::wl_surface::WlSurface) {} +//! # } +//! # +//! # type Target = WlSurface; +//! # impl SeatHandler for State { +//! # type KeyboardFocus = Target; +//! # type PointerFocus = Target; +//! # type TouchFocus = Target; +//! # fn seat_state(&mut self) -> &mut SeatState { unreachable!() } +//! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&Target>) {} +//! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) {} +//! # } +//! # impl PointerConstraintsHandler for State {} +//! # impl DndGrabHandler for State {} +//! # impl DataDeviceHandler for State { +//! # fn data_device_state(&mut self) -> &mut DataDeviceState { unreachable!() } +//! # } +//! # impl SelectionHandler for State { +//! # type SelectionUserData = (); +//! # } +//! # impl WaylandDndGrabHandler for State {} +//! +//! impl XwmHandler for State { +//! fn xwm_state(&mut self, xwm: XwmId) -> &mut X11Wm { +//! // ... +//! # unreachable!() +//! } +//! fn new_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn new_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn map_window_request(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn mapped_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn unmapped_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn destroyed_window(&mut self, xwm: XwmId, window: X11Surface) { /* ... */ } +//! fn configure_request(&mut self, xwm: XwmId, window: X11Surface, x: Option, y: Option, w: Option, h: Option, reorder: Option) { /* ... */ } +//! fn configure_notify(&mut self, xwm: XwmId, window: X11Surface, geometry: Rectangle, above: Option) { /* ... */ } +//! fn resize_request(&mut self, xwm: XwmId, window: X11Surface, button: u32, resize_edge: ResizeEdge) { /* ... */ } +//! fn move_request(&mut self, xwm: XwmId, window: X11Surface, button: u32) { /* ... */ } +//! fn send_selection(&mut self, xwm: XwmId, selection: SelectionTarget, mime_type: String, fd: OwnedFd) { /* ... */ } +//! } +//! # +//! # let dh = unreachable!(); +//! # let handle: smithay::reexports::calloop::LoopHandle<'static, State> = unreachable!(); +//! +//! let (xwayland, client) = XWayland::spawn( +//! &dh, +//! None, +//! std::iter::empty::<(String, String)>(), +//! std::iter::empty::(), +//! true, +//! Stdio::null(), +//! Stdio::null(), +//! |_| (), +//! ) +//! .expect("failed to start XWayland"); + +//! let ret = handle.insert_source(xwayland, move |event, _, data| match event { +//! XWaylandEvent::Ready { +//! x11_socket, +//! display_number: _, +//! } => { +//! let wm = X11Wm::start_wm( +//! handle.clone(), +//! &dh, +//! x11_socket, +//! client.clone(), +//! ) +//! .expect("Failed to attach X11 Window Manager"); +//! +//! // store the WM somewhere +//! } +//! XWaylandEvent::Error => eprintln!("XWayland failed to start!"), +//! }); if let Err(e) = ret { tracing::error!( "Failed to insert the +//! XWaylandSource into the event loop: {}", e ); } +//! ``` + +use crate::{ + input::{ + SeatHandler, + dnd::{DndFocus, DndGrabHandler}, + }, + output::Output, + utils::{Client, Logical, Point, Rectangle, Size, x11rb::X11Source}, + wayland::{ + selection::SelectionTarget, + xwayland_shell::{self, XWaylandShellHandler}, + }, +}; +use calloop::{Interest, LoopHandle, Mode, PostAction, generic::Generic, ping}; +use portable_atomic::AtomicF64; +use rustix::fs::OFlags; +use std::{ + cell::RefCell, + cmp::Reverse, + collections::{BinaryHeap, HashMap}, + ops::Deref, + os::unix::{ + io::{AsFd, OwnedFd}, + net::UnixStream, + }, + sync::{ + Arc, Weak, + atomic::{AtomicBool, Ordering}, + }, +}; +use tracing::{debug, debug_span, info, trace, warn}; +use wayland_server::{DisplayHandle, Resource}; + +pub use x11rb::protocol::xproto::Window as X11Window; +use x11rb::{ + connection::Connection as _, + errors::{ReplyError, ReplyOrIdError}, + properties::{WmHints, WmHintsState}, + protocol::{ + Event, + composite::{ConnectionExt as _, Redirect}, + randr::{ConnectionExt as _, Notify, NotifyMask}, + render::{ConnectionExt as _, CreatePictureAux, PictureWrapper}, + sync::{ConnectionExt as _, Counter}, + xfixes::ConnectionExt as _, + xproto::{ + AtomEnum, CONFIGURE_NOTIFY_EVENT, ChangeWindowAttributesAux, Colormap, ColormapAlloc, + ConfigWindow, ConfigureNotifyEvent, ConfigureWindowAux, ConnectionExt, CreateGCAux, + CreateWindowAux, CursorWrapper, EventMask, FontWrapper, GcontextWrapper, ImageFormat, InputFocus, + NotifyDetail, NotifyMode, PixmapWrapper, PropMode, Property, QueryExtensionReply, Screen, + StackMode, Visualid, WindowClass, + }, + }, + rust_connection::{ConnectionError, DefaultStream, RustConnection}, + wrapper::ConnectionExt as _, +}; + +mod dnd; +mod mwm; +pub use self::mwm::*; +pub mod settings; +use settings::{NameError, Value, XSettings}; +mod selection; +mod surface; +use self::dnd::XWmDnd; +pub use self::dnd::XwmOfferData; +use self::selection::*; +pub use self::surface::*; + +#[allow(missing_docs)] +mod atoms { + x11rb::atom_manager! { + /// Atoms used by the XWM and X11Surface types + pub Atoms: + AtomsCookie { + // wayland-stuff + WL_SURFACE_ID, + WL_SURFACE_SERIAL, + + // private + _SMITHAY_CLOSE_CONNECTION, + + // data formats + UTF8_STRING, + TEXT, + + // client -> server + WM_NAME, + WM_HINTS, + WM_PROTOCOLS, + WM_TAKE_FOCUS, + WM_DELETE_WINDOW, + WM_CHANGE_STATE, + _NET_WM_NAME, + _NET_WM_MOVERESIZE, + _NET_WM_PID, + _NET_WM_WINDOW_OPACITY, + _NET_WM_WINDOW_TYPE, + _NET_WM_WINDOW_TYPE_COMBO, + _NET_WM_WINDOW_TYPE_DESKTOP, + _NET_WM_WINDOW_TYPE_DND, + _NET_WM_WINDOW_TYPE_DOCK, + _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_WINDOW_TYPE_MENU, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_NORMAL, + _NET_WM_WINDOW_TYPE_POPUP_MENU, + _NET_WM_WINDOW_TYPE_SPLASH, + _NET_WM_WINDOW_TYPE_TOOLBAR, + _NET_WM_WINDOW_TYPE_TOOLTIP, + _NET_WM_WINDOW_TYPE_UTILITY, + _NET_WM_STATE_MODAL, + _NET_WM_OPAQUE_REGION, + _MOTIF_WM_HINTS, + _NET_STARTUP_ID, + _GTK_FRAME_EXTENTS, + + // server -> client + WM_S0, + WM_STATE, + _NET_WM_CM_S0, + _NET_SUPPORTED, + _NET_ACTIVE_WINDOW, + _NET_CLIENT_LIST, + _NET_CLIENT_LIST_STACKING, + _NET_WM_PING, + _NET_WM_STATE, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_DEMANDS_ATTENTION, + _NET_WM_STATE_FOCUSED, + _NET_WM_STATE_ABOVE, + _NET_WM_STATE_BELOW, + _NET_WM_STATE_SHADED, + _NET_WM_STATE_SKIP_TASKBAR, + _NET_WM_STATE_SKIP_PAGER, + _NET_WM_STATE_STICKY, + _NET_WM_SYNC_REQUEST, + _NET_WM_SYNC_REQUEST_COUNTER, + _NET_SHOWING_DESKTOP, + _NET_SUPPORTING_WM_CHECK, + _XSETTINGS_SETTINGS, + _XWAYLAND_ALLOW_COMMITS, + + // selection + _WL_SELECTION, + CLIPBOARD_MANAGER, + CLIPBOARD, + PRIMARY, + TARGETS, + TIMESTAMP, + INCR, + DELETE, + _XSETTINGS_S0, + + // dnd + XdndSelection, + XdndAware, + XdndStatus, + XdndPosition, + XdndEnter, + XdndLeave, + XdndDrop, + XdndFinished, + XdndProxy, + XdndTypeList, + XdndActionMove, + XdndActionCopy, + XdndActionAsk, + XdndActionPrivate, + } + } +} +pub use self::atoms::Atoms; + +use super::XWaylandClientData; + +crate::utils::ids::id_gen!(xwm_id); + +const XSYNC_REQUIRED_MAJOR_VERSION: u8 = 3; +const XSYNC_REQUIRED_MINOR_VERSION: u8 = 1; + +/// Id of an X11 WM +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct XwmId(usize); + +/// Window asks to be re-stacked +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Reorder { + /// to the top of the stack + Top, + /// directly above the given window id + Above(X11Window), + /// directly below the given window id + Below(X11Window), + /// to the bottom of the stack + Bottom, +} + +enum StackingDirection { + Downwards, + Upwards, +} + +impl StackingDirection { + fn pos_comparator(&self, pos: usize, last_pos: usize) -> bool { + match self { + Self::Downwards => last_pos < pos, + Self::Upwards => last_pos > pos, + } + } + + fn stack_mode(&self) -> StackMode { + match self { + Self::Downwards => StackMode::BELOW, + Self::Upwards => StackMode::ABOVE, + } + } +} + +/// Handler trait for X11Wm interactions +pub trait XwmHandler { + /// [`X11Wm`] getter for a given ID. + fn xwm_state(&mut self, xwm: XwmId) -> &mut X11Wm; + + /// A new X11 window was created. + /// + /// New windows are not mapped yet, but various information is already accessible. + /// In general new windows will either stay in this state, if they serve secondary purposes + /// or request to be mapped shortly afterwards. + fn new_window(&mut self, xwm: XwmId, window: X11Surface); + /// A new X11 window with the override redirect flag. + /// + /// New override_redirect windows are not mapped yet, but can become any time. + /// Window manager are not supposed to manage these windows and thus cannot intercept + /// most operations (including mapping). + /// + /// It is best to replicate their state in smithay as faithfully as possible (e.g. positioning) + /// and don't touch their state in any way. + fn new_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface); + /// Window requests to be mapped. + /// + /// To grant the wish you have to call `X11Surface::set_mapped(true)` for the window to become visible. + fn map_window_request(&mut self, xwm: XwmId, window: X11Surface); + /// Notification a window was mapped successfully + fn map_window_notify(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Override redirect window was mapped. + /// + /// This is a notification. The XWM cannot prohibit override redirect windows to become mapped. + fn mapped_override_redirect_window(&mut self, xwm: XwmId, window: X11Surface); + /// Window was unmapped. + fn unmapped_window(&mut self, xwm: XwmId, window: X11Surface); + /// Window was destroyed + fn destroyed_window(&mut self, xwm: XwmId, window: X11Surface); + + /// Window asks to be positioned or sized differently. + /// + /// Requests can be granted by calling [`X11Surface::configure`] with updated values. + #[allow(clippy::too_many_arguments)] + fn configure_request( + &mut self, + xwm: XwmId, + window: X11Surface, + x: Option, + y: Option, + w: Option, + h: Option, + reorder: Option, + ); + /// Window was reconfigured. + /// + /// This call will be done as a notification for both normal and override-redirect windows. + /// Override-redirect windows can modify their size, position and stack order at any time and the compositor + /// should properly reflect these new values to avoid bugs. + fn configure_notify( + &mut self, + xwm: XwmId, + window: X11Surface, + geometry: Rectangle, + above: Option, + ); + /// A window property has changed. + fn property_notify(&mut self, xwm: XwmId, window: X11Surface, property: WmWindowProperty) { + let _ = (xwm, window, property); + } + /// Window requests to be maximized. + fn maximize_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be unmaximized. + fn unmaximize_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be fullscreened. + fn fullscreen_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be unfullscreened. + fn unfullscreen_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be set as a modal dialog (see [`X11Surface::is_modal`]). + fn modal_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to no longer be a modal dialog. + fn unmodal_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be minimized. + fn minimize_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be unminimized. + fn unminimize_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + + /// Window requests to be placed above other windows. + fn above_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to no longer be placed above other windows. + fn unabove_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be placed below other windows. + fn below_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to no longer be placed below other windows. + fn unbelow_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be made sticky. + /// + /// This is usually used to indicate that the window should be shown on all workspaces. + fn stick_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be unstuck. + fn unstick_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be shaded. + fn shade_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window requests to be unshaded. + fn unshade_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window indicates it requires the user's attention. + fn demands_attention_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + /// Window no longer indicates it requires the user's attention. + fn undemands_attention_request(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + + /// Window requests to be resized. + /// + /// The window will be holding a grab on the mouse button provided and requests + /// to be resized on the edges passed. + fn resize_request(&mut self, xwm: XwmId, window: X11Surface, button: u32, resize_edge: ResizeEdge); + /// Window requests to be moved. + /// + /// The window will be holding a grab on the mouse button provided. + fn move_request(&mut self, xwm: XwmId, window: X11Surface, button: u32); + + /// Window requests to be set as active window. + /// + /// A client may include a `currently_active_window` in the request to + /// indicate this is a focus change from another window by the same client. + fn active_window_request( + &mut self, + xwm: XwmId, + window: X11Surface, + timestamp: u32, + currently_active_window: Option, + ) { + let _ = (xwm, window, timestamp, currently_active_window); + } + + /// Window has responded to a `_NET_WM_PING` request. + fn ping_acked(&mut self, xwm: XwmId, window: X11Surface, timestamp: u32) { + let _ = (xwm, window, timestamp); + } + + /// A client has acknowledged a `_NET_WM_SYNC_REQUEST` request. + /// + /// This is informational only; a compositor need not implement this or take any particular + /// action. + fn sync_request_acked(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + + /// A client's `_NET_WM_SYNC_REQUEST` has timed out. + /// + /// This is informational only; a compositor need not implement this or take any particular + /// action. + fn sync_request_timeout(&mut self, xwm: XwmId, window: X11Surface) { + let _ = (xwm, window); + } + + /// Window requests access to the given selection. + fn allow_selection_access(&mut self, xwm: XwmId, selection: SelectionTarget) -> bool { + let _ = (xwm, selection); + false + } + + /// The given selection is being read by an X client and needs to be written to the provided file descriptor + fn send_selection(&mut self, xwm: XwmId, selection: SelectionTarget, mime_type: String, fd: OwnedFd) { + let _ = (xwm, selection, mime_type, fd); + panic!( + "`allow_selection_access` returned true without `send_selection` implementation to handle transfers." + ); + } + + /// A new selection was set by an X client with provided mime_types + fn new_selection(&mut self, xwm: XwmId, selection: SelectionTarget, mime_types: Vec) { + let _ = (xwm, selection, mime_types); + } + + /// A proviously set selection of an X client got cleared + fn cleared_selection(&mut self, xwm: XwmId, selection: SelectionTarget) { + let _ = (xwm, selection); + } + + /// The primary output of the randr protocol state was updated + fn randr_primary_output_change(&mut self, xwm: XwmId, output_name: Option) { + let _ = (xwm, output_name); + } + + /// Application requests enabling "show desktop" mode + fn show_desktop_request(&mut self, xwm: XwmId) { + let _ = xwm; + } + + /// Application requests disabling "show desktop" mode + fn unshow_desktop_request(&mut self, xwm: XwmId) { + let _ = xwm; + } + + /// WM has lost connection to X server + fn disconnected(&mut self, _xwm: XwmId) {} +} + +/// The runtime state of an reparenting XWayland window manager. +#[derive(Debug)] +pub struct X11Wm { + id: XwmId, + conn: Arc, + client_scale: Arc, + screen: Screen, + wm_window: OwnedX11Window, + atoms: Atoms, + servertime_counter: Option, + xsettings: XSettings, + randr_primary: u32, + + pub(crate) unpaired_surfaces: HashMap, + sequences_to_ignore: BinaryHeap>, + colormaps: RefCell>, + + // selections + _xfixes_data: QueryExtensionReply, + clipboard: XWmSelection, + primary: XWmSelection, + dnd: XWmDnd, + + pub(crate) windows: Vec, + // oldest mapped -> newest + client_list: Vec, + // bottom -> top + client_list_stacking: Vec, + + is_showing_desktop: bool, + + pub(super) focus_release: FocusReleaseHandle, + + span: tracing::Span, +} + +impl Drop for X11Wm { + fn drop(&mut self) { + xwm_id::remove(self.id.0); + + // Break reference cycle caused by deferred_sync hook + for window in std::mem::take(&mut self.windows) { + window.handle_destroyed(); + } + } +} + +#[derive(Debug, Clone)] +pub(super) struct FocusReleaseHandle { + pending: Arc, + signal: ping::Ping, + conn: Weak, +} + +impl FocusReleaseHandle { + fn new(conn: &Arc) -> std::io::Result<(Self, ping::PingSource)> { + let (signal, source) = ping::make_ping()?; + Ok(( + Self { + pending: Arc::new(AtomicBool::new(false)), + signal, + conn: Arc::downgrade(conn), + }, + source, + )) + } + + fn schedule(&self) { + self.pending.store(true, Ordering::Release); + self.signal.ping(); + } + + fn cancel(&self) { + self.pending.store(false, Ordering::Release); + } + + fn dispatch(&self) { + if !self.pending.swap(false, Ordering::AcqRel) { + return; + } + let Some(conn) = self.conn.upgrade() else { return }; + if let Err(err) = conn.set_input_focus(InputFocus::NONE, x11rb::NONE, x11rb::CURRENT_TIME) { + warn!("Unable to release X11 keyboard focus: {}", err); + } + let _ = conn.flush(); + } +} + +/// Edge values for resizing +/// +// These values are used to indicate which edge of a surface is being dragged in a resize operation. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum ResizeEdge { + Top, + Bottom, + Left, + TopLeft, + BottomLeft, + Right, + TopRight, + BottomRight, +} + +/// Errors generated working with Xwm Selections +#[derive(thiserror::Error, Debug)] +pub enum SelectionError { + /// X11 Error occurred setting the selection + #[error(transparent)] + X11Error(#[from] ReplyOrIdError), + /// Calloop error occurred trying to register transfer with event loop + #[error(transparent)] + Calloop(#[from] calloop::Error), + /// Unable to determine internal Atom for given mime-type + #[error("Unable to determine ATOM matching mime-type")] + UnableToDetermineAtom, +} + +impl From for SelectionError { + #[inline] + fn from(err: ConnectionError) -> Self { + SelectionError::X11Error(err.into()) + } +} + +/// Errors generated updating XSETTINGS +#[derive(Debug, thiserror::Error)] +pub enum SettingsError { + /// X11 Error occurred updating XSETTINGS + #[error(transparent)] + X11Error(#[from] ConnectionError), + /// A provided name isn't allowed + #[error("Name '{name}' isn't valid: {error}")] + NameError { + /// Name that failed to parse + name: String, + #[source] + /// Validation error + error: NameError, + }, +} + +/// Errors generated updating the primary output +#[derive(Debug, thiserror::Error)] +pub enum PrimaryOutputError { + /// X11 Error occurred updating xrandr primary output + #[error(transparent)] + X11Error(#[from] ReplyError), + /// The output was unknown to Xwayland + #[error("The provided output wasn't known to Xwayland")] + OutputUnknown, +} + +impl From for PrimaryOutputError { + fn from(value: ConnectionError) -> Self { + PrimaryOutputError::X11Error(value.into()) + } +} + +#[derive(Debug, Clone)] +struct OwnedX11Window { + conn: Weak, + window: Arc, +} + +impl OwnedX11Window { + fn new(window: X11Window, conn: &Arc) -> Self { + OwnedX11Window { + conn: Arc::downgrade(conn), + window: Arc::new(window), + } + } +} + +impl PartialEq for OwnedX11Window { + fn eq(&self, other: &Self) -> bool { + *self.window == *other.window + } +} + +impl PartialEq for OwnedX11Window { + fn eq(&self, other: &X11Window) -> bool { + *self.window == *other + } +} + +impl Deref for OwnedX11Window { + type Target = X11Window; + + fn deref(&self) -> &Self::Target { + &self.window + } +} + +impl Drop for OwnedX11Window { + fn drop(&mut self) { + if let Some(window) = Arc::get_mut(&mut self.window) { + if let Some(conn) = self.conn.upgrade() { + if let Err(err) = conn.destroy_window(*window) { + warn!("Failed to destroy window: {}", err); + } + let _ = conn.flush(); + } + } + } +} + +impl X11Wm { + /// Start a new window manager for a given Xwayland connection + /// + /// ## Arguments + /// - `handle` is an eventloop handle used to queue up and handle incoming X11 events + /// - `dh` is the corresponding display handle to the wayland connection of the Xwayland instance + /// - `connection` is the corresponding x11 client connection of the Xwayland instance + /// - `client` is the wayland client instance of the Xwayland instance + pub fn start_wm( + handle: LoopHandle<'static, D>, + dh: &DisplayHandle, + connection: UnixStream, + client: wayland_server::Client, + ) -> Result> + where + D: XwmHandler, + D: xwayland_shell::XWaylandShellHandler, + D: SeatHandler, + D: DndGrabHandler, + ::PointerFocus: DndFocus, + ::TouchFocus: DndFocus, + D: 'static, + { + let id = XwmId(xwm_id::next()); + let span = debug_span!("xwayland_wm", id = id.0); + let _guard = span.enter(); + + // Create an X11 connection. XWayland only uses screen 0. + let screen = 0; + let stream = DefaultStream::from_unix_stream(connection)?.0; + let conn = RustConnection::connect_to_stream(stream, screen)?; + let atoms = Atoms::new(&conn)?.reply()?; + let screen = conn.setup().roots[0].clone(); + let randr_primary = conn.randr_get_output_primary(screen.root)?.reply()?.output; + + let sync_init = conn + .sync_initialize(XSYNC_REQUIRED_MAJOR_VERSION, XSYNC_REQUIRED_MINOR_VERSION)? + .reply_unchecked(); + let sync_supported = sync_init.ok().flatten().is_some(); + let servertime_counter = if sync_supported { + conn.sync_list_system_counters()? + .reply_unchecked()? + .and_then(|reply| { + reply + .counters + .into_iter() + .find(|counter| counter.name == b"SERVERTIME" && counter.counter != x11rb::NONE) + }) + .map(|counter| counter.counter) + } else { + None + }; + + { + let font = FontWrapper::open_font(&conn, "cursor".as_bytes())?; + let cursor = CursorWrapper::create_glyph_cursor( + &conn, + font.font(), + font.font(), + 68, + 69, + 0, + 0, + 0, + u16::MAX, + u16::MAX, + u16::MAX, + )?; + + // Actually become the WM by redirecting some operations + conn.change_window_attributes( + screen.root, + &ChangeWindowAttributesAux::default() + .event_mask( + EventMask::SUBSTRUCTURE_REDIRECT + | EventMask::SUBSTRUCTURE_NOTIFY + | EventMask::PROPERTY_CHANGE + | EventMask::FOCUS_CHANGE, + ) + // and also set a default root cursor in case downstream doesn't + .cursor(cursor.cursor()), + )?; + // Watch for primary output changes + conn.randr_select_input(screen.root, NotifyMask::OUTPUT_CHANGE | NotifyMask::SCREEN_CHANGE)?; + } + + // Tell XWayland that we are the WM by acquiring the WM_S0 selection. No X11 clients are accepted before this. + let win = conn.generate_id()?; + conn.create_window( + screen.root_depth, + win, + screen.root, + // x, y, width, height, border width + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_OUTPUT, + x11rb::COPY_FROM_PARENT, + &Default::default(), + )?; + conn.set_selection_owner(win, atoms.WM_S0, x11rb::CURRENT_TIME)?; + conn.set_selection_owner(win, atoms._NET_WM_CM_S0, x11rb::CURRENT_TIME)?; + conn.composite_redirect_subwindows(screen.root, Redirect::MANUAL)?; + + // Set some EWMH properties + let net_supported_base = [ + atoms._NET_WM_STATE, + atoms._NET_WM_STATE_MAXIMIZED_HORZ, + atoms._NET_WM_STATE_MAXIMIZED_VERT, + atoms._NET_WM_STATE_HIDDEN, + atoms._NET_WM_STATE_FULLSCREEN, + atoms._NET_WM_STATE_MODAL, + atoms._NET_WM_STATE_FOCUSED, + atoms._NET_WM_STATE_ABOVE, + atoms._NET_WM_STATE_BELOW, + atoms._NET_WM_STATE_SHADED, + atoms._NET_WM_STATE_SKIP_TASKBAR, + atoms._NET_WM_STATE_SKIP_PAGER, + atoms._NET_WM_STATE_STICKY, + atoms._NET_WM_STATE_DEMANDS_ATTENTION, + atoms._NET_ACTIVE_WINDOW, + atoms._NET_WM_MOVERESIZE, + atoms._NET_CLIENT_LIST, + atoms._NET_CLIENT_LIST_STACKING, + atoms._NET_SHOWING_DESKTOP, + atoms._NET_WM_OPAQUE_REGION, + atoms._NET_WM_PING, + atoms._GTK_FRAME_EXTENTS, + ]; + let net_supported = if sync_supported { + net_supported_base + .into_iter() + .chain([atoms._NET_WM_SYNC_REQUEST, atoms._NET_WM_SYNC_REQUEST_COUNTER]) + .collect::>() + } else { + net_supported_base.to_vec() + }; + conn.change_property32( + PropMode::REPLACE, + screen.root, + atoms._NET_SUPPORTED, + AtomEnum::ATOM, + &net_supported, + )?; + conn.change_property32( + PropMode::REPLACE, + screen.root, + atoms._NET_CLIENT_LIST, + AtomEnum::WINDOW, + &[], + )?; + conn.change_property32( + PropMode::REPLACE, + screen.root, + atoms._NET_CLIENT_LIST_STACKING, + AtomEnum::WINDOW, + &[], + )?; + conn.change_property32( + PropMode::REPLACE, + screen.root, + atoms._NET_ACTIVE_WINDOW, + AtomEnum::WINDOW, + &[0], + )?; + conn.change_property32( + PropMode::REPLACE, + screen.root, + atoms._NET_SHOWING_DESKTOP, + AtomEnum::CARDINAL, + &[0], + )?; + conn.change_property32( + PropMode::REPLACE, + screen.root, + atoms._NET_SUPPORTING_WM_CHECK, + AtomEnum::WINDOW, + &[win], + )?; + conn.change_property32( + PropMode::REPLACE, + win, + atoms._NET_SUPPORTING_WM_CHECK, + AtomEnum::WINDOW, + &[win], + )?; + conn.change_property8( + PropMode::REPLACE, + win, + atoms._NET_WM_NAME, + atoms.UTF8_STRING, + "Smithay X WM".as_bytes(), + )?; + debug!(window = win, "Created WM Window"); + + let conn = Arc::new(conn); + let xsettings = XSettings::new(&conn, screen.root_depth, screen.root, &atoms)?; + conn.flush()?; + + let source = X11Source::new(Arc::clone(&conn), win, atoms._SMITHAY_CLOSE_CONNECTION); + + let client_data = client.get_data::().unwrap(); + let client_scale = client_data.compositor_state.clone_client_scale(); + + // We need this for the commit hook. + client_data.user_data().insert_if_missing(|| id); + + let _xfixes_data = conn + .query_extension(x11rb::protocol::xfixes::X11_EXTENSION_NAME.as_bytes())? + .reply_unchecked()? + .ok_or(ConnectionError::UnsupportedExtension)?; + if !_xfixes_data.present { + return Err(ConnectionError::UnsupportedExtension.into()); + } + conn.xfixes_query_version(1, 0)?.reply_unchecked()?; // we just need version 1 for clipboard monitoring + + let clipboard = XWmSelection::new(&conn, &screen, &atoms, atoms.CLIPBOARD)?; + let primary = XWmSelection::new(&conn, &screen, &atoms, atoms.PRIMARY)?; + let dnd = XWmDnd::new(&conn, &screen, &atoms)?; + let wm_window = OwnedX11Window::new(win, &conn); + + let (focus_release, focus_release_source) = FocusReleaseHandle::new(&conn)?; + { + let release = focus_release.clone(); + handle.insert_source(focus_release_source, move |_, _, _| release.dispatch())?; + } + + drop(_guard); + let wm = Self { + id, + conn, + client_scale, + screen, + atoms, + servertime_counter, + xsettings, + randr_primary, + wm_window, + _xfixes_data, + clipboard, + primary, + dnd, + unpaired_surfaces: Default::default(), + sequences_to_ignore: Default::default(), + colormaps: Default::default(), + windows: Vec::new(), + client_list: Vec::new(), + client_list_stacking: Vec::new(), + is_showing_desktop: false, + focus_release, + span, + }; + + let event_handle = handle.downgrade(); + let dh = dh.clone(); + handle.insert_source(source, move |event, _, data| match event { + calloop::channel::Event::Msg(event) => { + if let Some(event_handle) = event_handle.upgrade() { + if let Err(err) = handle_event(&event_handle, &dh, data, id, event) { + warn!(id = id.0, err = ?err, "Failed to handle X11 event"); + } + } + } + calloop::channel::Event::Closed => { + data.disconnected(id); + } + })?; + Ok(wm) + } + + /// Id of this X11 WM + pub fn id(&self) -> XwmId { + self.id + } + + /// Whether or not the XSYNC extension is present + /// + /// This can be used to tell if the `_NET_WM_SYNC_REQUEST` protocol can be used. + pub fn sync_supported(&self) -> bool { + self.servertime_counter.is_some() + } + + /// Raises a window in the internal X11 state + /// + /// Needs to be called to match raising of windows inside the compositor to keep the stacking order + /// in sync with the compositor to avoid erroneous behavior. + pub fn raise_window<'a, W: X11Relatable + 'a>(&mut self, window: &'a W) -> Result<(), ConnectionError> { + if let Some(elem) = self.windows.iter().find(|s| window.is_window(s)) { + if self.client_list_stacking.last() == Some(&elem.window_id()) { + return Ok(()); + } + + let _guard = scopeguard::guard((), |_| { + let _ = self.conn.ungrab_server(); + let _ = self.conn.flush(); + }); + self.conn.grab_server()?; + self.conn.configure_window( + elem.mapped_window_id().unwrap_or_else(|| elem.window_id()), + &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE), + )?; + self.client_list_stacking.retain(|e| *e != elem.window_id()); + self.client_list_stacking.push(elem.window_id()); + self.conn.change_property32( + PropMode::REPLACE, + self.screen.root, + self.atoms._NET_CLIENT_LIST_STACKING, + AtomEnum::WINDOW, + &self.client_list_stacking, + )?; + } + Ok(()) + } + + fn update_stacking_order_impl<'a, W: X11Relatable + 'a>( + &mut self, + order: impl Iterator, + direction: StackingDirection, + ) -> Result<(), ConnectionError> { + let _guard = scopeguard::guard((), |_| { + let _ = self.conn.ungrab_server(); + let _ = self.conn.flush(); + }); + self.conn.grab_server()?; + + let mut last_pos = None; + let mut changed = false; + + for relatable in order { + let stacking_ordered_elems: Vec<&X11Surface> = self + .client_list_stacking + .iter() + .filter_map(|w| self.windows.iter().find(|s| s.window_id() == *w)) + .collect(); + let pos = stacking_ordered_elems.iter().position(|w| relatable.is_window(w)); + if let (Some(pos), Some(last_pos)) = (pos, last_pos) { + if direction.pos_comparator(pos, last_pos) { + let sibling = stacking_ordered_elems[last_pos]; + let sibling_id = self.client_list_stacking[last_pos]; + let elem = stacking_ordered_elems[pos]; + let elem_id = self.client_list_stacking.remove(pos); + self.conn.configure_window( + elem.mapped_window_id().unwrap_or(elem_id), + &ConfigureWindowAux::new() + .sibling(sibling.mapped_window_id().unwrap_or(sibling_id)) + .stack_mode(direction.stack_mode()), + )?; + self.client_list_stacking.insert(last_pos, elem_id); + changed = true; + continue; + } + } + + if pos.is_some() { + last_pos = pos; + } + } + + if changed { + self.conn.change_property32( + PropMode::REPLACE, + self.screen.root, + self.atoms._NET_CLIENT_LIST_STACKING, + AtomEnum::WINDOW, + &self.client_list_stacking, + )?; + } + Ok(()) + } + + /// Updates the stacking order by matching provided windows downwards. + /// + /// This function reorders provided x11 windows in such a way, + /// that windows inside the internal X11 stack follow the provided `order` + /// without moving other windows around as much as possible. The internal stack + /// stores windows bottom -> top, so order here is also bottom -> top. + /// + /// Window IDs unknown to this XWM will be ignored. + /// The first window in `order` found will not be moved. + /// + /// If a window is encountered in `order` that is stacked below the first window, + /// it will be moved to be directly above the previous window in `order`. + /// E.g. Windows `C -> A -> B -> E` given in order with an internal stack of `D -> A -> B -> C`, + /// will be reordered as `D -> C -> A -> B`. First `A` is moved to be directly + /// above `C`, then `B`is moved to be directly above `A`. + /// + /// Windows in the internal stack, that are not present in `order` + /// will be skipped over in the process. + /// + /// So if windows `A -> C` are given in order and the internal stack is `A -> B -> C`, + /// no reordering will occur. + /// + /// See [`X11Wm::update_stacking_order_upwards`] for a variant of this algorithm, + /// which works from the bottom up or [`X11Wm::raise_window`] for an easier but + /// much more limited way to reorder. + pub fn update_stacking_order_downwards<'a, W: X11Relatable + 'a>( + &mut self, + order: impl Iterator, + ) -> Result<(), ConnectionError> { + self.update_stacking_order_impl(order, StackingDirection::Downwards) + } + + /// Updates the stacking order by moving provided windows upwards. + /// + /// This function reorders provided x11 windows in such a way, + /// that windows inside the internal X11 stack follow the provided `order` + /// in reverse without moving other windows around as much as possible. The + /// internal stack stores windows bottom -> top, so due to the reversal, order + /// here is top -> bottom. + /// + /// Window IDs unknown to this XWM will be ignored. + /// The first window in `order` found will not be moved. + /// + /// If a window is encountered in `order` that is stacked above the first window, + /// it will be moved to be directly below the previous window in `order`. + /// E.g. Windows C -> A -> B given in order with an internal stack of `D -> A -> B -> C`, + /// will be reordered as `D -> B -> A -> C`. `A` is below `C`, so it isn't moved, + /// then `B` is moved to be directly below `A`. + /// + /// Windows in the internal stack, that are not present in `order` + /// will be skipped over in the process. + /// + /// So if windows `A -> C` are given in order and the internal stack is `C -> B -> A`, + /// no reordering will occur. + /// + /// See [`X11Wm::update_stacking_order_downwards`] for a variant of this algorithm, + /// which works from the top down or [`X11Wm::raise_window`] for an easier but + /// much more limited way to reorder. + pub fn update_stacking_order_upwards<'a, W: X11Relatable + 'a>( + &mut self, + order: impl Iterator, + ) -> Result<(), ConnectionError> { + self.update_stacking_order_impl(order, StackingDirection::Upwards) + } + + /// Set the default cursor used by X clients. + /// + /// `pixels` is expected to be in `rgba`-format with each channel encoded as an u8. + /// + /// This function will panic, if `pixels` is not at least `size.w * size.h * 4` long. + pub fn set_cursor( + &mut self, + pixels: &[u8], + size: Size, + hotspot: Point, + ) -> Result<(), ReplyOrIdError> { + assert!(pixels.len() >= size.w as usize * size.h as usize * 4usize); + let pixmap = PixmapWrapper::create_pixmap(&*self.conn, 32, self.screen.root, size.w, size.h)?; + let Some(render_format) = self + .conn + .render_query_pict_formats()? + .reply_unchecked()? + .unwrap_or_default() + .formats + .into_iter() + .filter(|f| f.depth == 32) + .map(|f| f.id) + .next() + else { + return Err(ReplyOrIdError::ConnectionError(ConnectionError::UnknownError)); + // TODO proper error type + }; + let picture = PictureWrapper::create_picture( + &*self.conn, + pixmap.pixmap(), + render_format, + &CreatePictureAux::new(), + )?; + { + let gc = GcontextWrapper::create_gc(&*self.conn, pixmap.pixmap(), &CreateGCAux::new())?; + self.conn.put_image( + ImageFormat::Z_PIXMAP, + pixmap.pixmap(), + gc.gcontext(), + size.w, + size.h, + 0, + 0, + 0, + 32, + pixels, + )?; + } + let cursor = self.conn.generate_id()?; + self.conn + .render_create_cursor(cursor, picture.picture(), hotspot.x, hotspot.y)?; + self.conn + .change_window_attributes(self.screen.root, &ChangeWindowAttributesAux::new().cursor(cursor))?; + let _ = self.conn.free_cursor(cursor); + Ok(()) + } + + /// Notify Xwayland of a new selection. + /// + /// `mime_types` being `None` indicate there is no active selection anymore. + pub fn new_selection( + &mut self, + selection: SelectionTarget, + mime_types: Option>, + ) -> Result<(), ReplyOrIdError> { + let selection = match selection { + SelectionTarget::Clipboard => &mut self.clipboard, + SelectionTarget::Primary => &mut self.primary, + }; + + if let Some(mime_types) = mime_types { + selection.mime_types = mime_types; + self.conn + .set_selection_owner(*selection.window, selection.atom, x11rb::CURRENT_TIME)?; + } else if selection.owner == *selection.window { + selection.mime_types = Vec::new(); + self.conn + .set_selection_owner(x11rb::NONE, selection.atom, selection.timestamp)?; + } + + Ok(()) + } + + /// Request to transfer the active `selection` for the provided `mime_type` to the provided file descriptor. + pub fn send_selection( + &mut self, + selection_type: SelectionTarget, + mime_type: String, + fd: OwnedFd, + ) -> Result<(), SelectionError> { + let selection = match selection_type { + SelectionTarget::Clipboard => &mut self.clipboard, + SelectionTarget::Primary => &mut self.primary, + }; + + info!( + selection = ?selection_type, + ?mime_type, + "Send request from XWayland", + ); + + let atom = atom_from_mime(&mime_type, &self.conn, &self.atoms)? + .ok_or(SelectionError::UnableToDetermineAtom)?; + + debug!("Mime-type {:?} / Atom {:?}", mime_type, atom); + + if let Err(err) = rustix::fs::fcntl_setfl(&fd, OFlags::WRONLY | OFlags::NONBLOCK) { + warn!(?err, "Failed to restrict wl file descriptor"); + } + + let incoming_window = self.conn.generate_id()?; + self.conn.create_window( + self.screen.root_depth, + incoming_window, + self.screen.root, + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_OUTPUT, + self.screen.root_visual, + &CreateWindowAux::new().event_mask(EventMask::PROPERTY_CHANGE), + )?; + self.conn.convert_selection( + incoming_window, + selection.atom, + atom, + self.atoms._WL_SELECTION, + x11rb::CURRENT_TIME, + )?; + self.conn.flush()?; + + selection.pending_transfers.lock().unwrap().insert( + incoming_window, + (OwnedX11Window::new(incoming_window, &self.conn), fd), + ); + Ok(()) + } + + /// Updates XSETTINGS with the newly provided name/value-pairs. + pub fn set_xsettings( + &mut self, + settings: impl Iterator, + ) -> Result<(), SettingsError> { + for (name, value) in settings { + self.xsettings + .set(name.clone(), value) + .map_err(move |error| SettingsError::NameError { name, error })?; + } + + self.xsettings.update(&self.conn)?; + + Ok(()) + } + + /// Removes settings from XSETTINGS. + pub fn remove_xsettings(&mut self, names: impl Iterator) -> Result<(), ConnectionError> { + let removed = names.fold(false, |any_removed, name| { + self.xsettings.remove(&name).is_some() | any_removed + }); + if removed { + self.xsettings.update(&self.conn)?; + } + Ok(()) + } + + /// Clears all settings from XSETTINGS. + pub fn clear_xsettings(&mut self) -> Result<(), ConnectionError> { + self.xsettings.clear(); + self.xsettings.update(&self.conn) + } + + /// Gets the current primary output as advertised by xrandr + pub fn get_randr_primary_output(&self) -> Result, ReplyError> { + let current_primary = self + .conn + .randr_get_output_primary(self.screen.root)? + .reply()? + .output; + if current_primary == x11rb::NONE { + return Ok(None); + } + + let res = self + .conn + .randr_get_screen_resources_current(self.screen.root)? + .reply()?; + let info = self + .conn + .randr_get_output_info(current_primary, res.config_timestamp)? + .reply()?; + Ok(Some( + std::str::from_utf8(&info.name) + .expect("X11 protocol violation, output name not utf-8") + .to_string(), + )) + } + + /// Updates the primary output as advertised by xrandr + pub fn set_randr_primary_output(&mut self, output: Option<&Output>) -> Result<(), PrimaryOutputError> { + let current_primary = self + .conn + .randr_get_output_primary(self.screen.root)? + .reply()? + .output; + let Some(output) = output else { + if current_primary != x11rb::NONE { + let cookie = self + .conn + .randr_set_output_primary(self.screen.root, x11rb::NONE)?; + self.sequences_to_ignore + .push(Reverse(cookie.sequence_number() as u16)); + } + return Ok(()); + }; + + let res = self + .conn + .randr_get_screen_resources_current(self.screen.root)? + .reply()?; + for output_xid in res.outputs { + let info = self + .conn + .randr_get_output_info(output_xid, res.config_timestamp)? + .reply()?; + let name = + std::str::from_utf8(&info.name).expect("X11 protocol violation, output name not utf-8"); + if name == output.name() { + // we got our output + if output_xid != current_primary { + let cookie = self.conn.randr_set_output_primary(self.screen.root, output_xid)?; + self.sequences_to_ignore + .push(Reverse(cookie.sequence_number() as u16)); + } + return Ok(()); + } + } + + Err(PrimaryOutputError::OutputUnknown) + } + + /// Sets the state of the "showing desktop" flag + /// + /// This updates the `_NET_SHOWING_DESKTOP` property on the root window to reflect whether or + /// not the compositor is in "show desktop" mode. This mode is usually used to temporarily + /// hide all windows so the user can see the desktop. + /// + /// Note that this function does not show or hide any windows; the compositor is expected to + /// manage window state itself. + pub fn set_showing_desktop(&mut self, is_showing_desktop: bool) -> Result<(), ReplyOrIdError> { + if self.is_showing_desktop != is_showing_desktop { + let value = if is_showing_desktop { 1 } else { 0 }; + self.conn.change_property32( + PropMode::REPLACE, + self.screen.root, + self.atoms._NET_SHOWING_DESKTOP, + AtomEnum::CARDINAL, + &[value], + )?; + self.conn.flush()?; + self.is_showing_desktop = is_showing_desktop; + } + + Ok(()) + } + + /// Returns the state of the "showing desktop" flag + pub fn is_showing_desktop(&self) -> bool { + self.is_showing_desktop + } + + fn colormap_for_visual(&self, visual: Visualid) -> Result { + if let Some(colormap) = self.colormaps.borrow().get(&visual) { + Ok(*colormap) + } else { + let colormap = self.conn.generate_id()?; + self.conn + .create_colormap(ColormapAlloc::NONE, colormap, self.screen.root, visual)? + .check()?; + self.colormaps.borrow_mut().insert(visual, colormap); + Ok(colormap) + } + } +} + +fn handle_event( + loop_handle: &LoopHandle<'_, D>, + dh: &DisplayHandle, + state: &mut D, + xwm_id: XwmId, + event: Event, +) -> Result<(), Box> +where + D: XwmHandler, + D: xwayland_shell::XWaylandShellHandler, + D: SeatHandler, + D: DndGrabHandler, + ::PointerFocus: DndFocus, + ::TouchFocus: DndFocus, + D: 'static, +{ + let xwm = state.xwm_state(xwm_id); + let _guard = xwm.span.enter(); + let conn = xwm.conn.clone(); + + let mut should_ignore = false; + if let Some(seqno) = event.wire_sequence_number() { + // Check sequences_to_ignore and remove entries with old (=smaller) numbers. + while let Some(&Reverse(to_ignore)) = xwm.sequences_to_ignore.peek() { + // Sequence numbers can wrap around, so we cannot simply check for + // "to_ignore <= seqno". This is equivalent to "to_ignore - seqno <= 0", which is what we + // check instead. Since sequence numbers are unsigned, we need a trick: We decide + // that values from [MAX/2, MAX] count as "<= 0" and the rest doesn't. + if to_ignore.wrapping_sub(seqno) <= u16::MAX / 2 { + // If the two sequence numbers are equal, this event should be ignored. + should_ignore = to_ignore == seqno; + break; + } + xwm.sequences_to_ignore.pop(); + } + } + + trace!( + event = ?event, + should_ignore = should_ignore, + "Got X11 event", + ); + if should_ignore { + return Ok(()); + } + + match event { + Event::CreateNotify(n) => { + if n.window == *xwm.wm_window + || n.window == *xwm.xsettings.window + || xwm.clipboard.has_window(&n.window) + || xwm.primary.has_window(&n.window) + || xwm.dnd.has_window(&n.window) + { + return Ok(()); + } + + let attrs = conn.get_window_attributes(n.window)?.reply()?; + if attrs.class != WindowClass::INPUT_OUTPUT { + return Ok(()); + } + + xwm.conn.change_window_attributes( + n.window, + &ChangeWindowAttributesAux::new() + .event_mask(EventMask::PROPERTY_CHANGE | EventMask::FOCUS_CHANGE), + )?; + xwm.conn.flush()?; + + if xwm.windows.iter().any(|s| s.mapped_window_id() == Some(n.window)) { + return Ok(()); + } + + let geo = conn.get_geometry(n.window)?.reply()?; + let geometry = Rectangle::::new( + (geo.x as i32, geo.y as i32).into(), + (geo.width as i32, geo.height as i32).into(), + ) + .to_f64() + .to_logical(xwm.client_scale.load(Ordering::Acquire)) + .to_i32_round(); + + let surface = X11Surface::new( + Some(xwm), + n.window, + n.override_redirect, + Arc::downgrade(&conn), + xwm.atoms, + xwm.servertime_counter, + geometry, + xwm.dnd.xdnd_active.clone(), + ); + surface.update_properties()?; + xwm.windows.push(surface.clone()); + + drop(_guard); + if n.override_redirect { + state.new_override_redirect_window(xwm_id, surface); + } else { + state.new_window(xwm_id, surface); + } + } + Event::MapRequest(r) => { + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == r.window).cloned() { + if surface.state.lock().unwrap().mapped_onto.is_none() { + // we reparent windows, because a lot of stuff expects, that we do + let geo_cookie = conn.get_geometry(r.window)?; + let attrs_cookie = conn.get_window_attributes(r.window)?; + let geo = geo_cookie.reply()?; + let attrs = attrs_cookie.reply()?; + let colormap = xwm.colormap_for_visual(attrs.visual)?; + + let win = r.window; + let frame_win = conn.generate_id()?; + let win_aux = CreateWindowAux::new() + .event_mask( + EventMask::SUBSTRUCTURE_NOTIFY + | EventMask::SUBSTRUCTURE_REDIRECT + | EventMask::PROPERTY_CHANGE, + ) + .colormap(colormap) + .background_pixmap(x11rb::NONE) + .border_pixel(0); + + { + let _guard = scopeguard::guard((), |_| { + let _ = conn.ungrab_server(); + }); + + conn.grab_server()?; + let cookie1 = conn.create_window( + geo.depth, + frame_win, + xwm.screen.root, + geo.x, + geo.y, + geo.width, + geo.height, + 0, + WindowClass::INPUT_OUTPUT, + attrs.visual, + &win_aux, + )?; + let cookie2 = conn.reparent_window(win, frame_win, 0, 0)?; + conn.map_window(win)?; + + // Ignore all events caused by reparent_window(). All those events have the sequence number + // of the reparent_window() request, thus remember its sequence number. The + // grab_server()/ungrab_server() is done so that the server does not handle other clients + // in-between, which could cause other events to get the same sequence number. + xwm.sequences_to_ignore + .push(Reverse(cookie1.sequence_number() as u16)); + xwm.sequences_to_ignore + .push(Reverse(cookie2.sequence_number() as u16)); + } + + surface.state.lock().unwrap().mapped_onto = Some(frame_win); + + // A MapRequest can only happen when an X11 window does not have the + // override-redirect flag set. It's possible for a window to be created as + // override-redirect and later set the flag to false before mapping. + // In that case, we set the X11Surface's override-redirect state to false here + // to prevent `set_mapped` and `configure` from failing. + surface.state.lock().unwrap().override_redirect = false; + + // Read initial _NET_WM_STATE set by the client before mapping. + // Per EWMH spec, clients may set _NET_WM_STATE prior to mapping + // and the window manager must respect it. + if let Ok(reply) = conn + .get_property(false, win, xwm.atoms._NET_WM_STATE, AtomEnum::ATOM, 0, 1024)? + .reply() + { + if let Some(states) = reply.value32() { + let mut state_lock = surface.state.lock().unwrap(); + for atom in states { + state_lock.net_state.insert(atom); + } + } + } + + if let Ok(Some(hints)) = WmHints::get(&*conn, win)?.reply_unchecked() { + let mut state = surface.state.lock().unwrap(); + if matches!(hints.initial_state, Some(WmHintsState::Iconic)) { + state.net_state.insert(xwm.atoms._NET_WM_STATE_HIDDEN); + } else { + state.net_state.remove(&xwm.atoms._NET_WM_STATE_HIDDEN); + } + } + + drop(_guard); + state.map_window_request(xwm_id, surface); + } + } + } + Event::MapNotify(n) => { + trace!(window = ?n.window, "mapped X11 Window"); + if let Some(surface) = xwm + .windows + .iter() + .find(|x| + // don't include the reparenting windows + (x.window_id() == n.window && n.override_redirect) || + x.mapped_window_id() == Some(n.window)) + .cloned() + { + // Client may have changed override-redirect flag since window creation + surface.state.lock().unwrap().override_redirect = n.override_redirect; + + if n.override_redirect { + drop(_guard); + state.mapped_override_redirect_window(xwm_id, surface); + } else { + xwm.client_list.push(surface.window_id()); + xwm.client_list_stacking.push(surface.window_id()); + conn.change_property32( + PropMode::APPEND, + xwm.screen.root, + xwm.atoms._NET_CLIENT_LIST, + AtomEnum::WINDOW, + &[surface.window_id()], + )?; + conn.change_property32( + PropMode::APPEND, + xwm.screen.root, + xwm.atoms._NET_CLIENT_LIST_STACKING, + AtomEnum::WINDOW, + &[surface.window_id()], + )?; + drop(_guard); + state.map_window_notify(xwm_id, surface); + } + } + } + Event::ConfigureRequest(r) => { + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == r.window).cloned() { + drop(_guard); + + let client_scale = xwm.client_scale.load(Ordering::Acquire); + + // Pass the request to downstream to decide + state.configure_request( + xwm_id, + surface.clone(), + if u16::from(r.value_mask) & u16::from(ConfigWindow::X) != 0 { + Some(((r.x as f64) / client_scale).round() as i32) + } else { + None + }, + if u16::from(r.value_mask) & u16::from(ConfigWindow::Y) != 0 { + Some(((r.y as f64) / client_scale).round() as i32) + } else { + None + }, + if u16::from(r.value_mask) & u16::from(ConfigWindow::WIDTH) != 0 { + Some(((r.width as f64) / client_scale).round() as u32) + } else { + None + }, + if u16::from(r.value_mask) & u16::from(ConfigWindow::HEIGHT) != 0 { + Some(((r.height as f64) / client_scale).round() as u32) + } else { + None + }, + if u16::from(r.value_mask) & u16::from(ConfigWindow::STACK_MODE) != 0 { + match r.stack_mode { + StackMode::ABOVE => { + if u16::from(r.value_mask) & u16::from(ConfigWindow::SIBLING) != 0 { + Some(Reorder::Above(r.sibling)) + } else { + Some(Reorder::Top) + } + } + StackMode::BELOW => { + if u16::from(r.value_mask) & u16::from(ConfigWindow::SIBLING) != 0 { + Some(Reorder::Below(r.sibling)) + } else { + Some(Reorder::Bottom) + } + } + _ => None, + } + } else { + None + }, + ); + // Synthetic event + surface.configure(None).map_err(|err| match err { + X11SurfaceError::UnsupportedForOverrideRedirect => unreachable!(), + err => err, + })?; + } + } + Event::ConfigureNotify(n) => { + trace!(window = ?n, "configured X11 Window"); + + let client_scale = xwm.client_scale.load(Ordering::Acquire); + let geometry = Rectangle::::new( + (n.x as i32, n.y as i32).into(), + (n.width as i32, n.height as i32).into(), + ) + .to_f64() + .to_logical(client_scale) + .to_i32_round(); + + if let Some(surface) = xwm + .windows + .iter() + .find(|x| x.mapped_window_id() == Some(n.window)) + .cloned() + { + drop(_guard); + state.configure_notify( + xwm_id, + surface, + geometry, + if n.above_sibling == x11rb::NONE { + None + } else { + Some(n.above_sibling) + }, + ); + } else if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == n.window).cloned() { + if surface.is_override_redirect() { + surface.state.lock().unwrap().last_configure = geometry; + drop(_guard); + state.configure_notify( + xwm_id, + surface, + geometry, + if n.above_sibling == x11rb::NONE { + None + } else { + Some(n.above_sibling) + }, + ); + } + } + } + Event::UnmapNotify(n) => { + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == n.window).cloned() { + xwm.client_list.retain(|w| *w != surface.window_id()); + xwm.client_list_stacking.retain(|w| *w != surface.window_id()); + { + let _guard = scopeguard::guard((), |_| { + let _ = conn.ungrab_server(); + }); + conn.grab_server()?; + conn.change_property32( + PropMode::REPLACE, + xwm.screen.root, + xwm.atoms._NET_CLIENT_LIST, + AtomEnum::WINDOW, + &xwm.client_list, + )?; + conn.change_property32( + PropMode::REPLACE, + xwm.screen.root, + xwm.atoms._NET_CLIENT_LIST_STACKING, + AtomEnum::WINDOW, + &xwm.client_list_stacking, + )?; + if !surface.is_override_redirect() { + let mut state = surface.state.lock().unwrap(); + conn.reparent_window( + n.window, + xwm.screen.root, + state.last_configure.loc.x as i16, + state.last_configure.loc.y as i16, + )?; + if let Some(frame) = state.mapped_onto.take() { + conn.destroy_window(frame)?; + } + conn.change_property32( + PropMode::REPLACE, + n.window, + xwm.atoms.WM_STATE, + xwm.atoms.WM_STATE, + &[0 /*WithdrawnState*/, 0 /*WINDOW_NONE*/], + )?; + } + } + drop(_guard); + state.unmapped_window(xwm_id, surface.clone()); + { + surface.set_wl_surface(state, None); + } + } + } + Event::DestroyNotify(n) => { + xwm.clipboard.window_destroyed(&n.window, loop_handle); + xwm.primary.window_destroyed(&n.window, loop_handle); + xwm.dnd.window_destroyed(&n.window, loop_handle); + + if let Some(pos) = xwm.windows.iter().position(|x| x.window_id() == n.window) { + let surface = xwm.windows.remove(pos); + surface.handle_destroyed(); + drop(_guard); + state.destroyed_window(xwm_id, surface); + } + } + Event::XfixesSelectionNotify(n) => { + let selection = match n.selection { + x if x == xwm.atoms.CLIPBOARD => &mut xwm.clipboard, + x if x == xwm.atoms.PRIMARY => &mut xwm.primary, + x if x == xwm.atoms.XdndSelection => { + let id = xwm.id(); + drop(_guard); + return XWmDnd::xfixes_selection_notify(dh, state, id, n).map_err(Into::into); + } + _ => return Ok(()), + }; + + selection.owner = n.owner; + if selection.owner == *selection.window { + selection.timestamp = n.timestamp; + return Ok(()); + } + + if n.owner == x11rb::NONE && selection.owner != *selection.window { + // A real X clients selection went away, not our proxy + let selection = selection.type_().unwrap(); + drop(_guard); + state.cleared_selection(xwm_id, selection); + return Ok(()); + } + + // Actually query the new selection, which will give us a SelectionNotify event + conn.convert_selection( + *selection.window, + selection.atom, + xwm.atoms.TARGETS, + xwm.atoms._WL_SELECTION, + n.timestamp, + )?; + } + Event::SelectionNotify(n) => { + let selection = match n.selection { + x if x == xwm.atoms.CLIPBOARD => &mut xwm.clipboard, + x if x == xwm.atoms.PRIMARY => &mut xwm.primary, + x if x == xwm.atoms.XdndSelection => &mut xwm.dnd.selection, + _ => return Ok(()), + }; + + match n.target { + x if x == xwm.atoms.TARGETS => { + if let Some(prop) = conn + .get_property( + true, + *selection.window, + xwm.atoms._WL_SELECTION, + AtomEnum::ANY, + 0, + 4096, + )? + .reply_unchecked()? + { + if prop.type_ == AtomEnum::ATOM.into() { + if let Some(values) = prop.value32() { + let mime_types = values + .filter_map(|val| match val { + val if val == xwm.atoms.TARGETS || val == xwm.atoms.TIMESTAMP => None, + val => mime_from_atom(val, &conn, &xwm.atoms).transpose(), + }) + .collect::, _>>()?; + + if let Some(selection) = selection.type_() { + drop(_guard); + XwmHandler::new_selection(state, xwm_id, selection, mime_types); + } + } + } + } + } + x if x == AtomEnum::NONE.into() => { + // transfer failed + if let Some(transfer) = selection.incoming.remove(&n.requestor) { + transfer.destroy(loop_handle); + } + } + _ => { + let transfer = if let Some(transfer) = selection.incoming.get_mut(&n.requestor) { + transfer + } else { + // create incoming transfer + let Some((window, fd)) = + selection.pending_transfers.lock().unwrap().remove(&n.requestor) + else { + // no file descriptor for incoming transfer + warn!( + "No file descriptor for incoming transfer to {}. Dropping...", + n.requestor + ); + return Ok(()); + }; + + let loop_handle_clone = loop_handle.clone(); + let incoming_window = *window; + let atom = n.selection; + let token = loop_handle + .insert_source( + Generic::new(fd, Interest::WRITE, Mode::Level), + move |_, fd, data| { + let xwm = data.xwm_state(xwm_id); + let conn = &xwm.conn; + let atoms = &xwm.atoms; + let selection = match atom { + x if x == xwm.atoms.CLIPBOARD => &mut xwm.clipboard, + x if x == xwm.atoms.PRIMARY => &mut xwm.primary, + x if x == xwm.atoms.XdndSelection => &mut xwm.dnd.selection, + _ => unreachable!(), + }; + if let Some(transfer) = selection.incoming.get_mut(&incoming_window) { + match write_selection_callback(fd.as_fd(), conn, atoms, transfer) { + Ok(IncomingAction::WaitForWritable) => { + return Ok(PostAction::Continue); + } + Ok(IncomingAction::WaitForProperty) if !transfer.incr_done => { + return Ok(PostAction::Disable); + } + Ok(_) | Err(_) => { + selection + .incoming + .remove(&incoming_window) + .unwrap() + .destroy(&loop_handle_clone); + } + }; + } + Ok(PostAction::Remove) + }, + ) + .map_err(|err| err.error)?; + loop_handle.disable(&token)?; + + let transfer = IncomingTransfer { + token: Some(token), + window, + incr: false, + source_data: Vec::new(), + incr_done: false, + }; + selection.incoming.insert(incoming_window, transfer); + selection.incoming.get_mut(&incoming_window).unwrap() + }; + + if let Some(prop) = conn + .get_property( + true, + *transfer.window, + xwm.atoms._WL_SELECTION, + AtomEnum::ANY, + 0, + 0x1fffffff, + )? + .reply_unchecked()? + { + let type_ = prop.type_; + if type_ == xwm.atoms.INCR { + transfer.incr = true; + return Ok(()); + } else if transfer.token.is_some() { + transfer.read_selection_prop(prop); + let _ = loop_handle.enable(transfer.token.as_ref().unwrap()); + } else { + selection.incoming.remove(&n.requestor); + } + } + } + } + } + Event::SelectionRequest(n) => { + let selection_type = match n.selection { + x if x == xwm.atoms.CLIPBOARD => xwm.clipboard.type_(), + x if x == xwm.atoms.PRIMARY => xwm.primary.type_(), + x if x == xwm.atoms.XdndSelection => xwm.dnd.selection.type_(), + _ => { + warn!( + target = ?n.selection, + "Got SelectionRequest for unknown Target", + ); + send_selection_notify_resp(&conn, &n, false)?; + return Ok(()); + } + }; + + // work around borrowing + drop(_guard); + let allow_access = selection_type + .map(|type_| state.allow_selection_access(xwm_id, type_)) + .unwrap_or(true); + + let xwm = state.xwm_state(xwm_id); + let selection = match n.selection { + x if x == xwm.atoms.CLIPBOARD => &mut xwm.clipboard, + x if x == xwm.atoms.PRIMARY => &mut xwm.primary, + x if x == xwm.atoms.XdndSelection => &mut xwm.dnd.selection, + _ => unreachable!(), + }; + + let _guard = xwm.span.enter(); + if n.requestor == *selection.window { + warn!("Got SelectionRequest from our own selection window."); + send_selection_notify_resp(&conn, &n, false)?; + return Ok(()); + } + + if selection.window != n.owner { + if n.time != x11rb::CURRENT_TIME && n.time < selection.timestamp { + warn!( + got = n.time, + expected = selection.timestamp, + "Ignoring request with too old timestamp", + ); + send_selection_notify_resp(&conn, &n, false)?; + } + + // dont fail requests, when we are not the owner anymore + return Ok(()); + } + + if allow_access { + match n.target { + x if x == xwm.atoms.TARGETS => { + if selection.mime_types.is_empty() { + send_selection_notify_resp(&conn, &n, false)?; + return Ok(()); + } + + let targets = [xwm.atoms.TARGETS, xwm.atoms.TIMESTAMP] + .iter() + .copied() + .chain( + selection + .mime_types + .iter() + .filter_map(|mime| atom_from_mime(mime, &conn, &xwm.atoms).ok()?), + ) + .collect::>(); + trace!(requstor = n.requestor, ?targets, "Sending TARGETS"); + conn.change_property32( + PropMode::REPLACE, + n.requestor, + n.property, + AtomEnum::ATOM, + &targets, + )?; + send_selection_notify_resp(&conn, &n, true)?; + } + x if x == xwm.atoms.TIMESTAMP => { + trace!( + requestor = n.requestor, + timestamp = selection.timestamp, + "Sending TIMESTAMP", + ); + conn.change_property32( + PropMode::REPLACE, + n.requestor, + n.property, + AtomEnum::INTEGER, + &[selection.timestamp], + )?; + send_selection_notify_resp(&conn, &n, true)?; + } + x if x == xwm.atoms.DELETE => { + send_selection_notify_resp(&conn, &n, true)?; + } + target => { + let Some(mime_type) = mime_from_atom(target, &conn, &xwm.atoms)? else { + debug!("Unable to determine mime type from atom: {}", target); + send_selection_notify_resp(&conn, &n, false)?; + return Ok(()); + }; + + if !selection.mime_types.contains(&mime_type) { + warn!(mime_type, "Mime type requested by X client not offered",); + send_selection_notify_resp(&conn, &n, false)?; + return Ok(()); + } + + let (recv_fd, send_fd) = rustix::pipe::pipe_with( + rustix::pipe::PipeFlags::CLOEXEC | rustix::pipe::PipeFlags::NONBLOCK, + ) + .map_err(|err| ConnectionError::IoError(std::io::Error::from(err)))?; + + // It seems that if we ever try to reply to a selection request after + // another has been sent by the same requestor, the requestor never reads + // from it. It appears to only ever read from the latest, so purge stale + // transfers to prevent clipboard hangs. + + if let Some(transfer) = selection.outgoing.remove(&n.requestor) { + debug!( + requestor = transfer.request.requestor, + "Destroying stale transfer", + ); + if transfer.token.is_some() { + send_selection_notify_resp(&transfer.conn, &transfer.request, false)?; + transfer.destroy(loop_handle); + } + } + + let requestor = n.requestor; + let atom = selection.atom; + + let token = loop_handle.insert_source( + Generic::new(recv_fd, Interest::READ, Mode::Level), + move |_, fd, data| { + let xwm = data.xwm_state(xwm_id); + let selection = match atom { + x if x == xwm.atoms.CLIPBOARD => &mut xwm.clipboard, + x if x == xwm.atoms.PRIMARY => &mut xwm.primary, + x if x == xwm.atoms.XdndSelection => &mut xwm.dnd.selection, + _ => unreachable!(), + }; + + if let Some(transfer) = selection.outgoing.get_mut(&requestor) { + match read_selection_callback(&xwm.conn, &xwm.atoms, fd.as_fd(), transfer) + { + Ok(OutgoingAction::WaitForReadable) => { + return Ok(PostAction::Continue); + } // transfer ongoing + Ok(OutgoingAction::Done) => { + let _ = transfer.token.take(); + selection.outgoing.remove(&requestor); + } + Err(err) => { + warn!(?err, "Transfer aborted"); + let _ = transfer.token.take(); + selection.outgoing.remove(&requestor); + } + Ok(OutgoingAction::DoneReading) => { + let _ = transfer.token.take(); + // Cleanup will happen on property delete for incremental transfers + } + }; + } + + Ok(PostAction::Remove) + }, + ); + + let token = match token { + Ok(token) => token, + Err(err) => { + warn!( + err = ?err.error, + "Failed to initialize event loop source for clipboard transfer", + + ); + send_selection_notify_resp(&conn, &n, false)?; + return Ok(()); + } + }; + + debug!(requestor = n.requestor, ?mime_type, "Created outgoing transfer",); + let transfer = OutgoingTransfer { + conn: conn.clone(), + incr: false, + token: Some(token), + source_data: Vec::new(), + request: n, + property_set: false, + flush_property_on_delete: false, + sent_finished: false, + }; + selection.outgoing.insert(n.requestor, transfer); + + let selection_type = selection.type_(); + drop(_guard); + + if let Some(selection_type) = selection_type { + XwmHandler::send_selection(state, xwm_id, selection_type, mime_type, send_fd); + } else { + xwm.dnd.transfer(&mime_type, send_fd); + } + } + } + } else { + // selection was denied + send_selection_notify_resp(&conn, &n, false)?; + } + } + Event::PropertyNotify(n) => { + if n.state == Property::NEW_VALUE && n.atom == xwm.atoms._WL_SELECTION { + if let Some(selection) = if xwm.clipboard.incoming.contains_key(&n.window) { + Some(&mut xwm.clipboard) + } else if xwm.primary.incoming.contains_key(&n.window) { + Some(&mut xwm.primary) + } else if xwm.dnd.selection.incoming.contains_key(&n.window) { + Some(&mut xwm.dnd.selection) + } else { + None + } { + let transfer = selection.incoming.get_mut(&n.window).unwrap(); + if transfer.incr { + if let Some(prop) = conn + .get_property( + true, + *transfer.window, + xwm.atoms._WL_SELECTION, + AtomEnum::ANY, + 0, + 0x1fffffff, + )? + .reply_unchecked()? + { + if prop.value_len == 0 { + debug!(?transfer, "Incr Transfer complete!"); + if transfer.source_data.is_empty() { + selection.incoming.remove(&n.window).unwrap().destroy(loop_handle); + } else { + transfer.incr_done = true; + } + } else { + transfer.read_selection_prop(prop); + if let Some(token) = transfer.token.as_ref() { + let _ = loop_handle.enable(token); + } else { + selection.incoming.remove(&n.window); + } + } + } + } + } + } else if n.atom == xwm.atoms._NET_WM_SYNC_REQUEST_COUNTER { + if let Some(surface) = xwm.windows.iter().find(|w| w.window_id() == n.window).cloned() { + surface.init_net_wm_sync_request()?; + } + } + + if n.state == Property::DELETE { + if let Some(selection) = if xwm + .clipboard + .outgoing + .get(&n.window) + .is_some_and(|t| t.incr && t.request.property == n.atom) + { + Some(&mut xwm.clipboard) + } else if xwm + .primary + .outgoing + .get(&n.window) + .is_some_and(|t| t.incr && t.request.property == n.atom) + { + Some(&mut xwm.primary) + } else if xwm + .dnd + .selection + .outgoing + .get(&n.window) + .is_some_and(|t| t.incr && t.request.property == n.atom) + { + Some(&mut xwm.dnd.selection) + } else { + None + } { + let transfer = selection.outgoing.get_mut(&n.window).unwrap(); + + transfer.property_set = false; + if transfer.flush_property_on_delete { + transfer.flush_property_on_delete = false; + let len = transfer.flush_data()?; + let requestor = transfer.request.requestor; + trace!(requestor, len, "Send data chunk"); + + if transfer.token.is_none() { + if len > 0 || !transfer.sent_finished { + // Either the transfer is done, but we still have bytes left, or + // all bytes have been transferred but the final 0-byte data chunk + // hasn't been sent yet + transfer.flush_property_on_delete = true; + } else { + // done + selection.outgoing.remove(&requestor); + } + } + } + } + } + + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == n.window).cloned() { + if let Some(property) = surface.update_property(n.atom)? { + drop(_guard); + state.property_notify(xwm_id, surface, property); + } + } + } + Event::FocusIn(n) + if n.event == xwm.screen.root + && matches!(n.detail, NotifyDetail::NONE | NotifyDetail::POINTER_ROOT) => + { + conn.change_property32( + PropMode::REPLACE, + xwm.screen.root, + xwm.atoms._NET_ACTIVE_WINDOW, + AtomEnum::WINDOW, + &[x11rb::NONE], + )?; + } + Event::FocusIn(n) + if n.detail != NotifyDetail::POINTER + && !matches!(n.mode, NotifyMode::GRAB | NotifyMode::UNGRAB) => + { + if xwm.windows.iter().any(|x| x.window_id() == n.event) { + conn.change_property32( + PropMode::REPLACE, + xwm.screen.root, + xwm.atoms._NET_ACTIVE_WINDOW, + AtomEnum::WINDOW, + &[n.event], + )?; + } + } + Event::ClientMessage(msg) => { + if let Some(reply) = conn.get_atom_name(msg.type_)?.reply_unchecked()? { + trace!( + event = std::str::from_utf8(&reply.name).unwrap(), + message = ?msg, + "got X11 client event message", + ); + } + match msg.type_ { + x if x == xwm.atoms.WL_SURFACE_ID => { + let wid = msg.data.as_data32()[0]; + info!( + window = ?msg.window, + surface = ?wid, + "mapped X11 window to surface", + ); + if let Some(surface) = xwm + .windows + .iter_mut() + .find(|x| x.window_id() == msg.window || x.mapped_window_id() == Some(msg.window)) + { + // This is the old, deprecated method for associating a + // wl_surface with an X11 window. It can race with the + // lifecycle of the wl_surface. Still, we can make it + // available to compositors who want to support old + // versions of xwayland by manually matching on ID in an + // idle callback. + surface.state.lock().unwrap().wl_surface_id = Some(wid); + } + } + x if x == xwm.atoms.WL_SURFACE_SERIAL => { + // This handles the case that the serial was already set on + // the wayland side. For the reverse order, we have a commit + // hook. + if let Some(xsurface) = xwm + .windows + .iter_mut() + .find(|x| x.window_id() == msg.window || x.mapped_window_id() == Some(msg.window)) + .cloned() + { + drop(_guard); + + let serial_lo = msg.data.as_data32()[0]; + let serial_hi = msg.data.as_data32()[1]; + let serial = u64::from(serial_lo) | (u64::from(serial_hi) << 32); + + let surface_state = xsurface.state.clone(); + let mut guard = surface_state.lock().unwrap(); + guard.wl_surface_serial = Some(serial); + + if let Some(wl_surface) = + xwayland_shell::XWaylandShellHandler::xwayland_shell_state(state) + .surface_for_serial(serial) + .clone() + { + debug!( + window = ?xsurface.window_id(), + wl_surface = ?wl_surface.id().protocol_id(), + "associated X11 window to wl_surface", + ); + + std::mem::drop(guard); + xsurface.set_wl_surface(state, Some(wl_surface.clone())); + XWaylandShellHandler::surface_associated(state, xwm_id, wl_surface, xsurface); + } else { + debug!( + window = ?msg.window, + serial = serial, + "no matching wl_surface for X11 window", + ); + let xwm = state.xwm_state(xwm_id); + xwm.unpaired_surfaces.insert(serial, xsurface.window_id()); + std::mem::drop(guard); + xsurface.set_wl_surface(state, None); + } + } + } + x if x == xwm.atoms.WM_CHANGE_STATE => { + let data = msg.data.as_data32(); + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == msg.window).cloned() { + drop(_guard); + match data[0] { + 1 => state.unminimize_request(xwm_id, surface), + 3 => state.minimize_request(xwm_id, surface), + _ => {} + } + } + } + x if x == xwm.atoms._NET_WM_STATE => { + let data = msg.data.as_data32(); + debug!( + "X11: Got _NET_WM_STATE change request to ({:?}): {:?} / {:?}", + match &data[0] { + 0 => "REMOVE", + 1 => "SET", + 2 => "TOGGLE", + _ => "Unknown", + }, + conn.get_atom_name(data[1])? + .reply_unchecked()? + .map(|reply| String::from_utf8(reply.name)), + conn.get_atom_name(data[2])? + .reply_unchecked()? + .map(|reply| String::from_utf8(reply.name)), + ); + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == msg.window).cloned() { + drop(_guard); + match &data[1..=2] { + &[x, y] + if (x == xwm.atoms._NET_WM_STATE_MAXIMIZED_HORZ + && y == xwm.atoms._NET_WM_STATE_MAXIMIZED_VERT) + || (x == xwm.atoms._NET_WM_STATE_MAXIMIZED_VERT + && y == xwm.atoms._NET_WM_STATE_MAXIMIZED_HORZ) => + { + match data[0] { + 0 => { + if surface.is_maximized() { + state.unmaximize_request(xwm_id, surface) + } + } + 1 => { + if !surface.is_maximized() { + state.maximize_request(xwm_id, surface) + } + } + 2 => { + if surface.is_maximized() { + state.unmaximize_request(xwm_id, surface) + } else { + state.maximize_request(xwm_id, surface) + } + } + _ => {} + } + } + actions if actions.contains(&xwm.atoms._NET_WM_STATE_FULLSCREEN) => { + match data[0] { + 0 => state.unfullscreen_request(xwm_id, surface), + 1 => state.fullscreen_request(xwm_id, surface), + 2 => { + if surface.is_fullscreen() { + state.unfullscreen_request(xwm_id, surface) + } else { + state.fullscreen_request(xwm_id, surface) + } + } + _ => {} + } + } + actions if actions.contains(&xwm.atoms._NET_WM_STATE_MODAL) => match data[0] { + 0 => state.unmodal_request(xwm_id, surface), + 1 => state.modal_request(xwm_id, surface), + 2 => { + if surface.is_modal() { + state.unmodal_request(xwm_id, surface) + } else { + state.modal_request(xwm_id, surface) + } + } + _ => {} + }, + actions if actions.contains(&xwm.atoms._NET_WM_STATE_ABOVE) => match data[0] { + 0 => state.unabove_request(xwm_id, surface), + 1 => state.above_request(xwm_id, surface), + 2 => { + if surface.is_above() { + state.unabove_request(xwm_id, surface) + } else { + state.above_request(xwm_id, surface) + } + } + _ => {} + }, + actions if actions.contains(&xwm.atoms._NET_WM_STATE_BELOW) => match data[0] { + 0 => state.unbelow_request(xwm_id, surface), + 1 => state.below_request(xwm_id, surface), + 2 => { + if surface.is_below() { + state.unbelow_request(xwm_id, surface) + } else { + state.below_request(xwm_id, surface) + } + } + _ => {} + }, + actions if actions.contains(&xwm.atoms._NET_WM_STATE_STICKY) => match data[0] { + 0 => state.unstick_request(xwm_id, surface), + 1 => state.stick_request(xwm_id, surface), + 2 => { + if surface.is_sticky() { + state.unstick_request(xwm_id, surface) + } else { + state.stick_request(xwm_id, surface) + } + } + _ => {} + }, + actions if actions.contains(&xwm.atoms._NET_WM_STATE_SHADED) => match data[0] { + 0 => state.unshade_request(xwm_id, surface), + 1 => state.shade_request(xwm_id, surface), + 2 => { + if surface.is_shaded() { + state.unshade_request(xwm_id, surface) + } else { + state.shade_request(xwm_id, surface) + } + } + _ => {} + }, + actions if actions.contains(&xwm.atoms._NET_WM_STATE_DEMANDS_ATTENTION) => { + match data[0] { + 0 => state.undemands_attention_request(xwm_id, surface), + 1 => state.demands_attention_request(xwm_id, surface), + 2 => { + if surface.demands_attention() { + state.undemands_attention_request(xwm_id, surface) + } else { + state.demands_attention_request(xwm_id, surface) + } + } + _ => {} + } + } + _ => {} + } + } + } + x if x == xwm.atoms._NET_WM_MOVERESIZE => { + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == msg.window).cloned() { + drop(_guard); + let data = msg.data.as_data32(); + match data[2] { + x @ 0..=7 => { + let resize_edge = match x { + 0 => ResizeEdge::TopLeft, + 1 => ResizeEdge::Top, + 2 => ResizeEdge::TopRight, + 3 => ResizeEdge::Right, + 4 => ResizeEdge::BottomRight, + 5 => ResizeEdge::Bottom, + 6 => ResizeEdge::BottomLeft, + 7 => ResizeEdge::Left, + _ => unreachable!(), + }; + state.resize_request(xwm_id, surface, data[3], resize_edge); + } + 8 => state.move_request(xwm_id, surface, data[3]), + _ => {} // ignore keyboard moves/resizes for now + } + } + } + x if x == xwm.atoms._NET_ACTIVE_WINDOW => { + let data = msg.data.as_data32(); + let timestamp = data[1]; + let currently_active_window = + xwm.windows.iter().find(|x| x.window_id() == data[2]).cloned(); + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == msg.window).cloned() { + drop(_guard); + state.active_window_request(xwm_id, surface, timestamp, currently_active_window); + } + } + x if x == xwm.atoms.WM_PROTOCOLS => { + let data = msg.data.as_data32(); + + match data[0] { + x if x == xwm.atoms._NET_WM_PING => { + let timestamp = data[1]; + // Some older clients will not faithfully copy the data back to us, and + // will only set the timestamp, so `data[2]` may be zero or unreliable. + let window_id = data[2]; + + let surface = (window_id != x11rb::NONE) + .then(|| xwm.windows.iter().find(|x| x.window_id() == window_id)) + .flatten() + .filter(|surface| { + surface.state.lock().unwrap().pending_ping_timestamp == Some(timestamp) + }) + .or_else(|| { + xwm.windows.iter().find(|x| { + x.state.lock().unwrap().pending_ping_timestamp == Some(timestamp) + }) + }) + .cloned(); + + if let Some(surface) = surface { + surface.state.lock().unwrap().pending_ping_timestamp = None; + drop(_guard); + state.ping_acked(xwm_id, surface.clone(), timestamp); + } + } + + _ => { + debug!( + "Unhandled WM_PROTOCOLS client msg of type {:?}", + String::from_utf8( + conn.get_atom_name(data[0])?.reply_unchecked()?.unwrap().name + ) + .ok() + ) + } + } + } + x if x == xwm.atoms._NET_SHOWING_DESKTOP => { + let data = msg.data.as_data32(); + let show_desktop = data[0]; + if show_desktop == 1 && !xwm.is_showing_desktop { + drop(_guard); + state.show_desktop_request(xwm_id); + } else if show_desktop == 0 && xwm.is_showing_desktop { + drop(_guard); + state.unshow_desktop_request(xwm_id); + } + } + // Dnd events incoming + x if x == xwm.atoms.XdndStatus => { + let window_id = msg.data.as_data32()[0]; + if let Some(surface) = xwm.windows.iter().find(|x| x.window_id() == window_id) { + xwm.dnd + .handle_status(surface, msg.data, &xwm.atoms, xwm.client_scale.clone()); + } + } + x if x == xwm.atoms.XdndFinished => { + xwm.dnd.handle_finished(msg.data); + } + // Dnd events incoming + x if x == xwm.atoms.XdndEnter => { + xwm.dnd.handle_enter(msg.data)?; + } + x if x == xwm.atoms.XdndPosition => { + xwm.dnd.handle_position(msg.data)?; + } + x if x == xwm.atoms.XdndLeave => { + xwm.dnd.handle_leave(msg.data)?; + } + x if x == xwm.atoms.XdndDrop => { + xwm.dnd.handle_drop(msg.data)?; + } + x => { + debug!( + "Unhandled client msg of type {:?}", + String::from_utf8(conn.get_atom_name(x)?.reply_unchecked()?.unwrap().name).ok() + ) + } + } + } + Event::RandrNotify(n) => { + if n.sub_code == Notify::OUTPUT_CHANGE { + let current_primary = xwm + .conn + .randr_get_output_primary(xwm.screen.root)? + .reply()? + .output; + if current_primary != xwm.randr_primary { + xwm.randr_primary = current_primary; + let primary_name = xwm.get_randr_primary_output().unwrap(); + drop(_guard); + + state.randr_primary_output_change(xwm_id, primary_name); + } + } + } + Event::RandrScreenChangeNotify(n) => { + xwm.screen.width_in_pixels = n.width; + xwm.screen.height_in_pixels = n.height; + xwm.screen.width_in_millimeters = n.mwidth; + xwm.screen.height_in_millimeters = n.mheight; + + xwm.dnd.update_screen(&xwm.screen)?; + } + Event::SyncAlarmNotify(n) => { + if let Some(surface) = xwm + .windows + .iter() + .find(|x| { + let state = x.state.lock().unwrap(); + state.sync_alarm == Some(n.alarm) || state.sync_timeout_alarm == Some(n.alarm) + }) + .cloned() + { + let is_sync_ack = surface.state.lock().unwrap().sync_alarm == Some(n.alarm); + + if is_sync_ack { + if surface.handle_sync_alarm(n.counter_value) { + drop(_guard); + state.sync_request_acked(xwm_id, surface); + } + } else { + surface.handle_sync_timeout(); + drop(_guard); + state.sync_request_timeout(xwm_id, surface); + } + } + } + Event::Error(err) => { + info!(?err, "Got X11 Error"); + } + _ => {} + } + conn.flush()?; + Ok(()) +} + +fn send_configure_notify( + conn: &RustConnection, + win: &X11Window, + geometry: Rectangle, + override_redirect: bool, +) -> Result<(), ConnectionError> { + conn.send_event( + false, + *win, + EventMask::STRUCTURE_NOTIFY, + ConfigureNotifyEvent { + response_type: CONFIGURE_NOTIFY_EVENT, + sequence: 0, + event: *win, + window: *win, + above_sibling: x11rb::NONE, + x: geometry.loc.x as i16, + y: geometry.loc.y as i16, + width: geometry.size.w as u16, + height: geometry.size.h as u16, + border_width: 0, + override_redirect, + }, + )?; + conn.flush()?; + Ok(()) +} + +fn mime_from_atom( + atom: u32, + conn: &impl ConnectionExt, + atoms: &Atoms, +) -> Result, ConnectionError> { + Ok(match atom { + x if x == atoms.TEXT => Some("text/plain".to_string()), + x if x == atoms.UTF8_STRING => Some("text/plain;charset=utf-8".to_string()), + x => conn + .get_atom_name(x)? + .reply_unchecked()? + .and_then(|reply| String::from_utf8(reply.name).ok()), + }) +} + +fn atom_from_mime( + mime: &(impl AsRef + ?Sized), + conn: &impl ConnectionExt, + atoms: &Atoms, +) -> Result, ConnectionError> { + Ok(match mime.as_ref() { + "text/plain" => Some(atoms.TEXT), + "text/plain;charset=utf-8" => Some(atoms.UTF8_STRING), + mime => conn + .intern_atom(false, mime.as_bytes())? + .reply_unchecked()? + .map(|reply| reply.atom), + }) +} diff --git a/vendor/smithay/src/xwayland/xwm/mwm.rs b/vendor/smithay/src/xwayland/xwm/mwm.rs new file mode 100644 index 0000000..5149721 --- /dev/null +++ b/vendor/smithay/src/xwayland/xwm/mwm.rs @@ -0,0 +1,140 @@ +/// The Motif WM Hints +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct MwmHints { + /// Lists the actions that can be done on the window. + /// + /// If `None`, all actions are assumed available. + pub functions: Option, + + /// List decorations (if any) that the app has requested be shown. + /// + /// If `None`, all decorations should be shown. + pub decorations: Option, + + /// The app's requested input mode. + pub input_mode: Option, + + /// The app's requested window status (type). + pub status: Option, +} + +bitflags::bitflags! { + /// Functions available for the window. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct MwmFunctionsHint: u32 { + /// All functions available, *except* for those specified by other present bits. + const ALL = (1 << 0); + /// The window can be resized. + const RESIZE = (1 << 1); + /// The window can be moved. + const MOVE = (1 << 2); + /// The window can be hidden. + const MINIMIZE = (1 << 3); + /// The window can be maximized. + const MAXIMIZE = (1 << 4); + /// The window can be closed. + const CLOSE = (1 << 5); + } +} + +bitflags::bitflags! { + /// Decoration parts the app would like shown. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct MwmDecorationsHint: u32 { + /// All decoration parts should be shown, *except* for those specified by other present bits. + const ALL = (1 << 0); + /// A border/frame should be drawn around the window. + const BORDER = (1 << 1); + /// Resize handles should be shown. + const RESIZEH = (1 << 2); + /// The window's title bar should be displayed. + const TITLE = (1 << 3); + /// A window menu button should be shown. + const MENU = (1 << 4); + /// A minimize button should be shown. + const MINIMIZE = (1 << 5); + /// A maximize button should be shown. + const MAXIMIZE = (1 << 6); + } +} + +/// The requested input mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum MwmInputMode { + /// Input goes to any window. + Modeless = 0, + /// Input does not go to ancestors of this window. + PrimaryApplicationModal = 1, + /// Input goes only to this window. + SystemModal = 2, + /// Input does not go to other windows in this application. + FullApplicationModal = 3, +} + +bitflags::bitflags! { + /// Other status information about the window. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct MwmStatusHint: u32 { + /// The window was "torn off" another window. + const TEAROFF_WINDOW = (1 << 0); + } +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct MwmHintsFlags: u32 { + const FUNCTIONS = (1 << 0); + const DECORATIONS = (1 << 1); + const INPUT_MODE = (1 << 2); + const STATUS = (1 << 3); + } +} + +impl TryFrom for MwmInputMode { + type Error = std::io::Error; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Modeless), + 1 => Ok(Self::PrimaryApplicationModal), + 2 => Ok(Self::SystemModal), + 3 => Ok(Self::FullApplicationModal), + other => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("{other} is not valid for MWM input mode"), + )), + } + } +} + +impl MwmHints { + pub(super) fn parse(data: &[u32]) -> Option { + match data { + [flags, functions, decorations, input_mode, status] => { + let flags = MwmHintsFlags::from_bits(*flags)?; + let functions = flags + .contains(MwmHintsFlags::FUNCTIONS) + .then(|| MwmFunctionsHint::from_bits_truncate(*functions)); + let decorations = flags + .contains(MwmHintsFlags::DECORATIONS) + .then(|| MwmDecorationsHint::from_bits_truncate(*decorations)); + let input_mode = flags + .contains(MwmHintsFlags::INPUT_MODE) + .then(|| MwmInputMode::try_from(*input_mode).ok()) + .flatten(); + let status = flags + .contains(MwmHintsFlags::STATUS) + .then(|| MwmStatusHint::from_bits_truncate(*status)); + + Some(Self { + functions, + decorations, + input_mode, + status, + }) + } + _ => None, + } + } +} diff --git a/vendor/smithay/src/xwayland/xwm/selection.rs b/vendor/smithay/src/xwayland/xwm/selection.rs new file mode 100644 index 0000000..9f2212e --- /dev/null +++ b/vendor/smithay/src/xwayland/xwm/selection.rs @@ -0,0 +1,403 @@ +use std::{ + collections::HashMap, + fmt, + os::fd::{BorrowedFd, OwnedFd}, + sync::{Arc, Mutex}, +}; + +use calloop::{LoopHandle, RegistrationToken}; +use tracing::{debug, trace, warn}; +use x11rb::{ + connection::Connection as _, + errors::ReplyOrIdError, + protocol::{ + xfixes::{ConnectionExt as _, SelectionEventMask}, + xproto::{ + Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, GetPropertyReply, PropMode, + SELECTION_NOTIFY_EVENT, Screen, SelectionNotifyEvent, SelectionRequestEvent, Window as X11Window, + WindowClass, + }, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, +}; + +use crate::{ + wayland::selection::SelectionTarget, + xwayland::xwm::{Atoms, OwnedX11Window}, +}; + +// copied from wlroots - docs say "maximum size can vary widely depending on the implementation" +// and there is no way to query the maximum size, you just get a non-descriptive `Length` error... +pub const INCR_CHUNK_SIZE: usize = 64 * 1024; + +#[derive(Debug)] +pub struct XWmSelection { + pub atom: Atom, + + pub conn: Arc, + pub atoms: Atoms, + pub window: OwnedX11Window, + pub owner: X11Window, + pub mime_types: Vec, + pub timestamp: u32, + + pub pending_transfers: Arc>>, + pub incoming: HashMap, + pub outgoing: HashMap, +} + +pub struct IncomingTransfer { + pub token: Option, + pub window: OwnedX11Window, + + pub incr: bool, + pub source_data: Vec, + pub incr_done: bool, +} + +impl fmt::Debug for IncomingTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IncomingTransfer") + .field("token", &self.token) + .field("window", &self.window) + .field("incr", &self.incr) + .field("source_data", &self.source_data) + .field("incr_done", &self.incr_done) + .finish() + } +} + +impl IncomingTransfer { + pub fn read_selection_prop(&mut self, reply: GetPropertyReply) { + self.source_data.extend(&reply.value) + } + + pub fn write_selection(&mut self, fd: BorrowedFd<'_>) -> std::io::Result { + if self.source_data.is_empty() { + return Ok(true); + } + + let len = rustix::io::write(fd, &self.source_data)?; + self.source_data = self.source_data.split_off(len); + + Ok(self.source_data.is_empty()) + } + + pub fn destroy(mut self, handle: &LoopHandle<'_, D>) { + if let Some(token) = self.token.take() { + handle.remove(token); + } + } +} + +impl Drop for IncomingTransfer { + fn drop(&mut self) { + if self.token.is_some() { + tracing::warn!( + ?self, + "IncomingTransfer freed before being removed from EventLoop" + ); + } + } +} + +pub struct OutgoingTransfer { + pub conn: Arc, + pub token: Option, + + pub incr: bool, + pub source_data: Vec, + pub request: SelectionRequestEvent, + + pub property_set: bool, + pub flush_property_on_delete: bool, + /// The final 0-byte data chunk has been sent, denoting the completion of this transfer + pub sent_finished: bool, +} + +impl fmt::Debug for OutgoingTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutgoingTransfer") + .field("conn", &"...") + .field("token", &self.token) + .field("incr", &self.incr) + .field("source_data", &self.source_data) + .field("request", &self.request) + .field("property_set", &self.property_set) + .field("flush_property_on_delete", &self.flush_property_on_delete) + .finish() + } +} + +impl OutgoingTransfer { + pub fn flush_data(&mut self) -> Result { + let len = std::cmp::min(self.source_data.len(), INCR_CHUNK_SIZE); + + if len == 0 { + // This flush will complete the transfer + self.sent_finished = true; + } + + let mut data = self.source_data.split_off(len); + std::mem::swap(&mut data, &mut self.source_data); + + self.conn.change_property8( + PropMode::REPLACE, + self.request.requestor, + self.request.property, + self.request.target, + &data, + )?; + self.conn.flush()?; + + let remaining = self.source_data.len(); + self.property_set = true; + Ok(remaining) + } + + pub fn destroy(mut self, handle: &LoopHandle<'_, D>) { + if let Some(token) = self.token.take() { + handle.remove(token); + } + } +} + +impl Drop for OutgoingTransfer { + fn drop(&mut self) { + if self.token.is_some() { + tracing::warn!( + ?self, + "OutgoingTransfer freed before being removed from EventLoop" + ); + } + } +} + +impl XWmSelection { + pub fn new( + conn: &Arc, + screen: &Screen, + atoms: &Atoms, + atom: Atom, + ) -> Result { + let window = conn.generate_id()?; + conn.create_window( + screen.root_depth, + window, + screen.root, + 0, + 0, + 10, + 10, + 0, + WindowClass::INPUT_OUTPUT, + screen.root_visual, + &CreateWindowAux::new().event_mask(EventMask::PROPERTY_CHANGE), + )?; + + if atom == atoms.CLIPBOARD { + conn.set_selection_owner(window, atoms.CLIPBOARD_MANAGER, x11rb::CURRENT_TIME)?; + } + conn.xfixes_select_selection_input( + window, + atom, + SelectionEventMask::SET_SELECTION_OWNER + | SelectionEventMask::SELECTION_WINDOW_DESTROY + | SelectionEventMask::SELECTION_CLIENT_CLOSE, + )?; + conn.flush()?; + + debug!( + selection_window = ?window, + ?atom, + "Selection init", + ); + + Ok(XWmSelection { + atom, + conn: conn.clone(), + atoms: *atoms, + window: OwnedX11Window::new(window, conn), + owner: x11rb::NONE, + mime_types: Vec::new(), + timestamp: x11rb::CURRENT_TIME, + pending_transfers: Arc::new(Mutex::new(HashMap::new())), + incoming: HashMap::new(), + outgoing: HashMap::new(), + }) + } + + pub fn window_destroyed(&mut self, window: &X11Window, loop_handle: &LoopHandle<'_, D>) -> bool { + (if let Some(transfer) = self.incoming.remove(window) { + transfer.destroy(loop_handle); + true + } else { + false + }) || (if let Some(transfer) = self.outgoing.remove(window) { + transfer.destroy(loop_handle); + true + } else { + false + }) || self.pending_transfers.lock().unwrap().remove(window).is_some() + } + + pub fn has_window(&self, window: &X11Window) -> bool { + self.window == *window + || self.incoming.contains_key(window) + || self.pending_transfers.lock().unwrap().contains_key(window) + } + + pub fn type_(&self) -> Option { + match self.atom { + x if x == self.atoms.CLIPBOARD => Some(SelectionTarget::Clipboard), + x if x == self.atoms.PRIMARY => Some(SelectionTarget::Primary), + _ => None, + } + } +} + +pub enum OutgoingAction { + Done, + DoneReading, + WaitForReadable, +} + +pub fn read_selection_callback( + conn: &RustConnection, + atoms: &Atoms, + fd: BorrowedFd<'_>, + transfer: &mut OutgoingTransfer, +) -> Result { + let mut buf = [0; INCR_CHUNK_SIZE]; + let Ok(len) = rustix::io::read(fd, &mut buf) else { + debug!( + requestor = transfer.request.requestor, + "File descriptor closed, aborting transfer." + ); + send_selection_notify_resp(conn, &transfer.request, false)?; + return Ok(OutgoingAction::Done); + }; + trace!( + requestor = transfer.request.requestor, + "Transfer became readable, read {} bytes", len + ); + + transfer.source_data.extend_from_slice(&buf[..len]); + if transfer.source_data.len() >= INCR_CHUNK_SIZE { + if !transfer.incr { + // start incr transfer + trace!( + requestor = transfer.request.requestor, + "Transfer became incremental", + ); + conn.change_property32( + PropMode::REPLACE, + transfer.request.requestor, + transfer.request.property, + atoms.INCR, + &[INCR_CHUNK_SIZE as u32], + )?; + conn.flush()?; + transfer.incr = true; + transfer.property_set = true; + transfer.flush_property_on_delete = true; + send_selection_notify_resp(conn, &transfer.request, true)?; + } else if transfer.property_set { + // got more bytes, waiting for property delete + transfer.flush_property_on_delete = true; + } else { + // got more bytes, property deleted + let len = transfer.flush_data()?; + trace!( + requestor = transfer.request.requestor, + "Send data chunk: {} bytes", len + ); + } + } + + if len == 0 { + if transfer.incr { + debug!("Incr transfer completed"); + if !transfer.property_set { + let len = transfer.flush_data()?; + trace!( + requestor = transfer.request.requestor, + "Send data chunk: {} bytes", len + ); + } + transfer.flush_property_on_delete = true; + Ok(OutgoingAction::DoneReading) + } else { + let len = transfer.flush_data()?; + debug!("Non-Incr transfer completed with {} bytes", len); + send_selection_notify_resp(conn, &transfer.request, true)?; + Ok(OutgoingAction::Done) + } + } else { + Ok(OutgoingAction::WaitForReadable) + } // nothing to be done, buffered the bytes +} + +pub enum IncomingAction { + Done, + WaitForProperty, + WaitForWritable, +} + +pub fn write_selection_callback( + fd: BorrowedFd<'_>, + conn: &RustConnection, + atoms: &Atoms, + transfer: &mut IncomingTransfer, +) -> Result { + match transfer.write_selection(fd) { + Ok(true) => { + if transfer.incr { + conn.delete_property(*transfer.window, atoms._WL_SELECTION)?; + Ok(IncomingAction::WaitForProperty) + } else { + debug!(?transfer, "Non-Incr Transfer complete!"); + Ok(IncomingAction::Done) + } + } + Ok(false) => Ok(IncomingAction::WaitForWritable), + Err(err) => { + warn!(?err, "Transfer errored"); + if transfer.incr { + // even if it failed, we still need to drain the incr transfer + conn.delete_property(*transfer.window, atoms._WL_SELECTION)?; + Ok(IncomingAction::WaitForProperty) + } else { + Ok(IncomingAction::Done) + } + } + } +} + +pub fn send_selection_notify_resp( + conn: &RustConnection, + req: &SelectionRequestEvent, + success: bool, +) -> Result<(), ReplyOrIdError> { + conn.send_event( + false, + req.requestor, + EventMask::NO_EVENT, + SelectionNotifyEvent { + response_type: SELECTION_NOTIFY_EVENT, + sequence: 0, + time: req.time, + requestor: req.requestor, + selection: req.selection, + target: req.target, + property: if success { + req.property + } else { + AtomEnum::NONE.into() + }, + }, + )?; + conn.flush()?; + Ok(()) +} diff --git a/vendor/smithay/src/xwayland/xwm/settings.rs b/vendor/smithay/src/xwayland/xwm/settings.rs new file mode 100644 index 0000000..96f4003 --- /dev/null +++ b/vendor/smithay/src/xwayland/xwm/settings.rs @@ -0,0 +1,244 @@ +//! Types related to XSETTINGS handling +//! +//! See + +use std::{borrow::Borrow, collections::HashMap, hash::Hash, sync::Arc}; + +use tracing::debug; +use x11rb::{ + connection::Connection, + errors::{ConnectionError, ReplyOrIdError}, + protocol::xproto::{ConnectionExt as _, PropMode, Window, WindowClass}, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, +}; + +use crate::xwayland::xwm::OwnedX11Window; + +#[derive(Debug)] +pub(super) struct XSettings { + pub(super) window: OwnedX11Window, + atoms: super::Atoms, + values: HashMap, + current_serial: u32, +} + +#[derive(Debug)] +struct Setting { + val: Value, + last_changed_serial: u32, +} + +#[derive(Debug)] +/// XSETTINGS value +pub enum Value { + /// String value + String(String), + /// Integer value + Integer(i32), + /// Color value + Color([u16; 4]), +} + +impl From for Value { + fn from(value: String) -> Self { + Value::String(value) + } +} +impl From for Value { + fn from(value: i32) -> Self { + Value::Integer(value) + } +} +impl From<[u16; 4]> for Value { + fn from(value: [u16; 4]) -> Self { + Value::Color(value) + } +} + +const fn pad(len: usize) -> usize { + (4 - (len % 4)) % 4 +} + +/// XSETTINGS Name validation error +#[derive(Debug, thiserror::Error)] +pub enum NameError { + /// Name contained an invalid character. Only a-z, A-Z, 0-9, /, _ are allowed. + #[error("Name contained an invalid character. Only a-z, A-Z, 0-9, /, _ are allowed.")] + DisallowedCharacter, + /// Name started or ended on a slash. + #[error("Name started or ended on a slash.")] + SlashAtStartOrEnd, + /// Name contained two consecutive slashes. + #[error("Name contained two consecutive slashes.")] + DoubleSlash, + /// Name contained a number at the start or immediately following a slash. + #[error("Name contained a number at the start or immediately following a slash.")] + LeadingNumber, + /// Name was empty. + #[error("Name was empty.")] + EmptyName, +} + +impl XSettings { + pub(super) fn new( + conn: &Arc, + depth: u8, + root: Window, + atoms: &super::Atoms, + ) -> Result { + let win = conn.generate_id()?; + conn.create_window( + depth, + win, + root, + // x, y, width, height, border width + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_OUTPUT, + x11rb::COPY_FROM_PARENT, + &Default::default(), + )?; + conn.set_selection_owner(win, atoms._XSETTINGS_S0, x11rb::CURRENT_TIME)?; + debug!(window = win, "Created XSettings window"); + + let mut this = XSettings { + window: OwnedX11Window::new(win, conn), + atoms: *atoms, + values: HashMap::default(), + current_serial: 0, + }; + this.update(conn)?; + Ok(this) + } + + pub fn set(&mut self, name: impl Into, value: impl Into) -> Result<(), NameError> { + let name = name.into(); + if name + .find(|c: char| !(c.is_ascii_alphanumeric() || c == '/' || c == '_')) + .is_some() + { + return Err(NameError::DisallowedCharacter); + } + if name.is_empty() { + return Err(NameError::EmptyName); + } + if name.starts_with('/') || name.ends_with('/') { + return Err(NameError::SlashAtStartOrEnd); + } + if name.split('/').any(str::is_empty) { + return Err(NameError::DoubleSlash); + } + if name + .split('/') + .any(|s| str::starts_with(s, |c: char| c.is_ascii_digit())) + { + return Err(NameError::LeadingNumber); + } + + self.values.insert( + name, + Setting { + val: value.into(), + last_changed_serial: self.current_serial, + }, + ); + + Ok(()) + } + + #[allow(dead_code)] + pub fn get(&self, name: &Q) -> Option<&Value> + where + Q: Hash + Eq + ?Sized, + String: Borrow, + { + self.values.get(name).map(|setting| &setting.val) + } + + pub fn remove(&mut self, name: &Q) -> Option + where + Q: Hash + Eq + ?Sized, + String: Borrow, + { + self.values.remove(name).map(|setting| setting.val) + } + + pub fn clear(&mut self) { + self.values.clear(); + } + + fn serialize(&mut self) -> Vec { + let mut data = Vec::new(); + + if cfg!(target_endian = "little") { + data.push(0); + } else { + data.push(1); + } + data.extend_from_slice(&[0u8; 3]); + data.extend_from_slice(&self.current_serial.to_ne_bytes()); + data.extend_from_slice(&(self.values.len() as i32).to_ne_bytes()); + + for (name, setting) in self.values.iter() { + debug_assert!(name.is_ascii()); + let name = name.as_bytes(); + + data.extend(&setting.val._type().to_ne_bytes()); + data.push(0u8); + data.extend(&(name.len() as u16).to_ne_bytes()); + data.extend(name); + data.extend(std::iter::repeat_n(0u8, pad(name.len()))); + data.extend(&setting.last_changed_serial.to_ne_bytes()); + + setting.val.serialize(&mut data); + } + + self.current_serial = self.current_serial.wrapping_add(1); + + data + } + + pub(super) fn update(&mut self, conn: &RustConnection) -> Result<(), ConnectionError> { + conn.change_property8( + PropMode::REPLACE, + *self.window, + self.atoms._XSETTINGS_SETTINGS, + self.atoms._XSETTINGS_SETTINGS, + &self.serialize(), + ) + .map(|_| ()) + } +} + +impl Value { + fn _type(&self) -> i8 { + match self { + Value::Integer(_) => 0, + Value::String(_) => 1, + Value::Color(_) => 2, + } + } + + fn serialize(&self, data: &mut Vec) { + match self { + Value::Integer(val) => { + data.extend(&val.to_ne_bytes()); + } + Value::String(val) => { + let val = val.as_bytes(); + data.extend(&(val.len() as u32).to_ne_bytes()); + data.extend(val); + data.extend(std::iter::repeat_n(0u8, pad(val.len()))); + } + Value::Color(val) => { + for component in val { + data.extend(&component.to_ne_bytes()); + } + } + } + } +} diff --git a/vendor/smithay/src/xwayland/xwm/surface.rs b/vendor/smithay/src/xwayland/xwm/surface.rs new file mode 100644 index 0000000..90fc338 --- /dev/null +++ b/vendor/smithay/src/xwayland/xwm/surface.rs @@ -0,0 +1,2540 @@ +use crate::{ + backend::{ + input::{InputTime, KeyState}, + renderer::utils::RendererSurfaceStateUserData, + }, + input::{ + Seat, SeatHandler, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, MotionEvent, PointerTarget, RelativeMotionEvent, + }, + tablet::{TabletSeatHandler, tool::TabletToolTarget}, + touch::{FrameMarker, TouchTarget}, + }, + utils::{ + Client, FrameExtents, HookId, IsAlive, Logical, Physical, Rectangle, Serial, Size, + user_data::UserDataMap, + }, + wayland::{ + compositor::{self, CompositorHandler, RectangleKind, RegionAttributes, SurfaceAttributes}, + pointer_constraints::PointerConstraintsHandler, + seat::{WaylandFocus, keyboard::enter_internal}, + }, + xwayland::xwm::MwmHints, +}; +#[cfg(feature = "desktop")] +use crate::{ + desktop::{WindowSurfaceType, utils::under_from_surface_tree}, + utils::Point, +}; + +use encoding_rs::WINDOWS_1252; +use portable_atomic::AtomicF64; +use std::{ + borrow::Cow, + collections::HashSet, + sync::{ + Arc, Mutex, MutexGuard, Weak, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; +use tracing::warn; +use wayland_server::protocol::wl_surface::WlSurface; +use xkbcommon::xkb::Keycode; + +use x11rb::{ + connection::Connection as _, + errors::{ReplyError, ReplyOrIdError}, + properties::{WmClass, WmHints, WmSizeHints}, + protocol::{ + res::{ClientIdSpec, query_client_ids}, + sync::{Alarm, ConnectionExt as _, Counter, CreateAlarmAux, Int64, TESTTYPE, VALUETYPE}, + xproto::{ + Atom, AtomEnum, ClientMessageEvent, ConfigureWindowAux, ConnectionExt as _, EventMask, + InputFocus, PropMode, Window as X11Window, + }, + }, + rust_connection::{ConnectionError, RustConnection}, + wrapper::ConnectionExt, + x11_utils::X11Error, +}; + +use super::{X11Wm, XwmId, send_configure_notify}; + +/// X11 window managed by an [`X11Wm`](super::X11Wm) +#[derive(Debug, Clone)] +pub struct X11Surface { + xwm: Option, + client_scale: Option>, + window: X11Window, + focus_release: Option, + pub(super) conn: Weak, + pub(super) atoms: super::Atoms, + servertime_counter: Option, + pub(crate) state: Arc>, + #[cfg_attr(not(feature = "desktop"), allow(dead_code))] + pub(super) xdnd_active: Arc, + user_data: Arc, +} + +/// Possible errors when calling [X11Surface::send_ping] +#[derive(Debug, thiserror::Error)] +pub enum PingError { + /// Not supported + #[error("Ping protocol is not supported for this window")] + NotSupported, + /// Invalid timestamp (must not be `0`/`CURRENT_TIME`) + #[error("Invalid timestamp provided")] + InvalidTimestamp, + /// Another ping is already in flight. + #[error("Ping with timestamp {0} is still in flight")] + PingAlreadyPending(u32), + /// Error on the underlying X11 Connection + #[error(transparent)] + Connection(#[from] ConnectionError), +} + +const DEFAULT_SYNC_REQUEST_TIMEOUT: Duration = Duration::from_secs(1); +// From http://fishsoup.net/misc/wm-spec-synchronization.html +// "If the client is continually redrawing, then the last seen value may be out of date when the +// window manager sends the message. Picking a number that is 240 later would allow for 1 second +// of frames at 60fps." +const SYNC_REQUEST_INCREMENT: i64 = 240; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SyncRequestCounter { + Basic(Counter), + Extended(Counter), +} + +#[derive(Debug, thiserror::Error)] +enum SyncRequestError { + #[error("sync request protocol not supported")] + NotSupported, + #[error("sync request already in flight")] + RequestPending, + #[error("no more XIDs")] + IdsExhausted, + #[error(transparent)] + Connection(#[from] ConnectionError), + #[error("X11 protocol error: {0:?}")] + X11(X11Error), +} + +#[derive(Debug)] +pub(crate) struct SharedSurfaceState { + pub(super) alive: bool, + pub(super) wl_surface_id: Option, + pub(super) wl_surface_serial: Option, + pub(super) mapped_onto: Option, + pub(super) last_configure: Rectangle, + pub(super) override_redirect: bool, + + // The associated wl_surface. + wl_surface: Option, + opaque_regions_hook_id: Option, + deferred_sync_hook_id: Option, + + // State for _NET_WM_SYNC_REQUEST. + sync_counter: Option, + pub(super) sync_alarm: Option, + last_set_sync_timeout: Duration, + pub(super) sync_timeout_alarm: Option, + counter_value: Int64, + // Highest counter value seen or sent. + next_counter_value: Int64, + pending_sync_wait_value: Option, + + // Configure to set after we receive an ACK for the in-flight sync request. + pending_configure: Option>, + // Configure update deferred while a sync request is in progress. + buffered_configure: Option>, + + title: String, + class: String, + instance: String, + startup_id: Option, + pid: Option, + protocols: Protocols, + hints: Option, + normal_hints: Option, + transient_for: Option, + pub(super) net_state: HashSet, + motif_hints: MwmHints, + window_type: Vec, + pub(crate) opacity: Option, + opaque_region: Option, + opaque_region_dirty: bool, + frame_extents: FrameExtents, + pending_enter: Option<( + Box, + Vec, + Option, + Serial, + )>, + pub(super) pending_ping_timestamp: Option, +} + +pub(super) type Protocols = Vec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) enum WMProtocol { + TakeFocus, + DeleteWindow, + NetWmPing, + NetWmSyncRequest, +} + +impl PartialEq for X11Surface { + #[inline] + fn eq(&self, other: &Self) -> bool { + let self_alive = self.state.lock().unwrap().alive; + let other_alive = other.state.lock().unwrap().alive; + self.xwm == other.xwm && self.window == other.window && self_alive && other_alive + } +} + +impl Drop for SharedSurfaceState { + fn drop(&mut self) { + if let Some(wl_surface) = self.wl_surface.as_ref() { + if let Some(hook_id) = self.opaque_regions_hook_id.take() { + compositor::remove_pre_commit_hook(wl_surface, &hook_id); + } + + if let Some(hook_id) = self.deferred_sync_hook_id.take() { + compositor::remove_pre_commit_hook(wl_surface, &hook_id); + } + } + } +} + +impl From for SyncRequestError { + fn from(value: ReplyError) -> Self { + match value { + ReplyError::X11Error(err) => err.into(), + ReplyError::ConnectionError(err) => err.into(), + } + } +} + +impl From for SyncRequestError { + fn from(value: ReplyOrIdError) -> Self { + match value { + ReplyOrIdError::IdsExhausted => Self::IdsExhausted, + ReplyOrIdError::X11Error(err) => err.into(), + ReplyOrIdError::ConnectionError(err) => err.into(), + } + } +} + +impl From for SyncRequestError { + fn from(value: X11Error) -> Self { + Self::X11(value) + } +} + +/// Errors that can happen for operations on an [`X11Surface`] +#[derive(Debug, thiserror::Error)] +pub enum X11SurfaceError { + /// Error on the underlying X11 Connection + #[error(transparent)] + Connection(#[from] ConnectionError), + /// X11 protocol error + #[error("X11 protocol error: {0:?}")] + X11(X11Error), + /// Operation was unsupported for an override_redirect window + #[error("Operation was unsupported for an override_redirect window")] + UnsupportedForOverrideRedirect, +} + +impl From for X11SurfaceError { + fn from(value: X11Error) -> Self { + Self::X11(value) + } +} + +/// Window types of [`X11Surface`]s +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[allow(missing_docs)] +pub enum WmWindowType { + Combo, + Desktop, + Dnd, + Dock, + DropdownMenu, + Dialog, + Menu, + Notification, + Normal, + PopupMenu, + Splash, + Toolbar, + Tooltip, + Utility, +} + +/// Window properties of [`X11Surface`]s +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[allow(missing_docs)] +pub enum WmWindowProperty { + Title, + Class, + Protocols, + Hints, + NormalHints, + TransientFor, + WindowType, + MotifHints, + StartupId, + Pid, + Opacity, + FrameExtents, + /// An unrecognized atom changed; forwarded so the compositor can react to + /// compositor/application-specific properties. + Other(Atom), +} + +/// https://x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html#input_focus +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum WmInputModel { + /// The client never expects keyboard input. + None, + /// The client expects keyboard input but never explicitly sets the input focus. + #[default] + Passive, + /// The client expects keyboard input and explicitly sets the input focus, + /// but it only does so when one of its windows already has the focus. + LocallyActive, + /// The client expects keyboard input and explicitly sets the input focus, + /// even when it is in windows the client does not own. + GloballyActive, +} + +impl X11Surface { + /// Create a new [`X11Surface`] usually handled by an [`X11Wm`](super::X11Wm) + /// + /// ## Arguments + /// + /// - `window` X11 window id + /// - `override_redirect` set if the X11 window has the override redirect flag set + /// - `conn` Weak reference on the X11 connection + /// - `servertime_counter` the XID of the server's SERVERTIME counter, or `None` + /// - `atoms` Atoms struct as defined by the [xwm module](super). + /// - `geometry` Initial geometry of the window + #[allow(clippy::too_many_arguments)] + pub fn new( + xwm: Option<&X11Wm>, + window: u32, + override_redirect: bool, + conn: Weak, + atoms: super::Atoms, + servertime_counter: Option, + geometry: Rectangle, + xdnd_active: Arc, + ) -> X11Surface { + X11Surface { + xwm: xwm.map(|wm| wm.id), + client_scale: xwm.map(|wm| wm.client_scale.clone()), + window, + conn, + atoms, + servertime_counter, + state: Arc::new(Mutex::new(SharedSurfaceState { + alive: true, + wl_surface_id: None, + wl_surface_serial: None, + wl_surface: None, + opaque_regions_hook_id: None, + deferred_sync_hook_id: None, + sync_counter: None, + sync_alarm: None, + last_set_sync_timeout: DEFAULT_SYNC_REQUEST_TIMEOUT, + sync_timeout_alarm: None, + counter_value: Default::default(), + next_counter_value: Default::default(), + pending_sync_wait_value: None, + pending_configure: None, + buffered_configure: None, + mapped_onto: None, + last_configure: geometry, + override_redirect, + title: String::from(""), + class: String::from(""), + instance: String::from(""), + startup_id: None, + pid: None, + protocols: Vec::new(), + hints: None, + normal_hints: None, + transient_for: None, + net_state: HashSet::new(), + motif_hints: MwmHints::default(), + window_type: Vec::new(), + opacity: None, + opaque_region: None, + opaque_region_dirty: true, + frame_extents: Default::default(), + pending_enter: None, + pending_ping_timestamp: None, + })), + xdnd_active, + focus_release: xwm.map(|wm| wm.focus_release.clone()), + user_data: Arc::new(UserDataMap::new()), + } + } + + /// Returns the id of the X11Wm responsible for this surface, if any + pub fn xwm_id(&self) -> Option { + self.xwm + } + + /// X11 protocol id of the underlying window + pub fn window_id(&self) -> X11Window { + self.window + } + + /// X11 protocol id of the reparented window, if any + pub fn mapped_window_id(&self) -> Option { + self.state.lock().unwrap().mapped_onto + } + + /// Set the X11 windows as mapped/unmapped affecting its visibility. + /// + /// It is an error to call this function on override redirect windows + pub fn set_mapped(&self, mapped: bool) -> Result<(), X11SurfaceError> { + if self.is_override_redirect() { + return Err(X11SurfaceError::UnsupportedForOverrideRedirect); + } + + if let Some(conn) = self.conn.upgrade() { + let state = self.state.lock().unwrap(); + if let Some(frame) = state.mapped_onto { + let wm_state = if mapped { + conn.map_window(frame)?; + if state.net_state.contains(&self.atoms._NET_WM_STATE_HIDDEN) { + 3u32 /*IconicState*/ + } else { + 1u32 /*NormalState*/ + } + } else { + conn.unmap_window(frame)?; + 0u32 /*WithdrawnState*/ + }; + conn.change_property32( + PropMode::REPLACE, + self.window, + self.atoms.WM_STATE, + self.atoms.WM_STATE, + &[wm_state, 0 /*WINDOW_NONE*/], + )?; + conn.flush()?; + } + } + + Ok(()) + } + + /// Returns if this window has the override redirect flag set or not + pub fn is_override_redirect(&self) -> bool { + self.state.lock().unwrap().override_redirect + } + + /// Returns if the window is currently mapped or not + pub fn is_mapped(&self) -> bool { + let state = self.state.lock().unwrap(); + state.wl_surface.is_some() + } + + /// Returns if the window is still alive + #[inline] + pub fn alive(&self) -> bool { + self.state.lock().unwrap().alive && self.conn.strong_count() != 0 + } + + /// Unconditionally configures the window and sends a configure notify. + fn send_configure( + &self, + state: &mut SharedSurfaceState, + rect: impl Into>>, + ) -> Result, X11SurfaceError> { + let rect = rect.into(); + if let Some(conn) = self.conn.upgrade() { + let client_scale = self + .client_scale + .as_ref() + .map(|s| s.load(Ordering::Acquire)) + .unwrap_or(1.); + let logical_rect = rect.unwrap_or(state.last_configure); + let rect = logical_rect.to_client_precise_round(client_scale); + let aux = ConfigureWindowAux::default() + .x(rect.loc.x) + .y(rect.loc.y) + .width(rect.size.w as u32) + .height(rect.size.h as u32) + .border_width(0); + if let Some(frame) = state.mapped_onto { + let win_aux = ConfigureWindowAux::default() + .width(rect.size.w as u32) + .height(rect.size.h as u32) + .border_width(0); + conn.configure_window(frame, &aux)?; + conn.configure_window(self.window, &win_aux)?; + send_configure_notify(&conn, &self.window, rect, false)?; + } else { + conn.configure_window(self.window, &aux)?; + } + conn.flush()?; + + Ok(logical_rect) + } else { + Err(X11SurfaceError::Connection(ConnectionError::UnknownError)) + } + } + + /// Send a configure to this window. + /// + /// If `rect` is provided the new state will be send to the window. + /// If `rect` is `None` a synthetic configure event with the existing state will be send. + /// + /// If a pending request sync is in progress (see [`X11Surface::configure_with_sync`]), this + /// will cancel the sync and send a configure immediately. + pub fn configure(&self, rect: impl Into>>) -> Result<(), X11SurfaceError> { + let rect = rect.into(); + if self.is_override_redirect() && rect.is_some() { + Err(X11SurfaceError::UnsupportedForOverrideRedirect) + } else { + let mut state = self.state.lock().unwrap(); + let rect = rect.or(state.pending_configure); + + if state.pending_sync_wait_value.is_some() { + self.finish_pending_sync(&mut state); + } + state.buffered_configure = None; + + let new_geometry = self.send_configure(&mut state, rect)?; + state.last_configure = new_geometry; + + Ok(()) + } + } + + /// Configure the window, syncing with the client's repaint. + /// + /// Sends a `_NET_WM_SYNC_REQUEST` message to this window, and configures it, sending a + /// `ConfigureNotify` event to the client. The client will notify us when it has finished + /// painting the window content that corresponds with this configure. + /// + /// If a pending sync is already in progress, it is buffered and will automatically be sent + /// (with a new sync request) after the in-progress sync is finished and the client has + /// committed a new buffer. + /// + /// Until the client ACKs the sync request, the surface's last configure remains frozen at the + /// previous value. When the ACK is received, + /// [`XwmHandler::sync_request_acked`](super::XwmHandler::sync_request_acked) is called. + /// + /// The `timeout` parameter specifies the amount of time before the sync request is considered + /// missed. If not provided, it defaults to one second. If the sync request times out, + /// [`XwmHandler::sync_request_timeout`](super::XwmHandler::sync_request_timeout) is called. + /// + /// If the client does not support the `_NET_WM_SYNC_REQUEST` protocol, or if `rect` has the + /// same size as the current configure and there is no sync in progress, a regular configure + /// sequence is initiated (as if [`X11Surface::configure`] was called). + /// + /// This configure method is most useful during interactive window resizes, as it avoids + /// sending configure notify events to the client faster than it can repaint, and also avoids + /// artifacts (like black bars or old window content) along the resize edge. However, in + /// principle the sync protocol can be used for *every* configure the compositor does, if it + /// wants better-looking size changes, at the expense of some overhead and complexity. + pub fn configure_with_sync( + &self, + rect: impl Into>, + timeout: Option, + ) -> Result<(), X11SurfaceError> { + if self.is_override_redirect() { + Err(X11SurfaceError::UnsupportedForOverrideRedirect) + } else { + let rect = rect.into(); + let mut state = self.state.lock().unwrap(); + + if rect.size == state.last_configure.size && state.pending_configure.is_none() { + // If the passed size is the same as our stored geometry's size, and there's no + // in-flight sync request, we send a normal configure without a sync request. Some + // clients, when they get a configure-notify with the same size as their current + // geometry, won't paint, and so won't update the sync counter, which will block + // further configure events until the timeout. But we still need to send the + // configure, especially if the location has changed. + drop(state); + self.configure(rect) + } else { + match self.send_sync_request(&mut state, timeout.unwrap_or(DEFAULT_SYNC_REQUEST_TIMEOUT)) { + Err(SyncRequestError::NotSupported) => { + drop(state); + self.configure(rect) + } + Err(SyncRequestError::RequestPending) => { + state.buffered_configure = Some(rect); + Ok(()) + } + Ok(_) => match self.send_configure(&mut state, rect) { + Err(err) => { + self.finish_pending_sync(&mut state); + Err(err) + } + Ok(pending_configure) => { + state.pending_configure = Some(pending_configure); + state.buffered_configure = None; + Ok(()) + } + }, + Err(SyncRequestError::IdsExhausted) => { + self.set_allow_commits(&state, true); + Err(ConnectionError::UnknownError.into()) + } + Err(SyncRequestError::Connection(err)) => { + self.set_allow_commits(&state, true); + Err(err.into()) + } + Err(SyncRequestError::X11(err)) => { + self.set_allow_commits(&state, true); + Err(err.into()) + } + } + } + } + } + + /// Sends a sync request to the client. + /// + /// This increments a counter used in the `_NET_WM_SYNC_REQUEST` protocol and sends a client + /// message to the surface's X11 window, which will notify the client that the compositor wants + /// to change the size of the client's window during a resize operation. + /// + /// When the client has finished repainting at the new size, + /// [`XwmHandler::sync_request_acked`](super::XwmHandler::sync_request_acked) will be called. + /// + /// This also registers a timeout. If the sync request times out, + /// [`XwmHandler::sync_request_timeout`](super::XwmHandler::sync_request_timeout) will be + /// called. + /// + /// This returns the value of the counter that was sent to the client. + fn send_sync_request( + &self, + state: &mut SharedSurfaceState, + timeout: Duration, + ) -> Result { + state.last_set_sync_timeout = timeout; + + if let Some(counter) = state + .sync_counter + .as_ref() + .filter(|_| self.servertime_counter.is_some()) + { + if state.pending_sync_wait_value.is_none() { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let is_extended = matches!(counter, SyncRequestCounter::Extended(_)); + + let next = add_xsync_value(state.next_counter_value, SYNC_REQUEST_INCREMENT); + state.next_counter_value = next; + + self.set_allow_commits(state, false); + + let msg_data = [ + self.atoms._NET_WM_SYNC_REQUEST, + x11rb::CURRENT_TIME, + next.lo, + next.hi as u32, + if is_extended { 1 } else { 0 }, + ]; + let event = ClientMessageEvent::new(32, self.window, self.atoms.WM_PROTOCOLS, msg_data); + conn.send_event(false, self.window, EventMask::NO_EVENT, event)?; + + self.init_sync_timeout(state, timeout)?; + + conn.flush()?; + + state.pending_sync_wait_value = Some(next); + + Ok(xsync_value(next)) + } else { + Err(SyncRequestError::RequestPending) + } + } else { + Err(SyncRequestError::NotSupported) + } + } + + /// Clears pending sync after an ACK has been received. + /// + /// Returns `true` if `value` is valid as an ACK for the pending sync request (or if there is + /// no pending sync, or if the protocol is not supported), `false` otherwise. + fn take_pending_sync_ack(&self, state: &mut SharedSurfaceState, value: i64) -> bool { + if let Some(counter) = &state.sync_counter { + let is_extended = matches!(counter, SyncRequestCounter::Extended(_)); + + if let Some(pending) = state.pending_sync_wait_value.take() { + let pending_v = xsync_value(pending); + + let valid = value >= pending_v && (!is_extended || value % 2 == 0); + if !valid { + state.pending_sync_wait_value = Some(pending); + } + valid + } else { + true + } + } else { + true + } + } + + /// Creates a sync alarm for the specified counter. + /// + /// This causes the X server to send us an X event every time the client increments the + /// counter by at least +1. + fn init_sync_alarm(&self, state: &mut MutexGuard<'_, SharedSurfaceState>) -> Result<(), ReplyOrIdError> { + if let Some(counter) = &state.sync_counter { + if state.sync_alarm.is_none() { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + + let (counter, is_extended) = match counter { + SyncRequestCounter::Basic(counter) => (*counter, false), + SyncRequestCounter::Extended(counter) => (*counter, true), + }; + + if is_extended { + state.counter_value = conn + .sync_query_counter(counter)? + .reply_unchecked()? + .ok_or(ConnectionError::UnknownError)? + .counter_value; + } else { + state.counter_value = Int64 { hi: 0, lo: 0 }; + conn.sync_set_counter(counter, state.counter_value)?; + } + + state.next_counter_value = state.counter_value; + + let aux = CreateAlarmAux::new() + .counter(counter) + .delta(value_to_xsync(1)) + .value_type(VALUETYPE::RELATIVE) + .value(value_to_xsync(1)) + .test_type(TESTTYPE::POSITIVE_COMPARISON) + .events(1); + let alarm = conn.generate_id()?; + conn.sync_create_alarm(alarm, &aux)?.check()?; + state.sync_alarm = Some(alarm); + } + } else { + self.destroy_sync_alarm(state); + } + + Ok(()) + } + + /// Destroys the counter sync alarm and finishes any in-flight sync. + fn destroy_sync_alarm(&self, state: &mut MutexGuard<'_, SharedSurfaceState>) { + if let Some(sync_alarm) = state.sync_alarm.take() { + if let Some(conn) = self.conn.upgrade() { + let _ = conn.sync_destroy_alarm(sync_alarm); + let _ = conn.flush(); + } + } + self.finish_pending_sync(state); + } + + /// Handles a sync alarm for the sync counter for this surface. + /// + /// If `new_value` is a valid ACK for the currently in-flight sync request, the pending + /// configure is applied. + /// + /// The caller is responsible for notifying the compositor that the sync was ACKed, and of the + /// new in-flight request, if any. + pub(super) fn handle_sync_alarm(&self, new_value: Int64) -> bool { + let new_value_raw = xsync_value(new_value); + + let mut state = self.state.lock().unwrap(); + state.counter_value = new_value; + state.next_counter_value = value_to_xsync(new_value_raw.max(xsync_value(state.next_counter_value))); + + if self.take_pending_sync_ack(&mut state, new_value_raw) { + self.finish_pending_sync(&mut state); + true + } else { + false + } + } + + /// Fetches the X server time and sets an alarm for that plus `timeout`. + fn init_sync_timeout( + &self, + state: &mut SharedSurfaceState, + timeout: Duration, + ) -> Result<(), SyncRequestError> { + self.destroy_sync_timeout(state); + + if let Some(conn) = self.conn.upgrade() { + if let Some(servertime_counter) = self.servertime_counter { + let now = xsync_value( + conn.sync_query_counter(servertime_counter)? + .reply_unchecked()? + .ok_or(SyncRequestError::NotSupported)? + .counter_value, + ) as u64; + // The SERVERTIME counter is an unsigned 64-bit millisecond value. + let timeout = value_to_xsync(now.wrapping_add(timeout.as_millis() as u64) as i64); + + let alarm = conn.generate_id()?; + + let aux = CreateAlarmAux::new() + .counter(self.servertime_counter) + .value_type(VALUETYPE::ABSOLUTE) + .value(timeout) + .test_type(TESTTYPE::POSITIVE_COMPARISON) + .delta(value_to_xsync(0)) + .events(1); + conn.sync_create_alarm(alarm, &aux)?.check()?; + conn.flush()?; + + state.sync_timeout_alarm = Some(alarm); + Ok(()) + } else { + Err(SyncRequestError::NotSupported) + } + } else { + Err(ConnectionError::UnknownError.into()) + } + } + + fn destroy_sync_timeout(&self, state: &mut SharedSurfaceState) { + if let Some(alarm) = state.sync_timeout_alarm.take() { + if let Some(conn) = self.conn.upgrade() { + if let Err(err) = conn.sync_destroy_alarm(alarm).and_then(|_| conn.flush()) { + tracing::warn!("Failed to unregister sync timeout alarm: {err}"); + } + } + } + } + + /// Cleans up after a sync request times out. + pub(super) fn handle_sync_timeout(&self) { + self.finish_pending_sync(&mut self.state.lock().unwrap()); + } + + /// Tells the X server to enable/disable commits on the window's underlying wl_surface. + fn set_allow_commits(&self, state: &SharedSurfaceState, allow_commits: bool) { + let result = self + .conn + .upgrade() + .ok_or(ConnectionError::UnknownError) + .and_then(|conn| { + let window = state.mapped_onto.unwrap_or(self.window); + conn.change_property32( + PropMode::REPLACE, + window, + self.atoms._XWAYLAND_ALLOW_COMMITS, + AtomEnum::CARDINAL, + &[if allow_commits { 1 } else { 0 }], + )?; + conn.flush()?; + Ok(()) + }); + + if let Err(err) = result { + tracing::warn!( + "Failed to update _XWAYLAND_ALLOW_COMMITS to {allow_commits} on window 0x{:08x}: {err}", + self.window, + ); + } + } + + /// Finishes an in-flight sync request. + /// + /// Promotes the pending configure to the active configure, unregisters the sync timeout, and + /// tells the XWayland server to start committing buffers again. + fn finish_pending_sync(&self, state: &mut SharedSurfaceState) { + state.pending_sync_wait_value = None; + if let Some(pending_configure) = state.pending_configure.take() { + state.last_configure = pending_configure; + } + self.destroy_sync_timeout(state); + self.set_allow_commits(state, true); + } + + /// Returns the associated wl_surface. + /// + /// This will only return `Some` once: + /// - The `WL_SURFACE_SERIAL` has been set on the x11 window, and + /// - The wl_surface has been assigned the same serial using the [xwayland + /// shell](crate::wayland::xwayland_shell) protocol on the wayland side, + /// and then committed. + #[inline] + pub fn wl_surface(&self) -> Option { + self.state.lock().unwrap().wl_surface.clone() + } + + /// Returns the associated `wl_surface` id, once it has been set by + /// xwayland. + /// + /// Note that XWayland will only set this if it was unable to bind the + /// [xwayland shell](crate::wayland::xwayland_shell) protocol on the wayland + /// side. + #[deprecated = "Since XWayland 23.1, the recommended approach is to use [wl_surface_serial] and the [xwayland shell](crate::wayland::xwayland_shell) protocol on the wayland side to match X11 windows."] + pub fn wl_surface_id(&self) -> Option { + self.state.lock().unwrap().wl_surface_id + } + + /// Returns the associated `wl_surface` serial, once it has been set by + /// xwayland. + /// + /// XWayland will set this if it has bound the [xwayland + /// shell](crate::wayland::xwayland_shell) protocol on the wayland side. + /// Otherwise, it will set [`wl_surface_id`][Self::wl_surface_id] instead. + pub fn wl_surface_serial(&self) -> Option { + self.state.lock().unwrap().wl_surface_serial + } + + pub(crate) fn set_wl_surface(&self, data: &mut D, surface: Option) { + let mut state = self.state.lock().unwrap(); + + if let Some(hook_id) = state.opaque_regions_hook_id.take() { + if let Some(wl_surface) = state.wl_surface.as_ref() { + compositor::remove_pre_commit_hook(wl_surface, &hook_id); + } + } + + if let Some(hook_id) = state.deferred_sync_hook_id.take() { + if let Some(wl_surface) = state.wl_surface.as_ref() { + compositor::remove_pre_commit_hook(wl_surface, &hook_id); + } + } + + if let (Some(surface), None) = (surface.as_ref(), state.wl_surface.as_ref()) { + if let Some((seat, keys, mods, serial)) = state.pending_enter.take() { + if let Ok(seat) = seat.downcast::>() { + enter_internal(surface, &seat, data, keys.into_iter(), serial); + if let Some(modifiers) = mods { + KeyboardTarget::modifiers(surface, &seat, data, modifiers, serial); + } + } + } + } + state.wl_surface = surface; + + if let Some(wl_surface) = state.wl_surface.clone() { + self.register_opaque_regions_hook::(&mut state, &wl_surface); + self.register_deferred_sync_hook::(&mut state, &wl_surface); + } + } + + fn register_opaque_regions_hook( + &self, + state: &mut SharedSurfaceState, + wl_surface: &WlSurface, + ) { + let hook_id = { + let state = Arc::downgrade(&self.state); + let conn = self.conn.clone(); + let window = self.window; + let property_atom = self.atoms._NET_WM_OPAQUE_REGION; + let client_scale = self.client_scale.clone(); + + compositor::add_pre_commit_hook::(wl_surface, move |_, _, wl_surface| { + if let Some(state) = state.upgrade() { + let update_opaque_region_if_needed = || { + if state.lock().unwrap().opaque_region_dirty { + if let Some(conn) = conn.upgrade() { + if let Ok(opaque_region) = + fetch_opaque_regions(&conn, window, property_atom, client_scale.as_ref()) + { + let mut state = state.lock().unwrap(); + state.opaque_region = opaque_region; + state.opaque_region_dirty = false; + return Some(state.opaque_region.clone()); + } + } + } + None + }; + + let opaque_region = update_opaque_region_if_needed() + .unwrap_or_else(|| state.lock().unwrap().opaque_region.clone()); + + compositor::with_states(wl_surface, |states| { + let mut guard = states.cached_state.get::(); + let attrs = guard.pending(); + attrs.opaque_region = opaque_region; + }); + } + }) + }; + state.opaque_regions_hook_id = Some(hook_id); + } + + fn register_deferred_sync_hook( + &self, + state: &mut SharedSurfaceState, + wl_surface: &WlSurface, + ) { + let hook_id = { + let surface = self.clone(); + + compositor::add_pre_commit_hook::(wl_surface, move |_, _, _| { + let (buffered_configure, timeout) = { + let mut state = surface.state.lock().unwrap(); + let buffered_configure = if state.pending_sync_wait_value.is_none() + && state + .buffered_configure + .is_some_and(|geom| geom != state.last_configure) + { + // We only send a new configure if: + // 1) there is no sync currently in progress, and + // 2) if the buffered configure is not the current configure. + state.buffered_configure.take() + } else { + None + }; + (buffered_configure, state.last_set_sync_timeout) + }; + + if let Some(buffered_configure) = buffered_configure { + if let Err(err) = surface.configure_with_sync(buffered_configure, Some(timeout)) { + tracing::info!( + "Failed to send buffered surface configure/sync for window 0x{:08x}: {err}", + surface.window + ); + } + } + }) + }; + state.deferred_sync_hook_id = Some(hook_id); + } + + /// Returns the last configure set on the underlying X11 window + pub fn last_configure(&self) -> Rectangle { + self.state.lock().unwrap().last_configure + } + + /// Returns the pending configure, if any + /// + /// The pending configure has already been sent to the X server and client as a part of + /// [`configure_with_sync`](X11Surface::configure_with_sync), but the client has not yet + /// acknowledged the corresponding sync request. + /// + /// The pending configure is promoted to [`last_configure`](X11Surface::last_configure) after + /// the sync request has been acknowledged. + pub fn pending_configure(&self) -> Option> { + self.state.lock().unwrap().pending_configure + } + + /// Returns the buffered configure, if any + /// + /// The buffered configure is the most recent rect passed to + /// [`configure_with_sync`](X11Surface::configure_with_sync). It has not yet been sent to the + /// X server or client because an older sync request is still in progress. Once the client + /// acknowledges the in-progress sync request and commits a buffer, the buffered configure will + /// be sent to the client as part of a new sync request. + /// + /// The buffered configure is promoted to [`pending_configure`](X11Surface::pending_configure) + /// once it has been sent to the client. + pub fn buffered_configure(&self) -> Option> { + self.state.lock().unwrap().buffered_configure + } + + /// Returns the bounding box of this surface + /// + /// This corresponds to the last committed buffer size from the XWayland server. + /// + /// If no buffer has been committed, returns [`last_configure`](X11Surface::last_configure). + pub fn bbox(&self) -> Rectangle { + let state = self.state.lock().unwrap(); + + let size = state + .wl_surface + .as_ref() + .and_then(|surface| { + compositor::with_states(surface, |states| { + states + .data_map + .get::() + .and_then(|s| s.lock().unwrap().surface_size()) + }) + }) + .unwrap_or_else(|| state.last_configure.size); + Rectangle::from_size(size) + } + + /// Returns the user-visible area of the surface + /// + /// This corresponds to the surface's bounding box, minus any frame elements like drop shadows. + /// Note that not all clients will advertise the sizes of frame elements, even if they are + /// present on the window. + pub fn geometry(&self) -> Rectangle { + self.bbox() - self.frame_extents() + } + + /// Returns the current title of the underlying X11 window + pub fn title(&self) -> String { + self.state.lock().unwrap().title.clone() + } + + /// Returns the current window class of the underlying X11 window + pub fn class(&self) -> String { + self.state.lock().unwrap().class.clone() + } + + /// Returns the current window instance of the underlying X11 window + pub fn instance(&self) -> String { + self.state.lock().unwrap().instance.clone() + } + + /// Returns the startup id of the underlying X11 window + pub fn startup_id(&self) -> Option { + self.state.lock().unwrap().startup_id.clone() + } + + /// Returns the PID of the underlying X11 window + pub fn pid(&self) -> Option { + self.state.lock().unwrap().pid + } + + /// Returns the opacity of the underlying X11 window + pub fn opacity(&self) -> Option { + self.state.lock().unwrap().opacity + } + + /// Returns if the window is a modal dialog. + /// + /// Corresponds to the `_NET_WM_STATE_MODAL` state of the underlying X11 window. + pub fn is_modal(&self) -> bool { + let state = self.state.lock().unwrap(); + state.net_state.contains(&self.atoms._NET_WM_STATE_MODAL) + } + + /// Returns if the window is considered to be a popup. + /// + /// Corresponds to the internal `_NET_WM_STATE_MODAL` state of the underlying X11 window. + #[deprecated = "use `X11Surface::is_modal` instead"] + pub fn is_popup(&self) -> bool { + self.is_modal() + } + + /// Returns if the underlying window is transient to another window. + /// + /// This might be used as a hint to manage windows in a group. + pub fn is_transient_for(&self) -> Option { + self.state.lock().unwrap().transient_for + } + + /// Returns the hints for the underlying X11 window + pub fn hints(&self) -> Option { + self.state.lock().unwrap().hints + } + + /// Returns the size hints for the underlying X11 window + pub fn size_hints(&self) -> Option { + self.state.lock().unwrap().normal_hints + } + + /// Returns the suggested minimum size of the underlying X11 window + pub fn min_size(&self) -> Option> { + let client_scale = self + .client_scale + .as_ref() + .map(|s| s.load(Ordering::Acquire)) + .unwrap_or(1.); + let state = self.state.lock().unwrap(); + state + .normal_hints + .as_ref() + .and_then(|hints| hints.min_size) + .map(Size::::from) + .map(|s| s.to_f64().to_logical(client_scale).to_i32_round()) + } + + /// Returns the suggested minimum size of the underlying X11 window + pub fn max_size(&self) -> Option> { + let client_scale = self + .client_scale + .as_ref() + .map(|s| s.load(Ordering::Acquire)) + .unwrap_or(1.); + let state = self.state.lock().unwrap(); + state + .normal_hints + .as_ref() + .and_then(|hints| hints.max_size) + .map(Size::::from) + .map(|s| s.to_f64().to_logical(client_scale).to_i32_round()) + } + + /// Returns the suggested base size of the underlying X11 window + pub fn base_size(&self) -> Option> { + let client_scale = self + .client_scale + .as_ref() + .map(|s| s.load(Ordering::Acquire)) + .unwrap_or(1.); + let state = self.state.lock().unwrap(); + let res = state + .normal_hints + .as_ref() + .and_then(|hints| hints.base_size) + .map(Size::::from) + .map(|s| s.to_f64().to_logical(client_scale).to_i32_round()); + std::mem::drop(state); + res.or_else(|| self.min_size()) + } + + /// Returns if the window is in the maximized state + pub fn is_maximized(&self) -> bool { + let state = self.state.lock().unwrap(); + state.net_state.contains(&self.atoms._NET_WM_STATE_MAXIMIZED_HORZ) + && state.net_state.contains(&self.atoms._NET_WM_STATE_MAXIMIZED_VERT) + } + + /// Returns if the window is in the fullscreen state + pub fn is_fullscreen(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_FULLSCREEN) + } + + /// Returns if the window is in the hidden state + pub fn is_hidden(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_HIDDEN) + } + + /// Returns if the window is in the activated state + pub fn is_activated(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_FOCUSED) + } + + /// Returns if the window is in the "above" (always on top) state + pub fn is_above(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_ABOVE) + } + + /// Returns if the window is in the "below" state + pub fn is_below(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_BELOW) + } + + /// Returns if the window has requested to be hidden from taskbars + pub fn is_skip_taskbar(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_SKIP_TASKBAR) + } + + /// Returns if the window has requested to be hidden from pagers + pub fn is_skip_pager(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_SKIP_PAGER) + } + + /// Returns if the window is sticky. + /// + /// This is usually used to mean that the window should be shown on all workspaces. + pub fn is_sticky(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_STICKY) + } + + /// Returns if the window is shaded. + pub fn is_shaded(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_SHADED) + } + + /// Returns if the window demands attention. + pub fn demands_attention(&self) -> bool { + self.state + .lock() + .unwrap() + .net_state + .contains(&self.atoms._NET_WM_STATE_DEMANDS_ATTENTION) + } + + /// Returns true if the window is client-side decorated + pub fn is_decorated(&self) -> bool { + let state = self.state.lock().unwrap(); + state + .motif_hints + .decorations + .as_ref() + .is_some_and(|decorations| decorations.is_empty()) + } + + /// Returns the Motif WM hints set on the window. + pub fn motif_hints(&self) -> MwmHints { + let state = self.state.lock().unwrap(); + state.motif_hints.clone() + } + + /// Sets the window as maximized or not. + /// + /// Allows the client to reflect this state in their UI. + pub fn set_maximized(&self, maximized: bool) -> Result<(), ConnectionError> { + if maximized { + self.change_net_state( + &[ + self.atoms._NET_WM_STATE_MAXIMIZED_HORZ, + self.atoms._NET_WM_STATE_MAXIMIZED_VERT, + ], + &[], + )?; + } else { + self.change_net_state( + &[], + &[ + self.atoms._NET_WM_STATE_MAXIMIZED_HORZ, + self.atoms._NET_WM_STATE_MAXIMIZED_VERT, + ], + )?; + } + Ok(()) + } + + /// Sets the window as fullscreen or not. + /// + /// Allows the client to reflect this state in their UI. + pub fn set_fullscreen(&self, fullscreen: bool) -> Result<(), ConnectionError> { + if fullscreen { + self.change_net_state(&[self.atoms._NET_WM_STATE_FULLSCREEN], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_FULLSCREEN])?; + } + Ok(()) + } + + /// Sets the window as hidden or not. + /// + /// Allows the client to e.g. stop rendering. + pub fn set_hidden(&self, suspended: bool) -> Result<(), ConnectionError> { + if suspended { + self.change_net_state(&[self.atoms._NET_WM_STATE_HIDDEN], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_HIDDEN])?; + } + + if let Some(conn) = self.conn.upgrade() { + if self.state.lock().unwrap().mapped_onto.is_some() { + let wm_state = if suspended { + 3u32 /*IconicState*/ + } else { + 1u32 /*NormalState*/ + }; + conn.change_property32( + PropMode::REPLACE, + self.window, + self.atoms.WM_STATE, + self.atoms.WM_STATE, + &[wm_state, 0 /*WINDOW_NONE*/], + )?; + } + } + Ok(()) + } + + /// Sets the window as activated or not. + /// + /// Allows the client to reflect this state in their UI. + pub fn set_activated(&self, activated: bool) -> Result<(), ConnectionError> { + if activated { + self.change_net_state(&[self.atoms._NET_WM_STATE_FOCUSED], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_FOCUSED])?; + } + Ok(()) + } + + /// Sets the window as a modal dialog or not. + /// + /// Corresponds to the `_NET_WM_STATE_MODAL` state, also reflected + /// by [`X11Surface::is_modal`]. + pub fn set_modal(&self, modal: bool) -> Result<(), ConnectionError> { + if modal { + self.change_net_state(&[self.atoms._NET_WM_STATE_MODAL], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_MODAL])?; + } + Ok(()) + } + + /// Sets the window as above (always on top) or not. + /// + /// Allows the client to reflect this state in their UI. + pub fn set_above(&self, above: bool) -> Result<(), ConnectionError> { + if above { + self.change_net_state(&[self.atoms._NET_WM_STATE_ABOVE], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_ABOVE])?; + } + Ok(()) + } + + /// Sets the window as below or not. + /// + /// Allows the client to reflect this state in their UI. + pub fn set_below(&self, below: bool) -> Result<(), ConnectionError> { + if below { + self.change_net_state(&[self.atoms._NET_WM_STATE_BELOW], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_BELOW])?; + } + Ok(()) + } + + /// Sets the window's sticky state. + /// + /// This is usually used to mean that the window should be shown on all workspaces. + pub fn set_sticky(&self, sticky: bool) -> Result<(), ConnectionError> { + if sticky { + self.change_net_state(&[self.atoms._NET_WM_STATE_STICKY], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_STICKY])?; + } + Ok(()) + } + + /// Sets the window's shaded state. + pub fn set_shaded(&self, shaded: bool) -> Result<(), ConnectionError> { + if shaded { + self.change_net_state(&[self.atoms._NET_WM_STATE_SHADED], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_SHADED])?; + } + Ok(()) + } + + /// Sets the window's demands-attention state. + /// + /// A client might flash the client-side decorations when demanding attention. + pub fn set_demands_attention(&self, demands_attention: bool) -> Result<(), ConnectionError> { + if demands_attention { + self.change_net_state(&[self.atoms._NET_WM_STATE_DEMANDS_ATTENTION], &[])?; + } else { + self.change_net_state(&[], &[self.atoms._NET_WM_STATE_DEMANDS_ATTENTION])?; + } + Ok(()) + } + + /// Returns the reported window type of the underlying X11 window if set. + /// + /// Windows without a window type set should be considered to be of type `Normal` for + /// backwards compatibility. + pub fn window_type(&self) -> Option { + self.state + .lock() + .unwrap() + .window_type + .iter() + .find_map(|atom| match atom { + x if *x == self.atoms._NET_WM_WINDOW_TYPE_COMBO => Some(WmWindowType::Combo), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_DESKTOP => Some(WmWindowType::Desktop), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_DND => Some(WmWindowType::Dnd), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_DOCK => Some(WmWindowType::Dock), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_DROPDOWN_MENU => Some(WmWindowType::DropdownMenu), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_DIALOG => Some(WmWindowType::Dialog), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_MENU => Some(WmWindowType::Menu), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_NOTIFICATION => Some(WmWindowType::Notification), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_NORMAL => Some(WmWindowType::Normal), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_POPUP_MENU => Some(WmWindowType::PopupMenu), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_SPLASH => Some(WmWindowType::Splash), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_TOOLBAR => Some(WmWindowType::Toolbar), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_TOOLTIP => Some(WmWindowType::Tooltip), + x if *x == self.atoms._NET_WM_WINDOW_TYPE_UTILITY => Some(WmWindowType::Utility), + _ => None, + }) + } + + fn change_net_state(&self, added: &[Atom], removed: &[Atom]) -> Result<(), ConnectionError> { + let mut state = self.state.lock().unwrap(); + + let mut changed = false; + for atom in removed { + changed |= state.net_state.remove(atom); + } + for atom in added { + changed |= state.net_state.insert(*atom); + } + + if changed { + let new_props = Vec::from_iter(state.net_state.iter().copied()); + + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + conn.grab_server()?; + let _guard = scopeguard::guard((), |_| { + let _ = conn.ungrab_server(); + let _ = conn.flush(); + }); + + conn.change_property32( + PropMode::REPLACE, + self.window, + self.atoms._NET_WM_STATE, + AtomEnum::ATOM, + &new_props, + )?; + } + + Ok(()) + } + + /// Input handling model requested by the underlying X11 window. + /// + /// See ICCCM §4.1.7 for details. + pub fn input_model(&self) -> WmInputModel { + let state = self.state.lock().unwrap(); + match ( + state.hints.as_ref().and_then(|hints| hints.input).unwrap_or(true), + state.protocols.contains(&WMProtocol::TakeFocus), + ) { + (false, false) => WmInputModel::None, + (true, false) => WmInputModel::Passive, // the default + (true, true) => WmInputModel::LocallyActive, + (false, true) => WmInputModel::GloballyActive, + } + } + + pub(super) fn update_properties(&self) -> Result<(), ConnectionError> { + self.update_title()?; + self.update_class()?; + self.update_protocols()?; + self.update_hints()?; + self.update_normal_hints()?; + self.update_transient_for()?; + // NET_WM_STATE is managed by the WM, we don't need to update it unless explicitly asked to + self.update_net_window_type()?; + self.update_motif_hints()?; + self.update_startup_id()?; + self.update_pid()?; + self.update_opacity()?; + if let Some(conn) = self.conn.upgrade() { + let mut state = self.state.lock().unwrap(); + state.opaque_region = fetch_opaque_regions( + &conn, + self.window, + self.atoms._NET_WM_OPAQUE_REGION, + self.client_scale.as_ref(), + )?; + state.opaque_region_dirty = false; + } + self.update_toolkit_frame_extents()?; + Ok(()) + } + + pub(super) fn update_property(&self, atom: Atom) -> Result, ConnectionError> { + match atom { + atom if atom == self.atoms._NET_WM_NAME || atom == AtomEnum::WM_NAME.into() => { + self.update_title()?; + Ok(Some(WmWindowProperty::Title)) + } + atom if atom == AtomEnum::WM_CLASS.into() => { + self.update_class()?; + Ok(Some(WmWindowProperty::Class)) + } + atom if atom == self.atoms.WM_PROTOCOLS => { + self.update_protocols()?; + Ok(Some(WmWindowProperty::Protocols)) + } + atom if atom == self.atoms.WM_HINTS => { + self.update_hints()?; + Ok(Some(WmWindowProperty::Hints)) + } + atom if atom == AtomEnum::WM_NORMAL_HINTS.into() => { + self.update_normal_hints()?; + Ok(Some(WmWindowProperty::NormalHints)) + } + atom if atom == AtomEnum::WM_TRANSIENT_FOR.into() => { + self.update_transient_for()?; + Ok(Some(WmWindowProperty::TransientFor)) + } + atom if atom == self.atoms._NET_WM_WINDOW_TYPE => { + self.update_net_window_type()?; + Ok(Some(WmWindowProperty::WindowType)) + } + atom if atom == self.atoms._MOTIF_WM_HINTS => { + self.update_motif_hints()?; + Ok(Some(WmWindowProperty::MotifHints)) + } + atom if atom == self.atoms._NET_STARTUP_ID => { + self.update_startup_id()?; + Ok(Some(WmWindowProperty::StartupId)) + } + atom if atom == self.atoms._NET_WM_PID => { + self.update_pid()?; + Ok(Some(WmWindowProperty::Pid)) + } + atom if atom == self.atoms._NET_WM_WINDOW_OPACITY => { + self.update_opacity()?; + Ok(Some(WmWindowProperty::Opacity)) + } + atom if atom == self.atoms._NET_WM_OPAQUE_REGION => { + let mut state = self.state.lock().unwrap(); + state.opaque_region = None; + state.opaque_region_dirty = true; + Ok(None) + } + atom if atom == self.atoms._GTK_FRAME_EXTENTS => { + self.update_toolkit_frame_extents()?; + Ok(Some(WmWindowProperty::FrameExtents)) + } + + _ => Ok(Some(WmWindowProperty::Other(atom))), + } + } + + fn update_class(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let (class, instance) = match WmClass::get(&*conn, self.window)?.reply_unchecked() { + Ok(Some(wm_class)) => ( + WINDOWS_1252.decode(wm_class.class()).0.to_string(), + WINDOWS_1252.decode(wm_class.instance()).0.to_string(), + ), + Ok(None) | Err(ConnectionError::ParseError(_)) => (Default::default(), Default::default()), // Getting the property failed + Err(err) => return Err(err), + }; + + let mut state = self.state.lock().unwrap(); + state.class = class; + state.instance = instance; + + Ok(()) + } + + fn update_hints(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let mut state = self.state.lock().unwrap(); + state.hints = match WmHints::get(&*conn, self.window)?.reply_unchecked() { + Ok(hints) => hints, + Err(ConnectionError::ParseError(_)) => None, + Err(err) => return Err(err), + }; + Ok(()) + } + + fn update_normal_hints(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let mut state = self.state.lock().unwrap(); + state.normal_hints = match WmSizeHints::get_normal_hints(&*conn, self.window)?.reply_unchecked() { + Ok(hints) => hints, + Err(ConnectionError::ParseError(_)) => None, + Err(err) => return Err(err), + }; + Ok(()) + } + + fn update_motif_hints(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let Some(hints) = (match conn + .get_property( + false, + self.window, + self.atoms._MOTIF_WM_HINTS, + AtomEnum::ANY, + 0, + 2048, + )? + .reply_unchecked() + { + Ok(Some(reply)) => reply.value32().map(|vals| vals.collect::>()), + Ok(None) | Err(ConnectionError::ParseError(_)) => return Ok(()), + Err(err) => return Err(err), + }) else { + return Ok(()); + }; + + if let Some(hints) = MwmHints::parse(&hints) { + let mut state = self.state.lock().unwrap(); + state.motif_hints = hints; + } + Ok(()) + } + + fn update_startup_id(&self) -> Result<(), ConnectionError> { + if let Some(startup_id) = self.read_window_property_string(self.atoms._NET_STARTUP_ID)? { + let mut state = self.state.lock().unwrap(); + state.startup_id = Some(startup_id); + } + Ok(()) + } + + fn update_pid(&self) -> Result<(), ConnectionError> { + if let Some(pid) = self.read_window_property_u32(self.atoms._NET_WM_PID)? { + let mut state = self.state.lock().unwrap(); + state.pid = Some(pid); + } + Ok(()) + } + + fn update_opacity(&self) -> Result<(), ConnectionError> { + if let Some(opacity) = self.read_window_property_u32(self.atoms._NET_WM_WINDOW_OPACITY)? { + let mut state = self.state.lock().unwrap(); + state.opacity = Some(opacity); + } + Ok(()) + } + + fn fetch_gtk_frame_extents(&self) -> Result>, ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + conn.get_property( + false, + self.window, + self.atoms._GTK_FRAME_EXTENTS, + AtomEnum::CARDINAL, + 0, + 4, + )? + .reply_unchecked() + .map(|reply| { + reply.and_then(|reply| { + reply.value32().and_then(|mut values| { + Some(FrameExtents::new( + (values.next()? as i32).max(0), + (values.next()? as i32).max(0), + (values.next()? as i32).max(0), + (values.next()? as i32).max(0), + )) + }) + }) + }) + } + + fn update_toolkit_frame_extents(&self) -> Result<(), ConnectionError> { + let frame_extents = self.fetch_gtk_frame_extents()?.unwrap_or_default(); + self.state.lock().unwrap().frame_extents = frame_extents; + Ok(()) + } + + /// Returns the logical pixel widths of the surface's frame + /// + /// This is usually the width of the drop shadows at the edges of the surface. + pub fn frame_extents(&self) -> FrameExtents { + let scale = self + .client_scale + .as_ref() + .map(|scale| scale.load(Ordering::Acquire)) + .unwrap_or(1.); + self.state + .lock() + .unwrap() + .frame_extents + .to_f64() + .to_logical(scale) + .to_i32_round() + } + + fn update_protocols(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let Some(protocols) = (match conn + .get_property( + false, + self.window, + self.atoms.WM_PROTOCOLS, + AtomEnum::ATOM, + 0, + 2048, + )? + .reply_unchecked() + { + Ok(Some(reply)) => reply.value32().map(|vals| vals.collect::>()), + Ok(None) | Err(ConnectionError::ParseError(_)) => return Ok(()), + Err(err) => return Err(err), + }) else { + return Ok(()); + }; + + let mut state = self.state.lock().unwrap(); + state.protocols = protocols + .into_iter() + .filter_map(|atom| match atom { + x if x == self.atoms.WM_TAKE_FOCUS => Some(WMProtocol::TakeFocus), + x if x == self.atoms.WM_DELETE_WINDOW => Some(WMProtocol::DeleteWindow), + x if x == self.atoms._NET_WM_PING => Some(WMProtocol::NetWmPing), + x if x == self.atoms._NET_WM_SYNC_REQUEST => Some(WMProtocol::NetWmSyncRequest), + _ => None, + }) + .collect::>(); + + drop(state); + self.init_net_wm_sync_request()?; + + Ok(()) + } + + pub(super) fn init_net_wm_sync_request(&self) -> Result<(), ConnectionError> { + let mut state = self.state.lock().unwrap(); + if state.protocols.contains(&WMProtocol::NetWmSyncRequest) { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let counters_reply = conn + .get_property( + false, + self.window, + self.atoms._NET_WM_SYNC_REQUEST_COUNTER, + AtomEnum::CARDINAL, + 0, + 2, + )? + .reply_unchecked()?; + + let counter = counters_reply.and_then(|reply| { + reply + .value32() + .and_then(|mut values| match (values.next(), values.next()) { + (_, Some(extended)) => Some(SyncRequestCounter::Extended(extended)), + (Some(basic), _) => Some(SyncRequestCounter::Basic(basic)), + _ => None, + }) + }); + + if counter != state.sync_counter { + state.sync_counter = counter; + self.destroy_sync_alarm(&mut state); + } + + if let Err(err) = self.init_sync_alarm(&mut state) { + state.sync_counter = None; + match err { + ReplyOrIdError::ConnectionError(err) => Err(err), + _ => Err(ConnectionError::UnknownError), + } + } else { + Ok(()) + } + } else { + state.sync_counter = None; + let _ = self.init_sync_alarm(&mut state); + Ok(()) + } + } + + fn update_transient_for(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let reply = match conn + .get_property( + false, + self.window, + AtomEnum::WM_TRANSIENT_FOR, + AtomEnum::WINDOW, + 0, + 2048, + )? + .reply_unchecked() + { + Ok(Some(reply)) => reply, + Ok(None) | Err(ConnectionError::ParseError(_)) => return Ok(()), + Err(err) => return Err(err), + }; + let window = reply + .value32() + .and_then(|mut iter| iter.next()) + .filter(|w| *w != 0); + + let mut state = self.state.lock().unwrap(); + state.transient_for = window; + Ok(()) + } + + fn update_title(&self) -> Result<(), ConnectionError> { + let title = self + .read_window_property_string(self.atoms._NET_WM_NAME)? + .or(self.read_window_property_string(AtomEnum::WM_NAME)?) + .unwrap_or_default(); + + let mut state = self.state.lock().unwrap(); + state.title = title; + Ok(()) + } + + fn update_net_window_type(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let atoms = match conn + .get_property( + false, + self.window, + self.atoms._NET_WM_WINDOW_TYPE, + AtomEnum::ATOM, + 0, + 1024, + )? + .reply_unchecked() + { + Ok(atoms) => atoms, + Err(ConnectionError::ParseError(_)) => return Ok(()), + Err(err) => return Err(err), + }; + + let mut state = self.state.lock().unwrap(); + state.window_type = atoms + .and_then(|atoms| Some(atoms.value32()?.collect::>())) + .unwrap_or_default(); + Ok(()) + } + + fn read_window_property_string(&self, atom: impl Into) -> Result, ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let reply = match conn + .get_property(false, self.window, atom, AtomEnum::ANY, 0, 2048)? + .reply_unchecked() + { + Ok(Some(reply)) => reply, + Ok(None) | Err(ConnectionError::ParseError(_)) => return Ok(None), + Err(err) => return Err(err), + }; + let Some(bytes) = reply.value8() else { + return Ok(None); + }; + let bytes = bytes.collect::>(); + + match reply.type_ { + x if x == AtomEnum::STRING.into() => Ok(Some(WINDOWS_1252.decode(&bytes).0.to_string())), + x if x == self.atoms.UTF8_STRING => Ok(String::from_utf8(bytes).ok()), + _ => Ok(None), + } + } + + fn read_window_property_u32(&self, atom: impl Into) -> Result, ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let reply = match conn + .get_property(false, self.window, atom, AtomEnum::CARDINAL, 0, 1)? + .reply_unchecked() + { + Ok(Some(reply)) => reply, + Ok(None) | Err(ConnectionError::ParseError(_)) => return Ok(None), + Err(err) => return Err(err), + }; + + if let Some(mut value32) = reply.value32() { + if let Some(value) = value32.next() { + return Ok(Some(value)); + } + } + + Ok(None) + } + + /// Retrieve user_data associated with this X11 window + pub fn user_data(&self) -> &UserDataMap { + &self.user_data + } + + /// Sends a `_NET_WM_PING` message to the window. + /// + /// The passed `timestamp` must be unique, as older clients may only send the timestamp in the + /// ping reply for use in matching the reply to the correct window. In particular, do not pass + /// `x11rb::CURRENT_TIME`. + /// + /// You can implement the + /// [`XwmHandler::ping_acked()`](crate::xwayland::xwm::XwmHandler::ping_acked) trait item in + /// order to be notified when the client responds. + pub fn send_ping(&self, timestamp: u32) -> Result<(), PingError> { + if timestamp == x11rb::CURRENT_TIME { + Err(PingError::InvalidTimestamp) + } else { + let mut state = self.state.lock().unwrap(); + if let Some(timestamp) = &state.pending_ping_timestamp { + Err(PingError::PingAlreadyPending(*timestamp)) + } else { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + if state.protocols.contains(&WMProtocol::NetWmPing) && timestamp != x11rb::CURRENT_TIME { + let event = ClientMessageEvent::new( + 32, + self.window, + self.atoms.WM_PROTOCOLS, + [self.atoms._NET_WM_PING, timestamp, self.window, 0, 0], + ); + conn.send_event(false, self.window, EventMask::NO_EVENT, event)?; + conn.flush()?; + state.pending_ping_timestamp = Some(timestamp); + Ok(()) + } else { + Err(PingError::NotSupported) + } + } + } + } + + /// Send a close request to this window. + /// + /// Will outright destroy windows that don't support the `NET_DELETE_WINDOW` protocol. + pub fn close(&self) -> Result<(), ConnectionError> { + let conn = self.conn.upgrade().ok_or(ConnectionError::UnknownError)?; + let state = self.state.lock().unwrap(); + if state.protocols.contains(&WMProtocol::DeleteWindow) { + let event = ClientMessageEvent::new( + 32, + self.window, + self.atoms.WM_PROTOCOLS, + [self.atoms.WM_DELETE_WINDOW, 0, 0, 0, 0], + ); + conn.send_event(false, self.window, EventMask::NO_EVENT, event)?; + } else { + conn.destroy_window(self.window)?; + } + conn.flush() + } + + /// Get the client PID associated with the X11 window. + pub fn get_client_pid(&self) -> Result> { + if let Some(connection) = self.conn.upgrade() { + let window = self.window; + + match query_client_ids( + &connection, + &[ClientIdSpec { + client: window, + mask: x11rb::protocol::res::ClientIdMask::LOCAL_CLIENT_PID, + }], + ) { + Ok(cookie) => { + let reply = cookie.reply()?; + + if let Some(id_value) = reply.ids.first() { + if let Some(pid) = id_value.value.first().copied() { + return Ok(pid); + } else { + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No matching client ID found", + ))); + } + } + } + Err(_) => { + return Ok(0); + } + } + } + Ok(0) + } + + /// Returns the topmost (sub-)surface under a given position of the surface. + /// + /// In case the window is not mapped or is unmanaged while an XDND operation is ongoing [`None`] is returned. + /// + /// - `point` has to be the position to query, relative to (0, 0) of the given surface + `location`. + /// - `location` can be used to offset the returned point. + /// - `surface_type` can be used to filter the underlying surface tree + #[cfg(feature = "desktop")] + pub fn surface_under( + &self, + point: Point, + location: impl Into>, + surface_type: WindowSurfaceType, + ) -> Option<(WlSurface, Point)> { + if !surface_type.contains(WindowSurfaceType::TOPLEVEL) { + return None; + } + if self.xdnd_active.load(Ordering::Acquire) && self.is_override_redirect() { + return None; + } + if let Some(surface) = X11Surface::wl_surface(self).as_ref() { + return under_from_surface_tree(surface, point, location, surface_type); + } + + None + } + + /// Returns whether or not the `_NET_WM_SYNC_REQUEST` protocol is supported. + /// + /// The `_NET_WM_SYNC_REQUEST` protocol is used to allow the compositor to synchronize + /// frame/decorations resize with the client's window repaint during interactive resizes. + pub fn sync_request_supported(&self) -> bool { + self.servertime_counter.is_some() && self.state.lock().unwrap().sync_counter.is_some() + } + + pub(super) fn handle_destroyed(&self) { + let mut state = self.state.lock().unwrap(); + state.alive = false; + self.destroy_sync_alarm(&mut state); + self.destroy_sync_timeout(&mut state); + // Counter is owned by the client; do not destroy it. + state.sync_counter = None; + + // Break reference cycle caused by deferred_sync hook + if let Some(hook_id) = state.deferred_sync_hook_id.take() { + if let Some(wl_surface) = state.wl_surface.as_ref() { + compositor::remove_pre_commit_hook(wl_surface, &hook_id); + } + } + } +} + +fn add_xsync_value(value: Int64, amount: i64) -> Int64 { + let value = xsync_value(value); + let value = value.wrapping_add(amount); + value_to_xsync(value) +} + +fn value_to_xsync(value: i64) -> Int64 { + Int64 { + hi: ((value >> 32) & 0xffffffff) as i32, + lo: (value & 0xffffffff) as u32, + } +} + +fn xsync_value(value: Int64) -> i64 { + (((value.hi as u64) << 32) | (value.lo as u64)) as i64 +} + +/// Trait for objects, that represent an x11 window in some shape or form +/// and can be tested for equality. +pub trait X11Relatable { + /// Returns if this object is considered to represent the same underlying x11 window as provided + fn is_window(&self, window: &X11Surface) -> bool; +} + +impl X11Relatable for X11Surface { + fn is_window(&self, window: &X11Surface) -> bool { + self == window + } +} + +impl X11Relatable for WlSurface { + fn is_window(&self, window: &X11Surface) -> bool { + let serial = compositor::with_states(self, |states| { + states + .cached_state + .get::() + .current() + .serial + }); + + window.wl_surface_serial() == serial + } +} + +impl IsAlive for X11Surface { + #[inline] + fn alive(&self) -> bool { + X11Surface::alive(self) + } +} + +impl KeyboardTarget for X11Surface { + fn enter(&self, seat: &Seat, data: &mut D, keys: Vec>, serial: Serial) { + let (set_input_focus, send_take_focus) = match self.input_model() { + WmInputModel::None => return, + WmInputModel::Passive => (true, false), + WmInputModel::LocallyActive => (true, true), + WmInputModel::GloballyActive => (false, true), + }; + + if let Some(release) = &self.focus_release { + release.cancel(); + } + + if let Some(conn) = self.conn.upgrade() { + if set_input_focus { + if let Err(err) = conn.set_input_focus(InputFocus::NONE, self.window, x11rb::CURRENT_TIME) { + warn!("Unable to set focus for X11Surface ({:?}): {}", self.window, err); + } + } + + if send_take_focus { + let event = ClientMessageEvent::new( + 32, + self.window, + self.atoms.WM_PROTOCOLS, + [self.atoms.WM_TAKE_FOCUS, x11rb::CURRENT_TIME, 0, 0, 0], + ); + if let Err(err) = conn.send_event(false, self.window, EventMask::NO_EVENT, event) { + warn!( + "Unable to send take focus event for X11Surface ({:?}): {}", + self.window, err + ); + } + let _ = conn.flush(); + } + + let _ = conn.flush(); + } + + let mut state = self.state.lock().unwrap(); + if let Some(surface) = state.wl_surface.as_ref() { + KeyboardTarget::enter(surface, seat, data, keys, serial); + } else { + state.pending_enter = Some(( + Box::new(seat.clone()) as Box, + keys.iter().map(|x| x.raw_code()).collect(), + None, + serial, + )); + } + } + + fn leave(&self, seat: &Seat, data: &mut D, serial: Serial) { + if self.input_model() == WmInputModel::None { + return; + } + + if let Some(release) = &self.focus_release { + release.schedule(); + } + + let mut state = self.state.lock().unwrap(); + let _ = state.pending_enter.take(); + if let Some(surface) = state.wl_surface.as_ref() { + KeyboardTarget::leave(surface, seat, data, serial); + } + } + + fn key( + &self, + seat: &Seat, + data: &mut D, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: InputTime, + ) { + let mut xstate = self.state.lock().unwrap(); + if let Some(surface) = xstate.wl_surface.as_ref() { + KeyboardTarget::key(surface, seat, data, key, state, serial, time) + } else if let Some((_, keys, _, pending_serial)) = xstate.pending_enter.as_mut() { + let raw = key.raw_code(); + if state == KeyState::Released { + keys.retain(|c| *c != raw); + } else { + keys.push(raw); + } + *pending_serial = serial + } + } + + fn modifiers(&self, seat: &Seat, data: &mut D, modifiers: ModifiersState, serial: Serial) { + let mut state = self.state.lock().unwrap(); + if let Some(surface) = state.wl_surface.as_ref() { + KeyboardTarget::modifiers(surface, seat, data, modifiers, serial); + } else if let Some((_, _, pending_modifiers, pending_serial)) = state.pending_enter.as_mut() { + *pending_modifiers = Some(modifiers); + *pending_serial = serial; + } + } +} + +impl PointerTarget for X11Surface { + fn enter(&self, seat: &Seat, data: &mut D, event: &MotionEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::enter(surface, seat, data, event); + } + } + + fn motion(&self, seat: &Seat, data: &mut D, event: &MotionEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::motion(surface, seat, data, event); + } + } + + fn relative_motion(&self, seat: &Seat, data: &mut D, event: &RelativeMotionEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::relative_motion(surface, seat, data, event); + } + } + + fn button(&self, seat: &Seat, data: &mut D, event: &ButtonEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::button(surface, seat, data, event); + } + } + + fn axis(&self, seat: &Seat, data: &mut D, frame: AxisFrame) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::axis(surface, seat, data, frame); + } + } + + fn frame(&self, seat: &Seat, data: &mut D) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::frame(surface, seat, data); + } + } + + fn leave(&self, seat: &Seat, data: &mut D, serial: Serial, time: InputTime) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::leave(surface, seat, data, serial, time); + } + } + + fn gesture_swipe_begin(&self, seat: &Seat, data: &mut D, event: &GestureSwipeBeginEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_swipe_begin(surface, seat, data, event); + } + } + + fn gesture_swipe_update(&self, seat: &Seat, data: &mut D, event: &GestureSwipeUpdateEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_swipe_update(surface, seat, data, event); + } + } + + fn gesture_swipe_end(&self, seat: &Seat, data: &mut D, event: &GestureSwipeEndEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_swipe_end(surface, seat, data, event); + } + } + + fn gesture_pinch_begin(&self, seat: &Seat, data: &mut D, event: &GesturePinchBeginEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_pinch_begin(surface, seat, data, event) + } + } + + fn gesture_pinch_update(&self, seat: &Seat, data: &mut D, event: &GesturePinchUpdateEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_pinch_update(surface, seat, data, event) + } + } + + fn gesture_pinch_end(&self, seat: &Seat, data: &mut D, event: &GesturePinchEndEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_pinch_end(surface, seat, data, event) + } + } + + fn gesture_hold_begin(&self, seat: &Seat, data: &mut D, event: &GestureHoldBeginEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_hold_begin(surface, seat, data, event) + } + } + + fn gesture_hold_end(&self, seat: &Seat, data: &mut D, event: &GestureHoldEndEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + PointerTarget::gesture_hold_end(surface, seat, data, event) + } + } +} + +impl TouchTarget for X11Surface { + fn down(&self, seat: &Seat, data: &mut D, event: &crate::input::touch::DownEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::down(surface, seat, data, event) + } + } + + fn up(&self, seat: &Seat, data: &mut D, event: &crate::input::touch::UpEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::up(surface, seat, data, event) + } + } + + fn motion(&self, seat: &Seat, data: &mut D, event: &crate::input::touch::MotionEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::motion(surface, seat, data, event) + } + } + + fn frame(&self, seat: &Seat, data: &mut D, marker: FrameMarker) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::frame(surface, seat, data, marker) + } + } + + fn cancel(&self, seat: &Seat, data: &mut D, marker: FrameMarker) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::cancel(surface, seat, data, marker) + } + } + + fn shape(&self, seat: &Seat, data: &mut D, event: &crate::input::touch::ShapeEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::shape(surface, seat, data, event) + } + } + + fn orientation(&self, seat: &Seat, data: &mut D, event: &crate::input::touch::OrientationEvent) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::orientation(surface, seat, data, event) + } + } + + fn last_frame(&self, seat: &Seat, data: &mut D) -> Option { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TouchTarget::last_frame(surface, seat, data) + } else { + None + } + } +} + +impl TabletToolTarget for X11Surface { + fn proximity_in( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + tablet: &crate::input::tablet::Tablet, + serial: Serial, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::proximity_in(surface, seat, data, tool_descriptor, tablet, serial); + } + } + + fn proximity_out( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::proximity_out(surface, seat, data, tool_descriptor); + } + } + + fn down( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + event: &crate::input::tablet::tool::DownEvent, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::down(surface, seat, data, tool_descriptor, event); + } + } + + fn up( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + event: &crate::input::tablet::tool::UpEvent, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::up(surface, seat, data, tool_descriptor, event); + } + } + + fn motion( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + event: &crate::input::tablet::tool::MotionEvent, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::motion(surface, seat, data, tool_descriptor, event); + } + } + + fn button( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + event: &crate::input::tablet::tool::ButtonEvent, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::button(surface, seat, data, tool_descriptor, event); + } + } + + fn axis( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + frame: crate::input::tablet::tool::AxisFrame, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::axis(surface, seat, data, tool_descriptor, frame); + } + } + + fn frame( + &self, + seat: &Seat, + data: &mut D, + tool_descriptor: &crate::backend::input::TabletToolDescriptor, + time: InputTime, + ) { + if let Some(surface) = self.state.lock().unwrap().wl_surface.as_ref() { + TabletToolTarget::frame(surface, seat, data, tool_descriptor, time); + } + } +} + +impl WaylandFocus for X11Surface { + fn wl_surface(&self) -> Option> { + X11Surface::wl_surface(self).map(Cow::Owned) + } +} + +fn fetch_opaque_regions( + conn: &Arc, + window: X11Window, + property_atom: Atom, + client_scale: Option<&Arc>, +) -> Result, ConnectionError> { + let reply = conn + .get_property(false, window, property_atom, AtomEnum::CARDINAL, 0, u32::MAX)? + .reply_unchecked()?; + + let opaque_region = reply + .and_then(|reply| { + reply.value32().map(|values| { + let client_scale = client_scale + .as_ref() + .map(|s| s.load(Ordering::Acquire)) + .unwrap_or(1.); + + values + .collect::>() + .chunks_exact(4) + .flat_map(|rect_values| { + let phys_rect = Rectangle::::new( + (rect_values[0] as i32, rect_values[1] as i32).into(), + ((rect_values[2] as i32).max(0), (rect_values[3] as i32).max(0)).into(), + ); + if phys_rect.is_empty() { + None + } else { + Some(( + RectangleKind::Add, + phys_rect.to_f64().to_logical(client_scale).to_i32_round::(), + )) + } + }) + .collect::>() + }) + }) + .map(|rects| RegionAttributes { rects }); + + Ok(opaque_region) +} diff --git a/vendor/smithay/test_clients/Cargo.toml b/vendor/smithay/test_clients/Cargo.toml new file mode 100644 index 0000000..d18f5b3 --- /dev/null +++ b/vendor/smithay/test_clients/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test_clients" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +smithay-client-toolkit = "0.19.2" +tracing = { version = "0.1.37" } +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } diff --git a/vendor/smithay/test_clients/src/bin/test_pointer_constraints.rs b/vendor/smithay/test_clients/src/bin/test_pointer_constraints.rs new file mode 100644 index 0000000..9b7b42e --- /dev/null +++ b/vendor/smithay/test_clients/src/bin/test_pointer_constraints.rs @@ -0,0 +1,355 @@ +//! Constrain the pointer to surface region + +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_output, delegate_pointer, delegate_pointer_constraints, delegate_registry, + delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, + output::{OutputHandler, OutputState}, + reexports::{ + calloop, client as wayland_client, + protocols::wp::pointer_constraints::zv1::client::{ + zwp_confined_pointer_v1::ZwpConfinedPointerV1, zwp_locked_pointer_v1::ZwpLockedPointerV1, + zwp_pointer_constraints_v1, + }, + }, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::{ + Capability, SeatHandler, SeatState, + pointer::{PointerEvent, PointerEventKind, PointerHandler}, + pointer_constraints::{PointerConstraintsHandler, PointerConstraintsState}, + }, + shell::{ + WaylandSurface, + xdg::{ + XdgShell, + window::{Window, WindowConfigure, WindowDecorations, WindowHandler}, + }, + }, + shm::{ + Shm, ShmHandler, + slot::{Buffer, SlotPool}, + }, +}; + +use wayland_client::{ + Connection, QueueHandle, delegate_noop, + protocol::{ + wl_output::{self, WlOutput}, + wl_pointer::WlPointer, + wl_region::WlRegion, + wl_seat, + wl_surface::{self, WlSurface}, + }, +}; + +use tracing::{info, warn}; + +fn main() { + test_clients::init_logging(); + + let (mut event_loop, globals, qh) = test_clients::init_connection::(); + + let compositor_state = CompositorState::bind(&globals, &qh).unwrap(); + let xdg_shell = XdgShell::bind(&globals, &qh).unwrap(); + + let surface = compositor_state.create_surface(&qh); + let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh); + + let shm = Shm::bind(&globals, &qh).unwrap(); + let pool = SlotPool::new(256 * 256 * 4, &shm).unwrap(); + + let mut simple_window = App { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + compositor_state, + seat_state: SeatState::new(&globals, &qh), + shm, + pointer: None, + confined_pointer: None, + pointer_constraint_state: PointerConstraintsState::bind(&globals, &qh), + + should_be_mapped: false, + + first_configure: true, + pool, + width: 256, + height: 256, + shift: 0, + buffer: None, + window, + loop_signal: event_loop.get_signal(), + }; + + simple_window.map(); + + event_loop.run(None, &mut simple_window, |_| {}).unwrap(); +} + +struct App { + registry_state: RegistryState, + output_state: OutputState, + compositor_state: CompositorState, + seat_state: SeatState, + shm: Shm, + + pointer: Option, + confined_pointer: Option, + pointer_constraint_state: PointerConstraintsState, + + should_be_mapped: bool, + + first_configure: bool, + pool: SlotPool, + width: u32, + height: u32, + shift: u32, + buffer: Option, + window: Window, + loop_signal: calloop::LoopSignal, +} + +impl CompositorHandler for App { + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.draw(conn, qh); + } + fn surface_enter(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn surface_leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn scale_factor_changed(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: i32) {} + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: wl_output::Transform, + ) { + } +} + +impl WindowHandler for App { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, _: &Window) { + self.loop_signal.stop(); + } + + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + self.buffer = None; + self.width = configure.new_size.0.map(|v| v.get()).unwrap_or(256); + self.height = configure.new_size.1.map(|v| v.get()).unwrap_or(256); + + if self.first_configure { + self.first_configure = false; + self.draw(conn, qh); + } + + if let Some(confined_pointer) = &self.confined_pointer { + confined_pointer.destroy(); + self.confined_pointer = None; + } + + if let Some(pointer) = &self.pointer { + let region = self.compositor_state.wl_compositor().create_region(qh, ()); + let w = self.width as i32; + let h = self.height as i32; + + region.add(0, 0, w, h); + region.subtract(w / 2, h / 2, w / 2, h / 2); + region.subtract(0, 0, w / 4, h / 4); + + let confined_pointer = self + .pointer_constraint_state + .confine_pointer( + self.window.wl_surface(), + &pointer, + Some(®ion), + zwp_pointer_constraints_v1::Lifetime::Persistent, + qh, + ) + .unwrap(); + + self.confined_pointer = Some(confined_pointer); + } + } +} + +impl App { + fn map(&mut self) { + self.first_configure = true; + self.should_be_mapped = true; + self.window.commit(); + } + + fn draw(&mut self, _conn: &Connection, qh: &QueueHandle) { + if !self.should_be_mapped { + return; + } + + test_clients::draw( + qh, + self.window.wl_surface(), + &mut self.pool, + &mut self.buffer, + self.width, + self.height, + &mut self.shift, + ); + } +} + +delegate_compositor!(App); +delegate_output!(App); +delegate_shm!(App); + +delegate_xdg_shell!(App); +delegate_xdg_window!(App); + +delegate_registry!(App); +delegate_seat!(App); +delegate_pointer!(App); +delegate_pointer_constraints!(App); +delegate_noop!(App: WlRegion); + +impl SeatHandler for App { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + info!("add capability: {capability}"); + if capability == Capability::Pointer && self.pointer.is_none() { + let pointer = self + .seat_state + .get_pointer(qh, &seat) + .expect("Failed to create pointer"); + + self.pointer = Some(pointer); + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _: &QueueHandle, + _: wl_seat::WlSeat, + capability: Capability, + ) { + warn!("remove capability: {capability}"); + if capability == Capability::Pointer && self.pointer.is_some() { + self.confined_pointer.take().unwrap().destroy(); + self.pointer.take().unwrap().release(); + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} +} + +impl PointerHandler for App { + fn pointer_frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _pointer: &WlPointer, + events: &[PointerEvent], + ) { + for event in events { + match event.kind { + PointerEventKind::Enter { .. } => { + info!("Pointer entered @{:?}", event.position); + } + PointerEventKind::Leave { .. } => { + info!("Pointer left"); + } + PointerEventKind::Motion { .. } => { + info!("Pointer motion: {:?}", event.position); + } + _ => {} + } + } + } +} + +impl PointerConstraintsHandler for App { + fn confined( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _confined_pointer: &ZwpConfinedPointerV1, + _surface: &WlSurface, + _pointer: &WlPointer, + ) { + info!("Pointer confined"); + } + + fn unconfined( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _confined_pointer: &ZwpConfinedPointerV1, + _surface: &WlSurface, + _pointer: &WlPointer, + ) { + warn!("Pointer unconfined"); + } + + fn locked( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _locked_pointer: &ZwpLockedPointerV1, + _surface: &WlSurface, + _pointer: &WlPointer, + ) { + } + + fn unlocked( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _locked_pointer: &ZwpLockedPointerV1, + _surface: &WlSurface, + _pointer: &WlPointer, + ) { + } +} + +impl OutputHandler for App { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} +} + +impl ShmHandler for App { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl ProvidesRegistryState for App { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState,]; +} diff --git a/vendor/smithay/test_clients/src/bin/test_subsurface.rs b/vendor/smithay/test_clients/src/bin/test_subsurface.rs new file mode 100644 index 0000000..c6da870 --- /dev/null +++ b/vendor/smithay/test_clients/src/bin/test_subsurface.rs @@ -0,0 +1,236 @@ +//! Attempt to reproduce https://github.com/Smithay/smithay/issues/1894 + +use smithay_client_toolkit::delegate_subcompositor; +use smithay_client_toolkit::reexports::client::protocol::wl_subsurface::WlSubsurface; +use smithay_client_toolkit::reexports::{calloop, client as wayland_client}; + +use smithay_client_toolkit::subcompositor::SubcompositorState; +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_output, delegate_registry, delegate_shm, delegate_xdg_shell, + delegate_xdg_window, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + shell::{ + WaylandSurface, + xdg::{ + XdgShell, + window::{Window, WindowConfigure, WindowDecorations, WindowHandler}, + }, + }, + shm::{ + Shm, ShmHandler, + slot::{Buffer, SlotPool}, + }, +}; +use tracing::info; +use wayland_client::{ + Connection, QueueHandle, + protocol::{ + wl_output::{self, WlOutput}, + wl_surface::{self, WlSurface}, + }, +}; + +fn main() { + test_clients::init_logging(); + + let (mut event_loop, globals, qh) = test_clients::init_connection::(); + + let compositor = CompositorState::bind(&globals, &qh).unwrap(); + let subcompositor = SubcompositorState::bind(compositor.wl_compositor().clone(), &globals, &qh).unwrap(); + let xdg_shell = XdgShell::bind(&globals, &qh).unwrap(); + + let surface = compositor.create_surface(&qh); + let subsurface = subcompositor.create_subsurface(surface.clone(), &qh); + + let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh); + + let shm = Shm::bind(&globals, &qh).unwrap(); + let pool = SlotPool::new(256 * 256 * 4, &shm).unwrap(); + + let mut app = App { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + shm, + + should_be_mapped: false, + + first_configure: true, + pool, + width: 256, + height: 256, + shift: 0, + buffer: None, + window, + subsurface, + commit_only_once: true, + loop_signal: event_loop.get_signal(), + }; + + app.map_toggle(); + + event_loop.run(None, &mut app, |_| {}).unwrap(); +} + +struct App { + registry_state: RegistryState, + output_state: OutputState, + shm: Shm, + + should_be_mapped: bool, + + first_configure: bool, + pool: SlotPool, + width: u32, + height: u32, + shift: u32, + buffer: Option, + window: Window, + subsurface: (WlSubsurface, WlSurface), + commit_only_once: bool, + + loop_signal: calloop::LoopSignal, +} + +impl CompositorHandler for App { + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.draw(conn, qh); + } + fn surface_enter(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn surface_leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn scale_factor_changed(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: i32) {} + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: wl_output::Transform, + ) { + } +} + +impl WindowHandler for App { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, _: &Window) { + self.loop_signal.stop(); + } + + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + if self.first_configure { + info!("Window initial configure"); + } else { + info!("Window configure"); + } + + self.buffer = None; + self.width = configure.new_size.0.map(|v| v.get()).unwrap_or(256); + self.height = configure.new_size.1.map(|v| v.get()).unwrap_or(256); + + if self.first_configure { + self.first_configure = false; + self.draw(conn, qh); + } + } +} + +impl App { + fn map_toggle(&mut self) { + if self.should_be_mapped { + self.unmap() + } else { + self.map() + } + } + + fn map(&mut self) { + self.first_configure = true; + self.should_be_mapped = true; + self.window.commit(); + } + + fn unmap(&mut self) { + self.buffer = None; + self.first_configure = false; + self.should_be_mapped = false; + self.window.attach(None, 0, 0); + self.window.commit(); + } + + fn draw(&mut self, _conn: &Connection, qh: &QueueHandle) { + if !self.should_be_mapped { + return; + } + + if std::mem::take(&mut self.commit_only_once) { + test_clients::draw( + qh, + &self.subsurface.1, + &mut self.pool, + &mut self.buffer, + self.width, + self.height, + &mut self.shift, + ); + } + + // Test if setting the pos and never calling commit on the subsurface still applies the pos on partent commit + self.subsurface.0.set_position(self.shift as i32, 20); + + test_clients::draw( + qh, + self.window.wl_surface(), + &mut self.pool, + &mut self.buffer, + self.width, + self.height, + &mut self.shift, + ); + } +} + +delegate_compositor!(App); +delegate_subcompositor!(App); + +delegate_output!(App); +delegate_shm!(App); + +delegate_xdg_shell!(App); +delegate_xdg_window!(App); + +delegate_registry!(App); + +impl OutputHandler for App { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} +} + +impl ShmHandler for App { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl ProvidesRegistryState for App { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState,]; +} diff --git a/vendor/smithay/test_clients/src/bin/test_xdg_map_unmap.rs b/vendor/smithay/test_clients/src/bin/test_xdg_map_unmap.rs new file mode 100644 index 0000000..c2dcc3c --- /dev/null +++ b/vendor/smithay/test_clients/src/bin/test_xdg_map_unmap.rs @@ -0,0 +1,216 @@ +//! Cycles through initial commit-configure sequence, over and over again (every 1s) + +use std::time::Duration; + +use smithay_client_toolkit::reexports::{calloop, client as wayland_client}; + +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_output, delegate_registry, delegate_shm, delegate_xdg_shell, + delegate_xdg_window, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + shell::{ + WaylandSurface, + xdg::{ + XdgShell, + window::{Window, WindowConfigure, WindowDecorations, WindowHandler}, + }, + }, + shm::{ + Shm, ShmHandler, + slot::{Buffer, SlotPool}, + }, +}; +use tracing::info; +use wayland_client::{ + Connection, QueueHandle, + protocol::{ + wl_output::{self, WlOutput}, + wl_surface::{self, WlSurface}, + }, +}; + +fn main() { + test_clients::init_logging(); + + let (mut event_loop, globals, qh) = test_clients::init_connection::(); + + let compositor = CompositorState::bind(&globals, &qh).unwrap(); + let xdg_shell = XdgShell::bind(&globals, &qh).unwrap(); + + let surface = compositor.create_surface(&qh); + let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh); + + let shm = Shm::bind(&globals, &qh).unwrap(); + let pool = SlotPool::new(256 * 256 * 4, &shm).unwrap(); + + let mut simple_window = App { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + shm, + + should_be_mapped: false, + + first_configure: true, + pool, + width: 256, + height: 256, + shift: 0, + buffer: None, + window, + loop_signal: event_loop.get_signal(), + }; + + event_loop + .handle() + .insert_source(calloop::timer::Timer::immediate(), |_, _, app| { + app.map_toggle(); + calloop::timer::TimeoutAction::ToDuration(Duration::from_secs(1)) + }) + .unwrap(); + + event_loop.run(None, &mut simple_window, |_| {}).unwrap(); +} + +struct App { + registry_state: RegistryState, + output_state: OutputState, + shm: Shm, + + should_be_mapped: bool, + + first_configure: bool, + pool: SlotPool, + width: u32, + height: u32, + shift: u32, + buffer: Option, + window: Window, + loop_signal: calloop::LoopSignal, +} + +impl CompositorHandler for App { + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.draw(conn, qh); + } + fn surface_enter(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn surface_leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn scale_factor_changed(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: i32) {} + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: wl_output::Transform, + ) { + } +} + +impl WindowHandler for App { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, _: &Window) { + self.loop_signal.stop(); + } + + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + if self.first_configure { + info!("Window initial configure"); + } else { + info!("Window configure"); + } + + self.buffer = None; + self.width = configure.new_size.0.map(|v| v.get()).unwrap_or(256); + self.height = configure.new_size.1.map(|v| v.get()).unwrap_or(256); + + if self.first_configure { + self.first_configure = false; + self.draw(conn, qh); + } + } +} + +impl App { + fn map_toggle(&mut self) { + if self.should_be_mapped { + self.unmap() + } else { + self.map() + } + } + + fn map(&mut self) { + self.first_configure = true; + self.should_be_mapped = true; + self.window.commit(); + } + + fn unmap(&mut self) { + self.buffer = None; + self.first_configure = false; + self.should_be_mapped = false; + self.window.attach(None, 0, 0); + self.window.commit(); + } + + fn draw(&mut self, _conn: &Connection, qh: &QueueHandle) { + if !self.should_be_mapped { + return; + } + + test_clients::draw( + qh, + self.window.wl_surface(), + &mut self.pool, + &mut self.buffer, + self.width, + self.height, + &mut self.shift, + ); + } +} + +delegate_compositor!(App); +delegate_output!(App); +delegate_shm!(App); + +delegate_xdg_shell!(App); +delegate_xdg_window!(App); + +delegate_registry!(App); + +impl OutputHandler for App { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} +} + +impl ShmHandler for App { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl ProvidesRegistryState for App { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState,]; +} diff --git a/vendor/smithay/test_clients/src/lib.rs b/vendor/smithay/test_clients/src/lib.rs new file mode 100644 index 0000000..2be5139 --- /dev/null +++ b/vendor/smithay/test_clients/src/lib.rs @@ -0,0 +1,107 @@ +use smithay_client_toolkit::{ + reexports::{ + calloop, + calloop_wayland_source::WaylandSource, + client::{ + self as wayland_client, + globals::GlobalList, + protocol::{wl_callback::WlCallback, wl_shm, wl_surface::WlSurface}, + }, + }, + shm::slot::{Buffer, SlotPool}, +}; + +use calloop::EventLoop; +use wayland_client::{ + Connection, Dispatch, QueueHandle, globals::GlobalListContents, globals::registry_queue_init, + protocol::wl_registry::WlRegistry, +}; + +pub fn init_logging() { + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt() + .compact() + .with_env_filter(env_filter) + .init(); + } else { + tracing_subscriber::fmt().compact().init(); + } +} + +pub fn init_connection() -> (EventLoop<'static, APP>, GlobalList, QueueHandle) +where + APP: Dispatch + 'static, +{ + let conn = Connection::connect_to_env().unwrap(); + + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + let event_loop: EventLoop = EventLoop::try_new().unwrap(); + let loop_handle = event_loop.handle(); + WaylandSource::new(conn.clone(), event_queue) + .insert(loop_handle) + .unwrap(); + + (event_loop, globals, qh) +} + +pub fn fill_with_gradient_bytes(canvas: &mut [u8], shift: u32, width: u32, height: u32) { + canvas.chunks_exact_mut(4).enumerate().for_each(|(index, chunk)| { + let x = ((index + shift as usize) % width as usize) as u32; + let y = (index / width as usize) as u32; + + let a = 0xFF; + let r = u32::min(((width - x) * 0xFF) / width, ((height - y) * 0xFF) / height); + let g = u32::min((x * 0xFF) / width, ((height - y) * 0xFF) / height); + let b = u32::min(((width - x) * 0xFF) / width, (y * 0xFF) / height); + let color = (a << 24) + (r << 16) + (g << 8) + b; + + let array: &mut [u8; 4] = chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + }); +} + +pub fn draw( + qh: &QueueHandle, + wl_surface: &WlSurface, + pool: &mut SlotPool, + buffer: &mut Option, + width: u32, + height: u32, + shift: &mut u32, +) where + D: 'static, + D: Dispatch, +{ + let stride = width as i32 * 4; + + let buffer = buffer.get_or_insert_with(|| { + pool.create_buffer(width as i32, height as i32, stride, wl_shm::Format::Argb8888) + .unwrap() + .0 + }); + + let canvas = match pool.canvas(buffer) { + Some(canvas) => canvas, + None => { + // This should be rare, but if the compositor has not released the previous + // buffer, we need double-buffering. + let (second_buffer, canvas) = pool + .create_buffer(width as i32, height as i32, stride, wl_shm::Format::Argb8888) + .unwrap(); + *buffer = second_buffer; + canvas + } + }; + + // Draw to the window: + fill_with_gradient_bytes(canvas, *shift, width, height); + *shift = (*shift + 1) % width; + + wl_surface.damage_buffer(0, 0, width as i32, height as i32); + + wl_surface.frame(qh, wl_surface.clone()); + + buffer.attach_to(wl_surface).expect("buffer attach"); + wl_surface.commit(); +} diff --git a/vendor/smithay/test_gbm_bo_create_with_modifiers2.c b/vendor/smithay/test_gbm_bo_create_with_modifiers2.c new file mode 100644 index 0000000..3fcecc4 --- /dev/null +++ b/vendor/smithay/test_gbm_bo_create_with_modifiers2.c @@ -0,0 +1,5 @@ +#include + +void test() { + gbm_bo_create_with_modifiers2(NULL, 0, 0, 0, NULL, 0, 0); +} \ No newline at end of file diff --git a/vendor/smithay/test_gbm_bo_get_fd_for_plane.c b/vendor/smithay/test_gbm_bo_get_fd_for_plane.c new file mode 100644 index 0000000..62501b0 --- /dev/null +++ b/vendor/smithay/test_gbm_bo_get_fd_for_plane.c @@ -0,0 +1,5 @@ +#include + +void test() { + gbm_bo_get_fd_for_plane(NULL, 0); +} \ No newline at end of file diff --git a/vendor/smithay/typos.toml b/vendor/smithay/typos.toml new file mode 100644 index 0000000..b3b43ba --- /dev/null +++ b/vendor/smithay/typos.toml @@ -0,0 +1,5 @@ +[default] + +[default.extend-words] +WRONLY = "WRONLY" +eis = "eis" diff --git a/vendor/smithay/wlcs_anvil/Cargo.toml b/vendor/smithay/wlcs_anvil/Cargo.toml new file mode 100644 index 0000000..47c6ddc --- /dev/null +++ b/vendor/smithay/wlcs_anvil/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wlcs_anvil" +version = "0.0.1" +authors = ["Victor Berger ", "Drakulix (Victoria Brekenfeld)"] +license = "MIT" +publish = false +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +smithay = { path = "..", default-features=false, features=["wayland_frontend", "backend_egl", "use_system_lib", "renderer_test"] } +anvil = { path = "../anvil", default-features=false } +wayland-sys = { version = "0.31.1", features = ["client", "server"] } +wlcs = "0.1" diff --git a/vendor/smithay/wlcs_anvil/src/lib.rs b/vendor/smithay/wlcs_anvil/src/lib.rs new file mode 100644 index 0000000..b4e4e86 --- /dev/null +++ b/vendor/smithay/wlcs_anvil/src/lib.rs @@ -0,0 +1,266 @@ +mod main_loop; + +use std::{ + io::{Error, ErrorKind}, + os::{ + fd::{AsRawFd, OwnedFd}, + unix::net::UnixStream, + }, + sync::atomic::{AtomicU32, Ordering}, + thread::JoinHandle, +}; + +use smithay::{ + reexports::calloop::{ + self, + channel::{Sender, channel}, + }, + utils::{Logical, Point}, +}; + +use wayland_sys::{ + client::*, + common::{wl_fixed_t, wl_fixed_to_double}, + ffi_dispatch, +}; +use wlcs::{ + Wlcs, extension_list, + ffi_display_server_api::{WlcsExtensionDescriptor, WlcsIntegrationDescriptor, WlcsServerIntegration}, + ffi_wrappers::wlcs_server, + wlcs_server_integration, +}; + +wlcs_server_integration!(AnvilDisplayServerHandle); + +static SUPPORTED_EXTENSIONS: &[WlcsExtensionDescriptor] = extension_list!( + ("wl_compositor", 4), + ("wl_subcompositor", 1), + ("wl_data_device_manager", 3), + ("wl_seat", 7), + ("wl_output", 4), + ("xdg_wm_base", 3), +); + +static DESCRIPTOR: WlcsIntegrationDescriptor = WlcsIntegrationDescriptor { + version: 1, + num_extensions: SUPPORTED_EXTENSIONS.len(), + supported_extensions: SUPPORTED_EXTENSIONS.as_ptr(), +}; + +static DEVICE_ID: AtomicU32 = AtomicU32::new(0); + +/// Event sent by WLCS to control the compositor +#[derive(Debug)] +pub enum WlcsEvent { + /// Stop the running server + Exit, + /// Create a new client from given RawFd + NewClient { + stream: UnixStream, + client_id: i32, + }, + /// Position this window from the client associated with this Fd on the global space + PositionWindow { + client_id: i32, + surface_id: u32, + location: Point, + }, + /* Pointer related events */ + /// A new pointer device is available + NewPointer { + device_id: u32, + }, + /// Move the pointer in absolute coordinate space + PointerMoveAbsolute { + device_id: u32, + location: Point, + }, + /// Move the pointer in relative coordinate space + PointerMoveRelative { + device_id: u32, + delta: Point, + }, + /// Press a pointer button + PointerButtonDown { + device_id: u32, + button_id: i32, + }, + /// Release a pointer button + PointerButtonUp { + device_id: u32, + button_id: i32, + }, + /// A pointer device is removed + PointerRemoved { + device_id: u32, + }, + /* Touch related events */ + /// A new touch device is available + NewTouch { + device_id: u32, + }, + /// A touch point is down + TouchDown { + device_id: u32, + location: Point, + }, + /// A touch point moved + TouchMove { + device_id: u32, + location: Point, + }, + /// A touch point is up + TouchUp { + device_id: u32, + }, + TouchRemoved { + device_id: u32, + }, +} + +struct AnvilDisplayServerHandle { + server: Option<(Sender, JoinHandle<()>)>, +} + +impl Wlcs for AnvilDisplayServerHandle { + type Pointer = PointerHandle; + type Touch = TouchHandle; + + fn new() -> Self { + AnvilDisplayServerHandle { server: None } + } + + fn start(&mut self) { + let (tx, rx) = channel(); + let join = crate::start_anvil(rx); + self.server = Some((tx, join)); + } + + fn stop(&mut self) { + if let Some((sender, join)) = self.server.take() { + let _ = sender.send(WlcsEvent::Exit); + let _ = join.join(); + } + } + + fn create_client_socket(&self) -> std::io::Result { + if let Some((ref sender, _)) = self.server { + if let Ok((client_side, server_side)) = UnixStream::pair() { + if let Err(e) = sender.send(WlcsEvent::NewClient { + stream: server_side, + client_id: client_side.as_raw_fd(), + }) { + return Err(Error::new(ErrorKind::ConnectionReset, e)); + } + return Ok(client_side.into()); + } + } + Err(Error::from(ErrorKind::NotFound)) + } + + fn position_window_absolute(&self, display: *mut wl_display, surface: *mut wl_proxy, x: i32, y: i32) { + let client_id = unsafe { ffi_dispatch!(wayland_client_handle(), wl_display_get_fd, display) }; + let surface_id = unsafe { ffi_dispatch!(wayland_client_handle(), wl_proxy_get_id, surface) }; + if let Some((ref sender, _)) = self.server { + let _ = sender.send(WlcsEvent::PositionWindow { + client_id, + surface_id, + location: (x, y).into(), + }); + } + } + + fn create_pointer(&mut self) -> Option { + let Some(ref server) = self.server else { + return None; + }; + Some(PointerHandle { + device_id: DEVICE_ID.fetch_add(1, Ordering::Relaxed), + sender: server.0.clone(), + }) + } + + fn create_touch(&mut self) -> Option { + let Some(ref server) = self.server else { + return None; + }; + Some(TouchHandle { + device_id: DEVICE_ID.fetch_add(1, Ordering::Relaxed), + sender: server.0.clone(), + }) + } + + fn get_descriptor(&self) -> &WlcsIntegrationDescriptor { + &crate::DESCRIPTOR + } +} + +struct PointerHandle { + device_id: u32, + sender: Sender, +} + +impl wlcs::Pointer for PointerHandle { + fn move_absolute(&mut self, x: wl_fixed_t, y: wl_fixed_t) { + let _ = self.sender.send(WlcsEvent::PointerMoveAbsolute { + device_id: self.device_id, + location: (wl_fixed_to_double(x), wl_fixed_to_double(y)).into(), + }); + } + + fn move_relative(&mut self, dx: wl_fixed_t, dy: wl_fixed_t) { + let _ = self.sender.send(WlcsEvent::PointerMoveRelative { + device_id: self.device_id, + delta: (wl_fixed_to_double(dx), wl_fixed_to_double(dy)).into(), + }); + } + + fn button_up(&mut self, button: i32) { + let _ = self.sender.send(WlcsEvent::PointerButtonUp { + device_id: self.device_id, + button_id: button, + }); + } + + fn button_down(&mut self, button: i32) { + let _ = self.sender.send(WlcsEvent::PointerButtonDown { + device_id: self.device_id, + button_id: button, + }); + } + + fn destroy(&mut self) {} +} + +struct TouchHandle { + device_id: u32, + sender: Sender, +} + +impl wlcs::Touch for TouchHandle { + fn touch_down(&mut self, x: wl_fixed_t, y: wl_fixed_t) { + let _ = self.sender.send(WlcsEvent::TouchDown { + device_id: self.device_id, + location: (wl_fixed_to_double(x), wl_fixed_to_double(y)).into(), + }); + } + + fn touch_move(&mut self, x: wl_fixed_t, y: wl_fixed_t) { + let _ = self.sender.send(WlcsEvent::TouchMove { + device_id: self.device_id, + location: (wl_fixed_to_double(x), wl_fixed_to_double(y)).into(), + }); + } + + fn touch_up(&mut self) { + let _ = self.sender.send(WlcsEvent::TouchUp { + device_id: self.device_id, + }); + } + + fn destroy(&mut self) {} +} + +fn start_anvil(channel: calloop::channel::Channel) -> JoinHandle<()> { + std::thread::spawn(move || main_loop::run(channel)) +} diff --git a/vendor/smithay/wlcs_anvil/src/main_loop.rs b/vendor/smithay/wlcs_anvil/src/main_loop.rs new file mode 100644 index 0000000..e3ce3ea --- /dev/null +++ b/vendor/smithay/wlcs_anvil/src/main_loop.rs @@ -0,0 +1,312 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex, atomic::Ordering}, + time::Duration, +}; + +use smithay::{ + backend::{ + input::{ButtonState, InputTime}, + renderer::{ + damage::OutputDamageTracker, + element::AsRenderElements, + test::{DummyFramebuffer, DummyRenderer}, + }, + }, + input::pointer::{ + ButtonEvent, CursorImageAttributes, CursorImageStatus, MotionEvent, RelativeMotionEvent, + }, + output::{Mode, Output, PhysicalProperties, Subpixel}, + reexports::{ + calloop::{ + EventLoop, + channel::{Channel, Event as ChannelEvent}, + }, + wayland_server::{Client, Display, Resource, protocol::wl_surface}, + }, + utils::{IsAlive, SERIAL_COUNTER as SCOUNTER, Scale}, + wayland::compositor, +}; + +use anvil::{AnvilState, ClientState, drawing::PointerElement, render::*, state::Backend}; + +use crate::WlcsEvent; + +const OUTPUT_NAME: &str = "anvil"; + +struct TestState { + clients: HashMap, +} + +impl Backend for TestState { + fn seat_name(&self) -> String { + "anvil_wlcs".into() + } + + fn reset_buffers(&mut self, _output: &Output) {} + fn early_import(&mut self, _surface: &wl_surface::WlSurface) {} + fn update_led_state(&mut self, _led_state: smithay::input::keyboard::LedState) {} +} + +pub fn run(channel: Channel) { + let mut event_loop = + EventLoop::>::try_new().expect("Failed to init the event loop."); + + let display = Display::new().expect("Failed to init display"); + let test_state = TestState { + clients: HashMap::new(), + }; + let mut state = AnvilState::init(display, event_loop.handle(), test_state, false); + + event_loop + .handle() + .insert_source(channel, move |event, &mut (), data| match event { + ChannelEvent::Msg(evt) => handle_event(evt, data), + ChannelEvent::Closed => handle_event(WlcsEvent::Exit, data), + }) + .unwrap(); + + let mut renderer = DummyRenderer::default(); + let mut framebuffer = DummyFramebuffer; + + let mode = Mode { + size: (800, 600).into(), + refresh: 60_000, + }; + + let output = Output::new( + OUTPUT_NAME.to_string(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "Smithay".into(), + model: "WLCS".into(), + serial_number: "Unknown".into(), + }, + ); + let _global = output.create_global::>(&state.display_handle); + output.change_current_state(Some(mode), None, None, Some((0, 0).into())); + output.set_preferred(mode); + state.space.map_output(&output, (0, 0)); + + let mut damage_tracker = OutputDamageTracker::from_output(&output); + let mut pointer_element = PointerElement::default(); + + while state.running.load(Ordering::SeqCst) { + // pretend to draw something + { + let scale = Scale::from(output.current_scale().fractional_scale()); + let mut elements: Vec> = Vec::new(); + + // draw the cursor as relevant + // reset the cursor if the surface is no longer alive + let mut reset = false; + if let CursorImageStatus::Surface(ref surface) = state.cursor_status { + reset = !surface.alive(); + } + if reset { + state.cursor_status = CursorImageStatus::default_named(); + } + + let cursor_hotspot = if let CursorImageStatus::Surface(ref surface) = state.cursor_status { + compositor::with_states(surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot + }) + } else { + (0, 0).into() + }; + let cursor_pos = state.pointer.current_location(); + + pointer_element.set_status(state.cursor_status.clone()); + elements.extend( + pointer_element.render_elements( + &mut renderer, + (cursor_pos - cursor_hotspot.to_f64()) + .to_physical(scale) + .to_i32_round(), + scale, + 1.0, + ), + ); + + // draw the dnd icon if any + if let Some(icon) = state.dnd_icon.as_ref() { + if icon.surface.alive() { + let dnd_icon_pos = (cursor_pos + icon.offset.to_f64()) + .to_physical(scale) + .to_i32_round(); + elements.extend(AsRenderElements::::render_elements( + &smithay::desktop::space::SurfaceTree::from_surface(&icon.surface), + &mut renderer, + dnd_icon_pos, + scale, + 1.0, + )); + } + } + + let _ = render_output( + &output, + &state.space, + elements, + &mut renderer, + &mut framebuffer, + &mut damage_tracker, + 0, + false, + ); + } + + // Send frame events so that client start drawing their next frame + state.space.elements().for_each(|window| { + window.send_frame(&output, state.clock.now(), Some(Duration::ZERO), |_, _| { + Some(output.clone()) + }) + }); + + if event_loop + .dispatch(Some(Duration::from_millis(16)), &mut state) + .is_err() + { + state.running.store(false, Ordering::SeqCst); + } else { + state.space.refresh(); + state.popups.cleanup(); + state.display_handle.flush_clients().unwrap(); + } + } +} + +fn handle_event(event: WlcsEvent, state: &mut AnvilState) { + match event { + WlcsEvent::Exit => state.running.store(false, Ordering::SeqCst), + WlcsEvent::NewClient { stream, client_id } => { + let client = state + .display_handle + .insert_client(stream, Arc::new(ClientState::default())) + .expect("Failed to insert client"); + state.backend_data.clients.insert(client_id, client); + } + WlcsEvent::PositionWindow { + client_id, + surface_id, + location, + } => { + // find the surface + let client = state.backend_data.clients.get(&client_id); + let toplevel = state.space.elements().find(|w| { + if let Some(surface) = w.wl_surface() { + state.display_handle.get_client(surface.id()).ok().as_ref() == client + && surface.id().protocol_id() == surface_id + } else { + false + } + }); + if let Some(toplevel) = toplevel.cloned() { + // set its location + state.space.map_element(toplevel, location, false); + } + } + // pointer inputs + WlcsEvent::NewPointer { .. } => {} + WlcsEvent::PointerMoveAbsolute { location, .. } => { + let serial = SCOUNTER.next_serial(); + let under = state.surface_under(location); + let time = InputTime::now(); + let ptr = state.pointer.clone(); + ptr.motion( + state, + under, + &MotionEvent { + location, + serial, + time, + }, + ); + ptr.frame(state); + } + WlcsEvent::PointerMoveRelative { delta, .. } => { + let pointer_location = state.pointer.current_location() + delta; + let serial = SCOUNTER.next_serial(); + let under = state.surface_under(pointer_location); + let time = InputTime::now(); + let ptr = state.pointer.clone(); + ptr.motion( + state, + under.clone(), + &MotionEvent { + location: pointer_location, + serial, + time, + }, + ); + ptr.relative_motion( + state, + under, + &RelativeMotionEvent { + delta, + delta_unaccel: delta, + time, + }, + ); + ptr.frame(state); + } + WlcsEvent::PointerButtonDown { button_id, .. } => { + let serial = SCOUNTER.next_serial(); + let ptr = state.seat.get_pointer().unwrap(); + if !ptr.is_grabbed() { + let under = state + .space + .element_under(ptr.current_location()) + .map(|(w, _)| w.clone()); + if let Some(window) = under.as_ref() { + state.space.raise_element(window, true); + } + state + .seat + .get_keyboard() + .unwrap() + .set_focus(state, under.map(Into::into), serial); + } + let time = InputTime::now(); + ptr.button( + state, + &ButtonEvent { + button: button_id as u32, + state: ButtonState::Pressed, + serial, + time, + }, + ); + ptr.frame(state); + } + WlcsEvent::PointerButtonUp { button_id, .. } => { + let serial = SCOUNTER.next_serial(); + let time = InputTime::now(); + let ptr = state.seat.get_pointer().unwrap(); + ptr.button( + state, + &ButtonEvent { + button: button_id as u32, + state: ButtonState::Released, + serial, + time, + }, + ); + ptr.frame(state); + } + WlcsEvent::PointerRemoved { .. } => {} + // touch inputs + WlcsEvent::NewTouch { .. } => {} + WlcsEvent::TouchDown { .. } => {} + WlcsEvent::TouchMove { .. } => {} + WlcsEvent::TouchUp { .. } => {} + WlcsEvent::TouchRemoved { .. } => {} + } +}