diff --git a/Cargo.toml b/Cargo.toml index e24c120826ad..47678c0c179a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ wayland-server = { version = "0.31.9", optional = true } wayland-sys = { version = "0.31.6", optional = true } wayland-backend = { version = "0.3.10", optional = true } winit = { version = "0.30.0", default-features = false, features = ["wayland", "wayland-dlopen", "x11", "rwh_06"], optional = true } +wl-input-method = { version = "0.0.2", git = "https://gitlab.freedesktop.org/dcz/wl-input-method.git", branch = "popup" } x11rb = { version = "0.13.0", optional = true, features = ["res"]} xkbcommon = { version = "0.8.0", features = ["wayland"]} encoding_rs = { version = "0.8.33", optional = true } diff --git a/anvil/src/shell/mod.rs b/anvil/src/shell/mod.rs index 541383ff2376..1e6808c0aed9 100644 --- a/anvil/src/shell/mod.rs +++ b/anvil/src/shell/mod.rs @@ -342,21 +342,27 @@ fn ensure_initial_configure(surface: &WlSurface, space: &Space, p return; } - if let Some(popup) = popups.find_popup(surface) { - let popup = match popup { - PopupKind::Xdg(ref popup) => popup, + if let Some(mut popup) = popups.find_popup(surface) { + match popup { + PopupKind::Xdg(ref popup) => { + 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"); + }; + } // Doesn't require configure PopupKind::InputMethod(ref _input_popup) => { return; } + PopupKind::InputMethodV3(ref mut popup) => { + if !popup.is_initial_configure_sent() { + popup.send_pending_configure(); + popup.input_method().done(); + }; + } }; - 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; }; diff --git a/anvil/src/state.rs b/anvil/src/state.rs index 278e3d62d719..d0718665e51a 100644 --- a/anvil/src/state.rs +++ b/anvil/src/state.rs @@ -15,11 +15,12 @@ use smithay::{ }, }, delegate_compositor, delegate_data_control, delegate_data_device, delegate_fractional_scale, - delegate_input_method_manager, delegate_keyboard_shortcuts_inhibit, delegate_layer_shell, - delegate_output, delegate_pointer_constraints, delegate_pointer_gestures, delegate_presentation, - delegate_primary_selection, delegate_relative_pointer, delegate_seat, delegate_security_context, - delegate_shm, delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter, - delegate_virtual_keyboard_manager, delegate_xdg_activation, delegate_xdg_decoration, delegate_xdg_shell, + delegate_input_method_manager, delegate_input_method_manager_v3, delegate_keyboard_shortcuts_inhibit, + delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_pointer_gestures, + delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat, + delegate_security_context, delegate_shm, delegate_tablet_manager, delegate_text_input_manager, + delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation, delegate_xdg_decoration, + delegate_xdg_shell, desktop::{ space::SpaceElement, utils::{ @@ -53,6 +54,10 @@ use smithay::{ fifo::{FifoBarrierCachedState, FifoManagerState}, fractional_scale::{with_fractional_scale, FractionalScaleHandler, FractionalScaleManagerState}, input_method::{InputMethodHandler, InputMethodManagerState, PopupSurface}, + input_method_v3::{ + self, InputMethodHandler as InputMethodHandlerV3, + InputMethodManagerState as InputMethodManagerStateV3, PopupSurface as PopupSurfaceV3, + }, keyboard_shortcuts_inhibit::{ KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, }, @@ -339,6 +344,64 @@ impl InputMethodHandler for AnvilState { delegate_input_method_manager!(@ AnvilState); +impl InputMethodHandlerV3 for AnvilState { + fn new_popup(&mut self, surface: PopupSurfaceV3) { + if let Err(err) = self.popups.track_popup(PopupKind::from(surface)) { + warn!("Failed to track popup: {}", err); + } + } + + fn popup_repositioned(&mut self, _: PopupSurfaceV3) {} + + fn dismiss_popup(&mut self, surface: PopupSurfaceV3) { + let parent = surface.get_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() + } + + fn popup_geometry( + &self, + parent: &WlSurface, + cursor: &Rectangle, + positioner: &input_method_v3::PositionerState, + ) -> Rectangle { + let Some(window) = self.window_for_surface(&parent) else { + panic!("Input method popup without parent window"); + }; + + let mut outputs_for_window = self.space.outputs_for_element(&window); + if outputs_for_window.is_empty() { + return Default::default(); + } + + // 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 -= window_geo.loc; + + positioner.get_geometry_from_anchor(*cursor, target) + } +} + +delegate_input_method_manager_v3!(@ AnvilState); + impl KeyboardShortcutsInhibitHandler for AnvilState { fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState { &mut self.keyboard_shortcuts_inhibit_state @@ -640,6 +703,7 @@ impl AnvilState { let commit_timing_manager_state = CommitTimingManagerState::new::(&dh); TextInputManagerState::new::(&dh); InputMethodManagerState::new::(&dh, |_client| true); + InputMethodManagerStateV3::new::(&dh, |_client| true); VirtualKeyboardManagerState::new::(&dh, |_client| true); // Expose global only if backend supports relative motion events if BackendData::HAS_RELATIVE_MOTION { diff --git a/smallvil/src/handlers/xdg_shell.rs b/smallvil/src/handlers/xdg_shell.rs index 64fd622305a0..5746e30cd9b0 100644 --- a/smallvil/src/handlers/xdg_shell.rs +++ b/smallvil/src/handlers/xdg_shell.rs @@ -174,7 +174,7 @@ pub fn handle_commit(popups: &mut PopupManager, space: &Space, surface: // Handle popup commits. popups.commit(surface); - if let Some(popup) = popups.find_popup(surface) { + if let Some(mut popup) = popups.find_popup(surface) { match popup { PopupKind::Xdg(ref xdg) => { if !xdg.is_initial_configure_sent() { @@ -184,6 +184,12 @@ pub fn handle_commit(popups: &mut PopupManager, space: &Space, surface: } } PopupKind::InputMethod(ref _input_method) => {} + PopupKind::InputMethodV3(ref mut popup) => { + if !popup.is_initial_configure_sent() { + popup.send_pending_configure(); + popup.input_method().done(); + }; + } } } } diff --git a/src/desktop/wayland/popup/manager.rs b/src/desktop/wayland/popup/manager.rs index 8a2a0250ab6e..2cbb3d5a466f 100644 --- a/src/desktop/wayland/popup/manager.rs +++ b/src/desktop/wayland/popup/manager.rs @@ -94,6 +94,9 @@ impl PopupManager { PopupKind::InputMethod(ref _input_method) => { return Err(PopupGrabError::InvalidGrab); } + PopupKind::InputMethodV3(ref _input_method) => { + return Err(PopupGrabError::InvalidGrab); + } } // The primary store for the grab is the seat, additional we store it diff --git a/src/desktop/wayland/popup/mod.rs b/src/desktop/wayland/popup/mod.rs index 6ea9a3957a39..7be7b6e3a8d2 100644 --- a/src/desktop/wayland/popup/mod.rs +++ b/src/desktop/wayland/popup/mod.rs @@ -9,7 +9,7 @@ use crate::{ utils::{IsAlive, Logical, Point, Rectangle}, wayland::{ compositor::with_states, - input_method, + input_method, input_method_v3, shell::xdg::{self, SurfaceCachedState, XdgPopupSurfaceData}, }, }; @@ -21,6 +21,8 @@ pub enum PopupKind { Xdg(xdg::PopupSurface), /// input-method [`PopupSurface`](input_method::PopupSurface) InputMethod(input_method::PopupSurface), + /// input-method-v3 [`PopupSurface`](input_method_v3::PopupSurface) + InputMethodV3(input_method_v3::PopupSurface), } impl IsAlive for PopupKind { @@ -29,6 +31,7 @@ impl IsAlive for PopupKind { match self { PopupKind::Xdg(ref p) => p.alive(), PopupKind::InputMethod(ref p) => p.alive(), + PopupKind::InputMethodV3(ref p) => p.alive(), } } } @@ -47,6 +50,7 @@ impl PopupKind { match *self { PopupKind::Xdg(ref t) => t.wl_surface(), PopupKind::InputMethod(ref t) => t.wl_surface(), + PopupKind::InputMethodV3(ref t) => t.wl_surface(), } } @@ -54,6 +58,7 @@ impl PopupKind { match *self { PopupKind::Xdg(ref t) => t.get_parent_surface(), PopupKind::InputMethod(ref t) => t.get_parent().map(|parent| parent.surface.clone()), + PopupKind::InputMethodV3(ref t) => Some(t.get_parent().surface.clone()), } } @@ -70,6 +75,7 @@ impl PopupKind { .unwrap_or_default() }), PopupKind::InputMethod(ref t) => t.get_parent().map(|parent| parent.location).unwrap_or_default(), + PopupKind::InputMethodV3(ref t) => t.get_parent().location, } } @@ -77,6 +83,7 @@ impl PopupKind { match *self { PopupKind::Xdg(ref t) => t.send_popup_done(), PopupKind::InputMethod(_) => {} //Nothing to do the IME takes care of this itself + PopupKind::InputMethodV3(_) => {} // The IME receives a deactivate event which already indicates that the popup is destroyed. } } @@ -98,6 +105,8 @@ impl PopupKind { .loc } PopupKind::InputMethod(ref t) => t.location(), + // Use (0,0) as the location for unmapped surfaces. Can't think of anything better. The only use in higher layers is to iterate over all popups, so maybe this is enough. + PopupKind::InputMethodV3(ref t) => t.location(), } } } @@ -115,3 +124,10 @@ impl From for PopupKind { PopupKind::InputMethod(p) } } + +impl From for PopupKind { + #[inline] + fn from(p: input_method_v3::PopupSurface) -> PopupKind { + PopupKind::InputMethodV3(p) + } +} diff --git a/src/reexports.rs b/src/reexports.rs index ebe577d32ca5..be8b40cadbc1 100644 --- a/src/reexports.rs +++ b/src/reexports.rs @@ -26,5 +26,7 @@ pub use wayland_protocols_wlr; pub use wayland_server; #[cfg(feature = "backend_winit")] pub use winit; +#[cfg(feature = "wayland_frontend")] +pub use wl_input_method; #[cfg(feature = "x11rb_event_source")] pub use x11rb; diff --git a/src/wayland/input_method_v3/configure_tracker.rs b/src/wayland/input_method_v3/configure_tracker.rs new file mode 100644 index 000000000000..cb93fd74fd33 --- /dev/null +++ b/src/wayland/input_method_v3/configure_tracker.rs @@ -0,0 +1,54 @@ +/*! Tracks serial number assignment */ +use crate::utils::{Serial, SERIAL_COUNTER}; + +/// Tracks states updated via configure sequences involving serials +#[derive(Debug)] +pub struct ConfigureTracker { + /// An ordered sequence of 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. + /// The newest configure has the highest index. + pending_configures: Vec<(State, Serial)>, + + /// Holds the last server_pending state that has been acknowledged by the + /// client. This state should be cloned to the current during a commit. + last_acked: Option, +} + +impl Default for ConfigureTracker { + fn default() -> Self { + Self { + pending_configures: Vec::new(), + last_acked: None, + } + } +} + +impl ConfigureTracker { + /// Assigns a new pending state and returns its serial + pub fn assign_serial(&mut self, state: State) -> Serial { + let serial = SERIAL_COUNTER.next_serial(); + self.pending_configures.push((state, serial)); + serial + } + + /// Marks that the user accepted the serial and returns the state associated with it. + /// If the serial is not currently pending, returns None. + pub fn ack_serial(&mut self, serial: Serial) -> Option { + let (state, _) = self + .pending_configures + .iter() + .find(|(_, c_serial)| *c_serial == serial) + .cloned()?; + + self.pending_configures.retain(|(_, c_serial)| *c_serial > serial); + self.last_acked = Some(state.clone()); + Some(state) + } + + /// Last state sent to the client but not acknowledged + pub fn last_pending_state(&self) -> Option<&State> { + self.pending_configures.last().map(|(s, _)| s) + } +} diff --git a/src/wayland/input_method_v3/input_method_handle.rs b/src/wayland/input_method_v3/input_method_handle.rs new file mode 100644 index 000000000000..478bc721cab4 --- /dev/null +++ b/src/wayland/input_method_v3/input_method_handle.rs @@ -0,0 +1,318 @@ +use std::{ + fmt, + sync::{Arc, Mutex}, +}; + +use wayland_server::{backend::ClientId, protocol::wl_surface::WlSurface}; +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, Resource}; +use wl_input_method::input_method::xx::server::{ + xx_input_method_v1::{self, XxInputMethodV1}, + xx_input_popup_surface_v2::XxInputPopupSurfaceV2, +}; + +use crate::{ + input::SeatHandler, + utils::{Logical, Rectangle}, + wayland::{compositor, seat::WaylandFocus, text_input::TextInputHandle}, +}; + +use super::{ + input_method_popup_surface::{ImPopupLocation, PopupParent, PopupSurface}, + positioner::{PositionerState, PositionerUserData}, + InputMethodHandler, InputMethodManagerState, InputMethodPopupSurfaceUserData, INPUT_POPUP_SURFACE_ROLE, +}; + +/// Slot for an optional input method +#[derive(Default, Debug)] +pub(crate) struct MaybeInstance { + /// Optional input method + pub instance: Option, +} + +/// Contains input method state +#[derive(Debug)] +pub(crate) struct InputMethod { + pub object: XxInputMethodV1, + pub serial: u32, + pub active: bool, + pub popup_handles: Vec, + /// Relative to surface on which input method is enabled + pub cursor_rectangle: Rectangle, +} + +impl InputMethod { + /// Send the done incrementing the serial. + pub(crate) fn done(&mut self) { + self.object.done(); + self.serial += 1; + } +} + +/// Handle to a possible input method instance. +#[derive(Default, Debug, Clone)] +pub struct InputMethodHandle { + // TODO: why does this need to be shared? + pub(crate) inner: Arc>, +} + +impl InputMethodHandle { + /// Assigns a new instance + pub(super) fn add_instance(&self, instance: &XxInputMethodV1) { + let mut inner = self.inner.lock().unwrap(); + if let Some(instance) = inner.instance.as_mut() { + instance.serial = 0; + instance.object.unavailable(); + } else { + inner.instance = Some(InputMethod { + object: instance.clone(), + serial: 0, + active: false, + popup_handles: vec![], + cursor_rectangle: Rectangle::default(), + }); + } + } + + /// Whether there's an active instance of input-method. + pub(crate) fn has_instance(&self) -> bool { + self.inner.lock().unwrap().instance.is_some() + } + + /// Callback function to access the input method object + pub(crate) fn with_instance(&self, f: F) + where + F: FnOnce(&mut InputMethod), + { + let mut inner = self.inner.lock().unwrap(); + if let Some(instance) = inner.instance.as_mut() { + f(instance); + } + } + + pub(crate) fn set_cursor_rectangle( + &self, + state: &mut D, + cursor: Rectangle, + ) { + let mut inner = self.inner.lock().unwrap(); + if let Some(ref mut inner) = &mut inner.instance { + let data = inner.object.data::>().unwrap(); + inner.cursor_rectangle = cursor; + for popup_surface in &mut inner.popup_handles { + let popup_geometry = (data.popup_geometry)( + state, + &popup_surface.get_parent().surface, + &cursor, + &popup_surface.positioner(), + ); + + let anchor = cursor; // FIXME: choose the anchor which the positioner wants + + popup_surface.set_position(ImPopupLocation { + anchor, + geometry: popup_geometry, + }); + + // TODO: send now or on .done? + (data.popup_repositioned)(state, popup_surface.clone()); + } + } + } + + pub(crate) fn done(&self) { + let mut inner = self.inner.lock().unwrap(); + + if let Some(ref mut inner) = &mut inner.instance { + for popup_surface in &mut inner.popup_handles { + popup_surface.send_pending_configure(); + } + inner.done(); + } + } + + /// Activate input method on the given surface. + pub(crate) fn activate_input_method(&self, _: &mut D, _surface: &WlSurface) { + self.with_instance(|im| { + im.object.activate(); + im.active = true; + }); + } + + /// Deactivate the active input method. + /// + /// This includes a complete sequence including .done. + pub(crate) fn deactivate_input_method(&self, state: &mut D) { + self.with_instance(|im| { + im.object.deactivate(); + im.done(); + im.active = false; + let data = im.object.data::>().unwrap(); + for popup in im.popup_handles.drain(..) { + (data.dismiss_popup)(state, popup.clone()); + } + }); + } +} + +/// User data of XxInputMethodV1 object +#[derive(Clone)] +pub struct InputMethodUserData { + pub(super) handle: InputMethodHandle, + pub(crate) text_input_handle: TextInputHandle, + /// This is just a copy from Input MethodHandler. It's here in order to break the requirement for D: InputMethodHandler on functions that call dismiss_popup. That means other modules don't have to explicitly put D: InputMethodHandler when they call something that ends up calling this. + /// (Not sure what the purpose of that is, but it seems consistent...) + pub(crate) popup_geometry: + fn(&D, &WlSurface, &Rectangle, &PositionerState) -> Rectangle, + 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) + .finish() + } +} + +impl Dispatch, D> for InputMethodManagerState +where + D: Dispatch>, + D: Dispatch, + D: SeatHandler, + D: InputMethodHandler, + ::KeyboardFocus: WaylandFocus, + D: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + im: &XxInputMethodV1, + request: xx_input_method_v1::Request, + data: &InputMethodUserData, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + use xx_input_method_v1::Request; + match request { + Request::CommitString { text } => { + data.text_input_handle.with_active_text_input(|ti, _surface| { + ti.commit_string(Some(text.clone())); + }); + } + Request::SetPreeditString { + text, + cursor_begin, + cursor_end, + } => { + data.text_input_handle.with_active_text_input(|ti, _surface| { + ti.preedit_string(Some(text.clone()), cursor_begin, cursor_end); + }); + } + Request::DeleteSurroundingText { + before_length, + after_length, + } => { + data.text_input_handle.with_active_text_input(|ti, _surface| { + ti.delete_surrounding_text(before_length, after_length); + }); + } + Request::Commit { serial } => { + let current_serial = data + .handle + .inner + .lock() + .unwrap() + .instance + .as_ref() + .map(|i| i.serial) + .unwrap_or(0); + + data.text_input_handle.done(serial != current_serial); + } + Request::GetInputPopupSurface { + id, + surface, + positioner, + } => { + let mut input_method = data.handle.inner.lock().unwrap(); + if let Some(instance) = &mut input_method.instance { + if instance.active { + if compositor::give_role(&surface, INPUT_POPUP_SURFACE_ROLE).is_err() + && compositor::get_role(&surface) != Some(INPUT_POPUP_SURFACE_ROLE) + { + im.post_error( + xx_input_method_v1::Error::SurfaceHasRole, + "Surface already has a role.", + ); + return; + } + + let parent_surface = match data.text_input_handle.focus().clone() { + Some(parent) => parent, + None => { + im.post_error( + xx_input_method_v1::Error::Inactive, + "Popup may only be created on an active input method (no surface in text input focus).", + ); + return; + } + }; + + let location = state.parent_geometry(&parent_surface); + let parent = PopupParent { + surface: parent_surface, + location, + }; + + let positioner_data = *positioner + .data::() + .unwrap() + .inner + .lock() + .unwrap(); + + let geometry = state.popup_geometry( + &parent.surface, + &instance.cursor_rectangle, + &positioner_data, + ); + + // TODO: feed the popup with the anchor chosen by the positioner + let popup = PopupSurface::new( + |data| data_init.init(id, data), + im.clone(), + parent, + surface, + instance.cursor_rectangle, + geometry, + positioner_data, + ); + instance.popup_handles.push(popup.clone()); + state.new_popup(popup); + } else { + im.post_error( + xx_input_method_v1::Error::Inactive, + "Popup may only be created on an active input method.", + ); + } + } + } + Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } + + fn destroyed( + _state: &mut D, + _client: ClientId, + _input_method: &XxInputMethodV1, + data: &InputMethodUserData, + ) { + data.handle.inner.lock().unwrap().instance = None; + data.text_input_handle.leave(); + } +} diff --git a/src/wayland/input_method_v3/input_method_popup_surface.rs b/src/wayland/input_method_v3/input_method_popup_surface.rs new file mode 100644 index 000000000000..6942dd7f7b22 --- /dev/null +++ b/src/wayland/input_method_v3/input_method_popup_surface.rs @@ -0,0 +1,349 @@ +use std::cmp::PartialEq; +use std::sync::{Arc, Mutex}; + +use wayland_server::{backend::ClientId, protocol::wl_surface::WlSurface, Dispatch, Resource}; +use wl_input_method::input_method::xx::server::xx_input_method_v1::XxInputMethodV1; +use wl_input_method::input_method::xx::server::xx_input_popup_surface_v2::{self, XxInputPopupSurfaceV2}; + +use crate::input::SeatHandler; +use crate::utils::{ + alive_tracker::{AliveTracker, IsAlive}, + Logical, Point, Rectangle, Serial, +}; + +use super::{ + configure_tracker::ConfigureTracker, + positioner::{PositionerState, PositionerUserData}, + InputMethodHandler, InputMethodManagerState, InputMethodUserData, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImPopupLocation { + /// Area for the positioner, relative to parent + pub anchor: Rectangle, + /// Geometry of the popup surface relative to parent. + pub geometry: 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: XxInputPopupSurfaceV2, + /// Surface containing the popup + surface: WlSurface, + /// Surface containing the text input. This surface doesn't change within the lifetime of the popup. + parent: PopupParent, + /// Tracks configures and serials + configure_tracker: Arc>>, + /// The compositor-assigned state acknowledged by client. + state: Arc>, + /// The compositor-assigned state, not sent to client yet + state_pending: Option, +} + +impl PopupSurface { + /// Creates a new popup surface. + /// Anchor is the anchor position relative to parent. Geometry is the popup position relative to parent. + pub(crate) fn new( + init: impl FnOnce(InputMethodPopupSurfaceUserData) -> XxInputPopupSurfaceV2, + input_method: XxInputMethodV1, + parent: PopupParent, + surface: WlSurface, + anchor: Rectangle, + geometry: Rectangle, + positioner_data: PositionerState, + ) -> Self { + let configure_tracker = Arc::new(Mutex::new(Default::default())); + let state = Arc::new(Mutex::new(PopupSurfaceState::new_uninit())); + + let instance = InputMethodPopupSurfaceUserData::new( + input_method.clone(), + surface.clone(), + configure_tracker.clone(), + state.clone(), + Mutex::new(positioner_data), + ); + let surface_role = init(instance); + Self { + surface_role, + configure_tracker, + state, + state_pending: Some(PopupSurfaceState { + position: ImPopupLocation { anchor, geometry }, + configured: false, + repositioned: None, + }), + surface, + parent, + } + } + + /// Returns a copy of the positioner. That can be used to calculate a new position. + pub fn positioner(&self) -> PositionerState { + let role_data: &InputMethodPopupSurfaceUserData = self.surface_role.data().unwrap(); + *role_data.positioner.lock().unwrap() + } + + /// 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) -> &PopupParent { + &self.parent + } + + /// Access the input method using this popup + pub fn input_method(&self) -> &XxInputMethodV1 { + let role_data: &InputMethodPopupSurfaceUserData = self.surface_role.data().unwrap(); + &role_data.input_method + } + + /// Used to access the location of an input popup surface relative to the parent + pub fn location(&self) -> Point { + self.state.lock().unwrap().position.geometry.loc + } + + /// `true` if the surface sent a + /// configure sequence since creating the popup object. + pub fn is_initial_configure_sent(&self) -> bool { + self.state.lock().unwrap().configured + } + + /// Set position information that should take effect when mapping. + /// Updates pending state. + pub fn set_position(&mut self, position: ImPopupLocation) { + let pending = &mut self.state_pending; + if pending.is_none() { + *pending = Some(self.state.lock().unwrap().clone()); + } + pending.as_mut().unwrap().position = position; + } + + /// Adds the repositioned token to pending state. + pub fn set_repositioned(&mut self, token: u32) { + let pending = &mut self.state_pending; + if pending.is_none() { + *pending = Some(self.state.lock().unwrap().clone()); + } + pending.as_mut().unwrap().repositioned = Some(token); + } + + /// 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. + /// Call this from input_method.done + pub fn send_pending_configure(&mut self) { + let new_state = { + let state = &mut self.state_pending; + if let Some(state) = state.as_mut() { + state.configured = true; + state.clone() + } else { + // there's nothing to update + return; + } + }; + + // TODO: there's too much locking here but too early to optimize... + let sent_state = { + let tracker = self.configure_tracker.lock().unwrap(); + tracker.last_pending_state().cloned() + } + .unwrap_or_else(|| self.state.lock().unwrap().clone()); + + // start_configure should be sent on any server-side change. Other events should follow with more granularity. + if new_state != sent_state { + let mut tracker = self.configure_tracker.lock().unwrap(); + let serial = tracker.assign_serial(new_state.clone()); + + let ImPopupLocation { anchor, geometry } = new_state.position.clone(); + let relative_to_popup = anchor.loc - geometry.loc; + self.surface_role.start_configure( + geometry.size.w as u32, + geometry.size.h as u32, + relative_to_popup.x, + relative_to_popup.y, + anchor.size.w as u32, + anchor.size.h as u32, + serial.into(), + ); + + if let (Some(new), sent) = (new_state.repositioned, sent_state.repositioned) { + if Some(new) != sent { + self.surface_role.repositioned(new); + } + } + } + } +} + +impl PartialEq for PopupSurface { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.surface_role == other.surface_role + } +} + +/// Compositor-defined state +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PopupSurfaceState { + /// Positioning information + position: ImPopupLocation, + /// Token to send to the client, if any + /// + /// The protocol doesn't mandate the lifecycle for this token, so this holds the last state and update events are sent on detected changes. + repositioned: Option, + /// Already issued a configure sequence + configured: bool, +} + +impl PopupSurfaceState { + /// Creates an initial state with uninitialized values. The values are never read in normal protocol usage. + fn new_uninit() -> Self { + PopupSurfaceState { + position: ImPopupLocation { + anchor: Default::default(), + geometry: Default::default(), + }, + configured: false, + repositioned: None, + } + } +} + +/// Parent surface and location for the IME popup. +#[derive(Debug, Clone)] +pub struct PopupParent { + /// The surface over which the IME popup is shown. + pub surface: WlSurface, + /// The location of the parent surface relative to TODO. + pub location: Rectangle, +} + +/// Data accessible from XxInputPopupSurfaceV2 object +#[derive(Debug)] +pub struct InputMethodPopupSurfaceUserData { + /// Input method controlling this popup + input_method: XxInputMethodV1, + pub(super) alive_tracker: AliveTracker, + pub(super) surface: WlSurface, + pub(super) configure_tracker: Arc>>, + /// State acknowledged by client. + pub(super) state: Arc>, + // State supplied by client. + /// Computes the position of the popup according to provided rules + pub(super) positioner: Mutex, +} + +impl InputMethodPopupSurfaceUserData { + fn new( + input_method: XxInputMethodV1, + surface: WlSurface, + configure_tracker: Arc>>, + popup_state: Arc>, + positioner: Mutex, + ) -> Self { + Self { + input_method, + alive_tracker: AliveTracker::default(), + surface, + configure_tracker, + state: popup_state, + positioner, + } + } +} + +impl Dispatch for InputMethodManagerState +where + D: InputMethodHandler + SeatHandler, +{ + fn request( + state: &mut D, + _client: &wayland_server::Client, + popup: &XxInputPopupSurfaceV2, + request: xx_input_popup_surface_v2::Request, + data: &InputMethodPopupSurfaceUserData, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + use xx_input_popup_surface_v2::Request; + match request { + Request::AckConfigure { serial } => { + let surface = &data.surface; + + let serial = Serial::from(serial); + let client_state = data.configure_tracker.lock().unwrap().ack_serial(serial); + + let client_state = match client_state { + Some(state) => state, + None => { + popup.post_error( + xx_input_popup_surface_v2::Error::InvalidSerial, + format!("Serial {} is not awaiting ack", ::from(serial)), + ); + return; + } + }; + *data.state.lock().unwrap() = client_state.clone(); + state.popup_ack_configure(surface, serial, client_state); + } + Request::Reposition { positioner, token } => { + let im: &InputMethodUserData = data.input_method.data().unwrap(); + let popup = { + let positioner: &PositionerUserData = positioner.data().unwrap(); + let positioner = *positioner.inner.lock().unwrap(); + let mut inner = im.handle.inner.lock().unwrap(); + // This request comes to an input_method object, so an empty instance is a bug. + let instance = inner.instance.as_mut().unwrap(); + let cursor = instance.cursor_rectangle; + let popup = instance + .popup_handles + .iter_mut() + .find(|h| h.surface_role == *popup) + .expect("This popup not tracked by its input method"); + let parent_surface = popup.get_parent().surface.clone(); + // This locks input method instance. The geometry callback is going to be limited here. The lock can be released and reacquired for .set_position, but it's less readable, so better do it when the need comes. + let popup_geometry = state.popup_geometry(&parent_surface, &cursor, &positioner); + *data.positioner.lock().unwrap() = positioner; + + popup.set_repositioned(token); + popup.set_position(ImPopupLocation { + anchor: cursor, + geometry: popup_geometry, + }); + popup.clone() + }; + + state.popup_repositioned(popup); + + im.handle.done(); + } + Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } + + fn destroyed( + _state: &mut D, + _client: ClientId, + _object: &XxInputPopupSurfaceV2, + data: &InputMethodPopupSurfaceUserData, + ) { + data.alive_tracker.destroy_notify(); + } +} diff --git a/src/wayland/input_method_v3/mod.rs b/src/wayland/input_method_v3/mod.rs new file mode 100644 index 000000000000..cca2ea1a45c9 --- /dev/null +++ b/src/wayland/input_method_v3/mod.rs @@ -0,0 +1,296 @@ +//! 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::{ +//! delegate_seat, delegate_input_method_manager_v3, delegate_text_input_manager, +//! # delegate_compositor, +//! }; +//! use smithay::input::{Seat, SeatState, SeatHandler, pointer::CursorImageStatus}; +//! # use smithay::wayland::compositor::{CompositorHandler, CompositorState, CompositorClientState}; +//! use smithay::wayland::input_method_v3::{InputMethodManagerState, InputMethodHandler, PopupSurface, PositionerState}; +//! use smithay::wayland::text_input::TextInputManagerState; +//! use smithay::reexports::wayland_server::{Display, protocol::wl_surface::WlSurface}; +//! # use smithay::reexports::wayland_server::Client; +//! use smithay::utils::{Rectangle, Logical}; +//! +//! # struct State { seat_state: SeatState }; +//! +//! delegate_seat!(State); +//! # delegate_compositor!(State); +//! +//! 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 popup_geometry(&self, _: &WlSurface, _: &Rectangle, _: &PositionerState) -> smithay::utils::Rectangle { +//! Rectangle::default() +//! } +//! fn parent_geometry(&self, parent: &WlSurface) -> Rectangle { +//! Rectangle::default() +//! } +//! } +//! +//! // Delegate input method handling for State to InputMethodManagerState. +//! delegate_input_method_manager_v3!(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 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) {} +//! # } +//! +//! // 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 +//! delegate_text_input_manager!(State); +//! TextInputManagerState::new::(&display_handle); +//! +//! ``` + +use wayland_server::{ + backend::GlobalId, protocol::wl_surface::WlSurface, Client, DataInit, Dispatch, DisplayHandle, + GlobalDispatch, New, +}; + +use wl_input_method::input_method::xx::{ + server::xx_input_popup_positioner_v1::XxInputPopupPositionerV1, + server::{ + xx_input_method_manager_v2::{self, XxInputMethodManagerV2}, + xx_input_method_v1::XxInputMethodV1, + }, +}; + +use crate::{ + input::{Seat, SeatHandler}, + utils::{Logical, Rectangle, Serial}, +}; + +pub use input_method_handle::{InputMethodHandle, InputMethodUserData}; + +use super::text_input::TextInputHandle; + +const MANAGER_VERSION: u32 = 2; + +/// The role of the input method popup. +pub const INPUT_POPUP_SURFACE_ROLE: &str = "zwp_input_popup_surface_v3"; + +mod configure_tracker; +mod input_method_handle; +mod input_method_popup_surface; +mod positioner; + +pub use input_method_popup_surface::{ + InputMethodPopupSurfaceUserData, PopupParent, PopupSurface, PopupSurfaceState, +}; +pub use positioner::{PositionerState, PositionerUserData}; + +/// 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. + /// + /// This gets called after calculating and applying the new geometry but before input_method.done is sent. + fn popup_repositioned(&mut self, surface: PopupSurface); + + /// Returns the position of the popup, given the cursor rectangle expressed in position relative to surface. + /// This may be called while locks on some input-method objects are held. + fn popup_geometry( + &self, + parent: &WlSurface, + cursor: &Rectangle, + positioner: &PositionerState, + ) -> Rectangle; + + /// Sets the parent location so the popup surface can be placed correctly + fn parent_geometry(&self, parent: &WlSurface) -> Rectangle; + + /// Copied from wl_layer_surface. + /// What is this for? What arguments make sense? + fn popup_ack_configure( + &mut self, + _surface: &WlSurface, + _serial: Serial, + _client_state: PopupSurfaceState, + ) { + // the compositor doesn't need to implement this if it doesn't have a use for it + } +} + +/// Extends [Seat] with input method functionality +pub trait InputMethodSeat { + /// Get an input method associated with this seat + fn input_method_v3(&self) -> &InputMethodHandle; +} + +impl InputMethodSeat for Seat { + fn input_method_v3(&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 manager global + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch for InputMethodManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch>, + D: SeatHandler, + D: 'static, +{ + fn bind( + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + _: &InputMethodManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &InputMethodManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for InputMethodManagerState +where + D: Dispatch, + D: Dispatch>, + D: Dispatch, + D: SeatHandler + InputMethodHandler, + D: 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _: &XxInputMethodManagerV2, + request: xx_input_method_manager_v2::Request, + _: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + xx_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 instance = data_init.init( + input_method, + InputMethodUserData { + handle: handle.clone(), + text_input_handle: text_input_handle.clone(), + dismiss_popup: D::dismiss_popup, + popup_geometry: D::popup_geometry, + popup_repositioned: D::popup_repositioned, + }, + ); + handle.add_instance(&instance); + } + xx_input_method_manager_v2::Request::GetPositioner { id } => { + data_init.init(id, PositionerUserData::default()); + } + xx_input_method_manager_v2::Request::Destroy => { + // Nothing to do + } + _ => unreachable!(), + } + } +} + +#[allow(missing_docs)] // TODO +#[macro_export] +macro_rules! delegate_input_method_manager_v3 { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + $crate::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wl_input_method::input_method::xx::server::xx_input_method_manager_v2::XxInputMethodManagerV2: + $crate::wayland::input_method_v3::InputMethodManagerGlobalData + ] => $crate::wayland::input_method_v3::InputMethodManagerState); + $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wl_input_method::input_method::xx::server::xx_input_method_manager_v2::XxInputMethodManagerV2: () + ] => $crate::wayland::input_method_v3::InputMethodManagerState); + $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wl_input_method::input_method::xx::server::xx_input_method_v1::XxInputMethodV1: + $crate::wayland::input_method_v3::InputMethodUserData + ] => $crate::wayland::input_method_v3::InputMethodManagerState); + $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wl_input_method::input_method::xx::server::xx_input_popup_surface_v2::XxInputPopupSurfaceV2: + $crate::wayland::input_method_v3::InputMethodPopupSurfaceUserData + ] => $crate::wayland::input_method_v3::InputMethodManagerState); + $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wl_input_method::input_method::xx::server::xx_input_popup_positioner_v1::XxInputPopupPositionerV1: + $crate::wayland::input_method_v3::PositionerUserData + ] => $crate::wayland::input_method_v3::InputMethodManagerState); + }; +} diff --git a/src/wayland/input_method_v3/positioner.rs b/src/wayland/input_method_v3/positioner.rs new file mode 100644 index 000000000000..0bed219880fb --- /dev/null +++ b/src/wayland/input_method_v3/positioner.rs @@ -0,0 +1,443 @@ +use super::InputMethodManagerState; +use crate::utils::{Logical, Point, Rectangle, Size}; +use std::cmp::min; +use std::sync::Mutex; +use wayland_server::{Dispatch, Resource, WEnum}; +use wl_input_method::input_method::xx::server::xx_input_popup_positioner_v1::{ + self, Anchor, ConstraintAdjustment, Gravity, XxInputPopupPositionerV1, +}; + +/// Not sure what to write here. I just copied the pattern of UserData without analyzing it. +#[derive(Default, Debug)] +pub struct PositionerUserData { + pub(crate) inner: Mutex, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// The state of a positioner, as set by the client +pub struct PositionerState { + /// Requested size of the rectangle to position. + /// + /// This is treated as the preferred size to aim for, even if it can't always be reached (e.g. due to output too small). + pub rect_size: Size, + /// Edges defining the anchor point + pub anchor_edges: Anchor, + /// Gravity direction for positioning the child surface + /// relative to its anchor point + pub gravity: Gravity, + /// Adjustments to do if previous criteria constrain the + /// surface + pub constraint_adjustment: 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, +} + +impl Default for PositionerState { + fn default() -> Self { + PositionerState { + anchor_edges: Anchor::None, + constraint_adjustment: ConstraintAdjustment::empty(), + gravity: Gravity::None, + offset: Default::default(), + rect_size: Default::default(), + reactive: false, + } + } +} + +// This mostly but not completely copied from xdg positioner. +// Converted to stateless: PositionerState doesn't store any state. +impl PositionerState { + pub(crate) fn anchor_has_edge(&self, edge: Anchor) -> bool { + match edge { + Anchor::Top => { + self.anchor_edges == Anchor::Top + || self.anchor_edges == Anchor::TopLeft + || self.anchor_edges == Anchor::TopRight + } + Anchor::Bottom => { + self.anchor_edges == Anchor::Bottom + || self.anchor_edges == Anchor::BottomLeft + || self.anchor_edges == Anchor::BottomRight + } + Anchor::Left => { + self.anchor_edges == Anchor::Left + || self.anchor_edges == Anchor::TopLeft + || self.anchor_edges == Anchor::BottomLeft + } + Anchor::Right => { + self.anchor_edges == Anchor::Right + || self.anchor_edges == Anchor::TopRight + || self.anchor_edges == 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, anchor_rect: Rectangle) -> Point { + let y = anchor_rect.loc.y + + if self.anchor_has_edge(Anchor::Top) { + 0 + } else if self.anchor_has_edge(Anchor::Bottom) { + anchor_rect.size.h + } else { + anchor_rect.size.h / 2 + }; + + let x = anchor_rect.loc.x + + if self.anchor_has_edge(Anchor::Left) { + 0 + } else if self.anchor_has_edge(Anchor::Right) { + anchor_rect.size.w + } else { + anchor_rect.size.w / 2 + }; + + (x, y).into() + } + + pub(crate) fn gravity_has_edge(&self, edge: Gravity) -> bool { + match edge { + Gravity::Top => { + self.gravity == Gravity::Top + || self.gravity == Gravity::TopLeft + || self.gravity == Gravity::TopRight + } + Gravity::Bottom => { + self.gravity == Gravity::Bottom + || self.gravity == Gravity::BottomLeft + || self.gravity == Gravity::BottomRight + } + Gravity::Left => { + self.gravity == Gravity::Left + || self.gravity == Gravity::TopLeft + || self.gravity == Gravity::BottomLeft + } + Gravity::Right => { + self.gravity == Gravity::Right + || self.gravity == Gravity::TopRight + || self.gravity == Gravity::BottomRight + } + _ => unreachable!(), + } + } + + /// Get the geometry without taking surface or display size into account. + /// + /// `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. + fn get_geometry(&self, anchor_rect: Rectangle) -> 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 loc = self.offset; + let size = 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. + loc += self.get_anchor_point(anchor_rect); + + // 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. + loc.y = if self.gravity_has_edge(Gravity::Top) { + loc.y.saturating_sub_unsigned(size.h) + } else if !self.gravity_has_edge(Gravity::Bottom) { + loc.y.saturating_sub_unsigned(size.h / 2) + } else { + loc.y + }; + + loc.x = if self.gravity_has_edge(Gravity::Left) { + loc.x.saturating_sub_unsigned(size.w) + } else if !self.gravity_has_edge(Gravity::Right) { + loc.x.saturating_sub_unsigned(size.w / 2) + } else { + loc.x + }; + + let size = ( + 0i32.saturating_add_unsigned(self.rect_size.w), + 0i32.saturating_add_unsigned(self.rect_size.h), + ) + .into(); + + Rectangle { loc, size } + } + + /// 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, + anchor_rect: Rectangle, + 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(anchor_rect); + 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(anchor_rect); + 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(anchor_rect); + 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_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_bottom) = compute_offsets(target, geo); + // off_left and off_right are the same since we're using rectangles. + } + + // Try to resize horizontally. This makes sense only if the popup is at least partially to the left + // of the right target edge, which is the same as checking that the offset is smaller than the width. + if off_right > 0 + && off_right < geo.size.w + && self.constraint_adjustment.contains(ConstraintAdjustment::ResizeX) + { + geo.size.w -= off_right; + } + + // Try to resize vertically. This makes sense only if the popup is at least partially to the top of + // the bottom target edge, which is the same as checking that the offset is smaller than the height. + if off_bottom > 0 + && off_bottom < geo.size.h + && self.constraint_adjustment.contains(ConstraintAdjustment::ResizeY) + { + geo.size.h -= off_bottom; + } + + geo + } + + /// Return the popup geometry computed based on the cursor anchor. + pub fn get_geometry_from_anchor( + &self, + cursor: Rectangle, + target: Rectangle, + ) -> Rectangle { + self.get_unconstrained_geometry(cursor, target) + } +} + +impl Dispatch for InputMethodManagerState +where +/*D: Dispatch, +D: XdgShellHandler, +D: 'static,*/ +{ + fn request( + _state: &mut D, + _client: &wayland_server::Client, + positioner: &XxInputPopupPositionerV1, + request: xx_input_popup_positioner_v1::Request, + data: &PositionerUserData, + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, D>, + ) { + let mut state = data.inner.lock().unwrap(); + use xx_input_popup_positioner_v1::Request; + match request { + Request::SetSize { width, height } => { + if width < 1 || height < 1 { + positioner.post_error( + xx_input_popup_positioner_v1::Error::InvalidInput, + "Invalid size for positioner.", + ); + } else { + state.rect_size = (width, height).into(); + } + } + Request::SetAnchor { anchor } => { + if let WEnum::Value(anchor) = anchor { + state.anchor_edges = anchor; + } + } + Request::SetGravity { gravity } => { + if let WEnum::Value(gravity) = gravity { + state.gravity = gravity; + } + } + Request::SetConstraintAdjustment { + constraint_adjustment, + } => { + if let WEnum::Value(constraint_adjustment) = constraint_adjustment { + state.constraint_adjustment = constraint_adjustment; + } + } + Request::SetOffset { x, y } => { + state.offset = (x, y).into(); + } + Request::SetReactive => { + state.reactive = true; + } + Request::Destroy => { + // handled by destructor + } + _ => unreachable!(), + } + } +} + +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, + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 70d0df354cd8..07e74f4b256d 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -62,6 +62,7 @@ pub mod fractional_scale; pub mod idle_inhibit; pub mod idle_notify; pub mod input_method; +pub mod input_method_v3; pub mod keyboard_shortcuts_inhibit; pub mod output; pub mod pointer_constraints; diff --git a/src/wayland/seat/keyboard.rs b/src/wayland/seat/keyboard.rs index 71ade8650c89..bd95d72c59ce 100644 --- a/src/wayland/seat/keyboard.rs +++ b/src/wayland/seat/keyboard.rs @@ -18,7 +18,9 @@ use crate::{ Seat, SeatHandler, SeatState, }, utils::{iter::new_locked_obj_iter_from_vec, Serial}, - wayland::{input_method::InputMethodSeat, text_input::TextInputSeat}, + wayland::{ + input_method::InputMethodSeat, input_method_v3::InputMethodSeat as _, text_input::TextInputSeat, + }, }; impl KeyboardHandle @@ -182,12 +184,17 @@ impl KeyboardTarget for WlSurface { input_method.deactivate_input_method(state); } + let input_method_v3 = seat.input_method_v3(); + if input_method_v3.has_instance() { + input_method_v3.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(self.clone())); // Only notify on `enter` once we have an actual IME. - if input_method.has_instance() { + if input_method.has_instance() || input_method_v3.has_instance() { text_input.enter(); } } @@ -200,6 +207,14 @@ impl KeyboardTarget for WlSurface { if input_method.has_instance() { input_method.deactivate_input_method(state); + } + + let input_method_v3 = seat.input_method_v3(); + if input_method_v3.has_instance() { + input_method_v3.deactivate_input_method(state); + } + + if input_method.has_instance() || input_method_v3.has_instance() { text_input.leave(); } diff --git a/src/wayland/text_input/mod.rs b/src/wayland/text_input/mod.rs index fe707df93b35..bb31c1b32f2d 100644 --- a/src/wayland/text_input/mod.rs +++ b/src/wayland/text_input/mod.rs @@ -62,7 +62,8 @@ use crate::input::{Seat, SeatHandler}; pub use text_input_handle::TextInputHandle; pub use text_input_handle::TextInputUserData; -use super::input_method::InputMethodHandle; +use super::input_method; +use super::input_method_v3; const MANAGER_VERSION: u32 = 1; @@ -149,20 +150,26 @@ where let user_data = seat.user_data(); user_data.insert_if_missing(TextInputHandle::default); - user_data.insert_if_missing(InputMethodHandle::default); + user_data.insert_if_missing(input_method::InputMethodHandle::default); + user_data.insert_if_missing(input_method_v3::InputMethodHandle::default); let handle = user_data.get::().unwrap(); - let input_method_handle = user_data.get::().unwrap(); + let input_method_handle = user_data.get::().unwrap(); + let input_method_v3_handle = user_data.get::().unwrap(); let instance = data_init.init( id, TextInputUserData { handle: handle.clone(), input_method_handle: input_method_handle.clone(), + input_method_v3_handle: input_method_v3_handle.clone(), }, ); handle.add_instance(&instance); if input_method_handle.has_instance() { handle.enter(); } + if input_method_v3_handle.has_instance() { + handle.enter(); + } } zwp_text_input_manager_v3::Request::Destroy => { // Nothing to do diff --git a/src/wayland/text_input/text_input_handle.rs b/src/wayland/text_input/text_input_handle.rs index 4b4ccd7e483d..434c14c2ad36 100644 --- a/src/wayland/text_input/text_input_handle.rs +++ b/src/wayland/text_input/text_input_handle.rs @@ -1,7 +1,7 @@ use std::mem; use std::sync::{Arc, Mutex}; -use tracing::debug; +use tracing::{debug, warn}; use wayland_protocols::wp::text_input::zv3::server::zwp_text_input_v3::{ self, ChangeCause, ContentHint, ContentPurpose, ZwpTextInputV3, }; @@ -10,7 +10,8 @@ use wayland_server::{protocol::wl_surface::WlSurface, Dispatch, Resource}; use crate::input::SeatHandler; use crate::utils::{Logical, Rectangle}; -use crate::wayland::input_method::InputMethodHandle; +use crate::wayland::input_method; +use crate::wayland::input_method_v3; use super::TextInputManagerState; @@ -186,13 +187,15 @@ impl TextInputHandle { #[derive(Debug)] pub struct TextInputUserData { pub(super) handle: TextInputHandle, - pub(crate) input_method_handle: InputMethodHandle, + pub(crate) input_method_handle: input_method::InputMethodHandle, + pub(crate) input_method_v3_handle: input_method_v3::InputMethodHandle, } impl Dispatch for TextInputManagerState where D: Dispatch, D: SeatHandler, + //D: input_method_v3::InputMethodHandler, D: 'static, { fn request( @@ -209,12 +212,17 @@ where data.handle.increment_serial(resource); } - // Discard requsets without any active input method instance. - if !data.input_method_handle.has_instance() { + // Discard requests without any active input method instance. + if !data.input_method_handle.has_instance() && !data.input_method_v3_handle.has_instance() { debug!("discarding text-input request without IME running"); return; } + if data.input_method_handle.has_instance() && data.input_method_v3_handle.has_instance() { + warn!("Two separate versions of input method registered for the seat. Expect conflicts."); + // We'll try to drive both IM instances because it makes the code simpler. The results are going to be unexpected no matter what strategy is chosen now. + } + let focus = match data.handle.focus() { Some(focus) if focus.id().same_client_as(&resource.id()) => focus, _ => { @@ -274,12 +282,14 @@ where // Drop the guard before calling to other subsystem. drop(guard); data.input_method_handle.activate_input_method(state, &focus); + data.input_method_v3_handle.activate_input_method(state, &focus); } Some(false) => { *active_text_input_id = None; // Drop the guard before calling to other subsystem. drop(guard); data.input_method_handle.deactivate_input_method(state); + data.input_method_v3_handle.deactivate_input_method(state); return; } None => { @@ -294,7 +304,10 @@ where } if let Some((text, cursor, anchor)) = new_state.surrounding_text.take() { - data.input_method_handle.with_instance(move |input_method| { + data.input_method_handle.with_instance(|input_method| { + input_method.object.surrounding_text(text.clone(), cursor, anchor) + }); + data.input_method_v3_handle.with_instance(move |input_method| { input_method.object.surrounding_text(text, cursor, anchor) }); } @@ -303,22 +316,30 @@ where data.input_method_handle.with_instance(move |input_method| { input_method.object.text_change_cause(cause); }); + data.input_method_v3_handle.with_instance(move |input_method| { + input_method.object.text_change_cause(cause); + }); } if let Some((hint, purpose)) = new_state.content_type.take() { data.input_method_handle.with_instance(move |input_method| { input_method.object.content_type(hint, purpose); }); + data.input_method_v3_handle.with_instance(move |input_method| { + input_method.object.content_type(hint, purpose); + }); } if let Some(rect) = new_state.cursor_rectangle.take() { data.input_method_handle .set_text_input_rectangle::(state, rect); + data.input_method_v3_handle.set_cursor_rectangle::(state, rect); } data.input_method_handle.with_instance(|input_method| { input_method.done(); }); + data.input_method_v3_handle.done(); } zwp_text_input_v3::Request::Destroy => { // Nothing to do @@ -349,6 +370,7 @@ where if deactivate_im { data.input_method_handle.deactivate_input_method(state); + data.input_method_v3_handle.deactivate_input_method(state); } } }