diff --git a/docs/features.md b/docs/features.md index 0c4f9993..d9348a9e 100644 --- a/docs/features.md +++ b/docs/features.md @@ -141,10 +141,12 @@ Jay supports the following wayland protocols: | zwlr_layer_shell_v1 | 4[^no_exclusive] | Yes | | zwlr_screencopy_manager_v1 | 3 | Yes | | zwp_idle_inhibit_manager_v1 | 1 | | +| zwp_input_method_manager_v2 | 1 | Yes | | zwp_linux_dmabuf_v1 | 5 | | | zwp_pointer_constraints_v1 | 1 | | | zwp_primary_selection_device_manager_v1 | 1 | | | zwp_relative_pointer_manager_v1 | 1 | | +| zwp_text_input_manager_v3 | 1 | | | zwp_virtual_keyboard_manager_v1 | 1 | Yes | | zxdg_decoration_manager_v1 | 1 | | | zxdg_output_manager_v1 | 3 | | diff --git a/release-notes.md b/release-notes.md index 21fd6552..c4b37960 100644 --- a/release-notes.md +++ b/release-notes.md @@ -3,6 +3,8 @@ - Add support for wp-alpha-modifier. - Add support for per-device keymaps. - Add support for virtual-keyboard-unstable-v1. +- Add support for zwp_input_method_manager_v2. +- Add support for zwp_text_input_manager_v3. # 1.0.3 (2024-04-11) diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index fc72a813..4a598416 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -593,6 +593,7 @@ impl MetalConnector { Some(output.global.pos.get()), Some(rr), output.global.persistent.scale.get(), + true, render_hw_cursor, output.has_fullscreen(), output.global.persistent.transform.get(), diff --git a/src/compositor.rs b/src/compositor.rs index 3d3add14..f3b358fc 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -18,7 +18,7 @@ use { globals::Globals, ifs::{ wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, - wl_surface::NoneSurfaceExt, + wl_surface::{zwp_input_popup_surface_v2::input_popup_positioning, NoneSurfaceExt}, }, io_uring::{IoUring, IoUringError}, leaks, @@ -172,6 +172,7 @@ fn start_compositor2( pending_output_render_data: Default::default(), pending_float_layout: Default::default(), pending_float_titles: Default::default(), + pending_input_popup_positioning: Default::default(), dbus: Dbus::new(&engine, &ring, &run_toplevel), fdcloser: FdCloser::new(), logger: logger.clone(), @@ -327,6 +328,7 @@ fn start_global_event_handlers( eng.spawn2(Phase::Layout, float_layout(state.clone())), eng.spawn2(Phase::PostLayout, float_titles(state.clone())), eng.spawn2(Phase::PostLayout, idle(state.clone(), backend.clone())), + eng.spawn2(Phase::PostLayout, input_popup_positioning(state.clone())), ] } diff --git a/src/gfx_api.rs b/src/gfx_api.rs index 96b6060b..cd3cc317 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -327,6 +327,7 @@ impl dyn GfxFramebuffer { cursor_rect: Option, result: Option<&mut RenderResult>, scale: Scale, + render_cursor: bool, render_hardware_cursor: bool, black_background: bool, transform: Transform, @@ -347,6 +348,18 @@ impl dyn GfxFramebuffer { let seats = state.globals.lock_seats(); for seat in seats.values() { let (mut x, mut y) = seat.get_position(); + if let Some(im) = seat.input_method() { + for (_, popup) in &im.popups { + if popup.surface.node_visible() { + let pos = popup.surface.buffer_abs_pos.get(); + let extents = popup.surface.extents.get().move_(pos.x1(), pos.y1()); + if extents.intersects(&rect) { + let (x, y) = rect.translate(pos.x1(), pos.y1()); + renderer.render_surface(&popup.surface, x, y, None); + } + } + } + } if let Some(drag) = seat.toplevel_drag() { if let Some(tl) = drag.toplevel.get() { if tl.xdg.surface.buffer.get().is_some() { @@ -368,12 +381,14 @@ impl dyn GfxFramebuffer { renderer.render_surface(&dnd_icon, x, y, None); } } - if let Some(cursor) = seat.get_cursor() { - if render_hardware_cursor || !seat.hardware_cursor() { - cursor.tick(); - x -= Fixed::from_int(rect.x1()); - y -= Fixed::from_int(rect.y1()); - cursor.render(&mut renderer, x, y); + if render_cursor { + if let Some(cursor) = seat.get_cursor() { + if render_hardware_cursor || !seat.hardware_cursor() { + cursor.tick(); + x -= Fixed::from_int(rect.x1()); + y -= Fixed::from_int(rect.y1()); + cursor.render(&mut renderer, x, y); + } } } } @@ -407,6 +422,7 @@ impl dyn GfxFramebuffer { cursor_rect, result, scale, + true, render_hardware_cursor, node.has_fullscreen(), node.global.persistent.transform.get(), @@ -420,6 +436,7 @@ impl dyn GfxFramebuffer { cursor_rect: Option, result: Option<&mut RenderResult>, scale: Scale, + render_cursor: bool, render_hardware_cursor: bool, black_background: bool, transform: Transform, @@ -430,6 +447,7 @@ impl dyn GfxFramebuffer { cursor_rect, result, scale, + render_cursor, render_hardware_cursor, black_background, transform, diff --git a/src/globals.rs b/src/globals.rs index 842aca3b..6e3aa850 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -17,9 +17,14 @@ use { wl_output::WlOutputGlobal, wl_registry::WlRegistry, wl_seat::{ + text_input::{ + zwp_input_method_manager_v2::ZwpInputMethodManagerV2Global, + zwp_text_input_manager_v3::ZwpTextInputManagerV3Global, + }, zwp_pointer_constraints_v1::ZwpPointerConstraintsV1Global, zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1Global, - zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1Global, WlSeatGlobal, + zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1Global, + WlSeatGlobal, }, wl_shm::WlShmGlobal, wl_subcompositor::WlSubcompositorGlobal, @@ -177,6 +182,8 @@ impl Globals { add_singleton!(ZwlrDataControlManagerV1Global); add_singleton!(WpAlphaModifierV1Global); add_singleton!(ZwpVirtualKeyboardManagerV1Global); + add_singleton!(ZwpInputMethodManagerV2Global); + add_singleton!(ZwpTextInputManagerV3Global); } pub fn add_backend_singletons(&self, backend: &Rc) { diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 04075381..11831159 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -1,6 +1,7 @@ mod event_handling; mod kb_owner; mod pointer_owner; +pub mod text_input; pub mod wl_keyboard; pub mod wl_pointer; pub mod wl_touch; @@ -37,6 +38,10 @@ use { wl_seat::{ kb_owner::KbOwnerHolder, pointer_owner::PointerOwnerHolder, + text_input::{ + zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2, + zwp_input_method_v2::ZwpInputMethodV2, zwp_text_input_v3::ZwpTextInputV3, + }, wl_keyboard::{WlKeyboard, WlKeyboardError, REPEAT_INFO_SINCE}, wl_pointer::WlPointer, wl_touch::WlTouch, @@ -63,7 +68,7 @@ use { wire::{ wl_seat::*, ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId, WlSeatId, ZwlrDataControlDeviceV1Id, ZwpPrimarySelectionDeviceV1Id, - ZwpRelativePointerV1Id, + ZwpRelativePointerV1Id, ZwpTextInputV3Id, }, xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState}, }, @@ -78,7 +83,7 @@ use { rc::{Rc, Weak}, }, thiserror::Error, - uapi::{c, Errno, OwnedFd}, + uapi::OwnedFd, }; pub const POINTER: u32 = 1; @@ -166,6 +171,10 @@ pub struct WlSeatGlobal { constraint: CloneCell>>, idle_notifications: CopyHashMap<(ClientId, ExtIdleNotificationV1Id), Rc>, last_input_usec: Cell, + text_inputs: RefCell>>>, + text_input: CloneCell>>, + input_method: CloneCell>>, + input_method_grab: CloneCell>>, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -230,6 +239,10 @@ impl WlSeatGlobal { idle_notifications: Default::default(), last_input_usec: Cell::new(now_usec()), wlr_data_devices: Default::default(), + text_inputs: Default::default(), + text_input: Default::default(), + input_method: Default::default(), + input_method_grab: Default::default(), }); state.add_cursor_size(*DEFAULT_CURSOR_SIZE); let seat = slf.clone(); @@ -250,6 +263,10 @@ impl WlSeatGlobal { self.seat_kb_map.get() } + pub fn input_method(&self) -> Option> { + self.input_method.get() + } + pub fn toplevel_drag(&self) -> Option> { self.pointer_owner.toplevel_drag() } @@ -731,6 +748,9 @@ impl WlSeatGlobal { } } } + if let Some(grab) = self.input_method_grab.get() { + grab.send_repeat_info(); + } } pub fn close(self: &Rc) { @@ -1048,6 +1068,10 @@ impl WlSeatGlobal { self.tree_changed_handler.set(None); self.output.set(self.state.dummy_output.get().unwrap()); self.constraint.take(); + self.text_inputs.borrow_mut().clear(); + self.text_input.take(); + self.input_method.take(); + self.input_method_grab.take(); } pub fn id(&self) -> SeatId { @@ -1116,6 +1140,11 @@ impl WlSeatGlobal { tl.tl_set_visible(visible); } } + if let Some(im) = self.input_method.get() { + for (_, popup) in &im.popups { + popup.update_visible(); + } + } } } @@ -1175,21 +1204,7 @@ impl WlSeat { if self.version >= READ_ONLY_KEYMAP_SINCE { return Ok(state.map.clone()); } - let fd = match uapi::memfd_create("shared-keymap", c::MFD_CLOEXEC) { - Ok(fd) => fd, - Err(e) => return Err(WlKeyboardError::KeymapMemfd(e.into())), - }; - let target = state.map_len as c::off_t; - let mut pos = 0; - while pos < target { - let rem = target - pos; - let res = uapi::sendfile(fd.raw(), state.map.raw(), Some(&mut pos), rem as usize); - match res { - Ok(_) | Err(Errno(c::EINTR)) => {} - Err(e) => return Err(WlKeyboardError::KeymapCopy(e.into())), - } - } - Ok(Rc::new(fd)) + Ok(state.create_new_keymap_fd()?) } } diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index f7328a65..f39527ee 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -14,6 +14,7 @@ use { DynDataSource, }, wl_seat::{ + text_input::TextDisconnectReason, wl_keyboard::{self, WlKeyboard}, wl_pointer::{ self, PendingScroll, WlPointer, AXIS_DISCRETE_SINCE_VERSION, @@ -393,8 +394,12 @@ impl WlSeatGlobal { t.send_key(self.id, time_usec, key, key_state); }); let node = self.keyboard_node.get(); + let input_method_grab = self.input_method_grab.get(); if shortcuts.is_empty() { - node.node_on_key(self, time_usec, key, state, &xkb_state.kb_state); + match &input_method_grab { + Some(g) => g.on_key(time_usec, key, state, &xkb_state.kb_state), + _ => node.node_on_key(self, time_usec, key, state, &xkb_state.kb_state), + } } else if let Some(config) = self.state.config.get() { let id = xkb_state.kb_state.id; drop(xkb_state); @@ -411,7 +416,10 @@ impl WlSeatGlobal { self.state.for_each_seat_tester(|t| { t.send_modifiers(self.id, &xkb_state.kb_state.mods); }); - node.node_on_mods(self, &xkb_state.kb_state); + match &input_method_grab { + Some(g) => g.on_modifiers(&xkb_state.kb_state), + _ => node.node_on_mods(self, &xkb_state.kb_state), + } } match key_state { KeyState::Released => { @@ -760,6 +768,18 @@ impl WlSeatGlobal { // Unfocus callbacks impl WlSeatGlobal { pub fn unfocus_surface(&self, surface: &WlSurface) { + if let Some(ti) = self.text_input.take() { + if let Some(con) = ti.connection.get() { + con.disconnect(TextDisconnectReason::FocusLost); + } + } + if let Some(tis) = self.text_inputs.borrow().get(&surface.client.id) { + for ti in tis.lock().values() { + ti.send_leave(surface); + ti.send_done(); + } + } + let serial = surface.client.next_serial(); self.surface_kb_event(Version::ALL, surface, |k| k.send_leave(serial, surface.id)) } @@ -785,6 +805,13 @@ impl WlSeatGlobal { &surface.client, ); } + + if let Some(tis) = self.text_inputs.borrow_mut().get(&surface.client.id) { + for ti in tis.lock().values() { + ti.send_enter(surface); + ti.send_done(); + } + } } } diff --git a/src/ifs/wl_seat/text_input.rs b/src/ifs/wl_seat/text_input.rs new file mode 100644 index 00000000..44154554 --- /dev/null +++ b/src/ifs/wl_seat/text_input.rs @@ -0,0 +1,96 @@ +use { + crate::ifs::{ + wl_seat::{ + text_input::{ + zwp_input_method_v2::ZwpInputMethodV2, zwp_text_input_v3::ZwpTextInputV3, + }, + WlSeatGlobal, + }, + wl_surface::WlSurface, + }, + std::rc::Rc, +}; + +pub mod zwp_input_method_keyboard_grab_v2; +pub mod zwp_input_method_manager_v2; +pub mod zwp_input_method_v2; +pub mod zwp_text_input_manager_v3; +pub mod zwp_text_input_v3; + +const MAX_TEXT_SIZE: usize = 4000; + +pub struct TextInputConnection { + pub seat: Rc, + pub text_input: Rc, + pub input_method: Rc, + pub surface: Rc, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum TextConnectReason { + TextInputEnabled, + InputMethodCreated, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum TextDisconnectReason { + FocusLost, + TextInputDisabled, + InputMethodDestroyed, +} + +impl WlSeatGlobal { + fn create_text_input_connection(self: &Rc, text_connect_reason: TextConnectReason) { + let Some(im) = self.input_method.get() else { + return; + }; + let Some(ti) = self.text_input.get() else { + return; + }; + let Some(surface) = self.keyboard_node.get().node_into_surface() else { + log::warn!("Seat has text input but keyboard node is not a surface"); + return; + }; + if surface.client.id != ti.client.id { + log::warn!("Seat's text input belongs to different client than the keyboard node"); + return; + } + let con = Rc::new(TextInputConnection { + seat: self.clone(), + text_input: ti.clone(), + input_method: im.clone(), + surface: surface.clone(), + }); + con.connect(text_connect_reason); + } +} + +impl TextInputConnection { + fn connect(self: &Rc, reason: TextConnectReason) { + self.input_method.connection.set(Some(self.clone())); + self.text_input.connection.set(Some(self.clone())); + self.surface + .text_input_connections + .insert(self.seat.id, self.clone()); + + self.input_method.activate(); + if reason == TextConnectReason::InputMethodCreated { + self.text_input.send_all_to(&self.input_method); + self.input_method.send_done(); + } + } + + pub fn disconnect(&self, reason: TextDisconnectReason) { + self.text_input.connection.take(); + self.input_method.connection.take(); + self.surface.text_input_connections.remove(&self.seat.id); + + if reason != TextDisconnectReason::InputMethodDestroyed { + self.input_method.send_deactivate(); + self.input_method.send_done(); + for (_, popup) in &self.input_method.popups { + popup.update_visible(); + } + } + } +} diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_keyboard_grab_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_keyboard_grab_v2.rs new file mode 100644 index 00000000..062f0d0f --- /dev/null +++ b/src/ifs/wl_seat/text_input/zwp_input_method_keyboard_grab_v2.rs @@ -0,0 +1,126 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::wl_seat::{text_input::zwp_input_method_v2::ZwpInputMethodV2, wl_keyboard}, + leaks::Tracker, + object::{Object, Version}, + utils::errorfmt::ErrorFmt, + wire::{zwp_input_method_keyboard_grab_v2::*, ZwpInputMethodKeyboardGrabV2Id}, + xkbcommon::{KeyboardState, KeyboardStateId}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct ZwpInputMethodKeyboardGrabV2 { + pub id: ZwpInputMethodKeyboardGrabV2Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + pub input_method: Rc, + pub kb_state_id: Cell, +} + +impl ZwpInputMethodKeyboardGrabV2 { + fn detach(&self) { + self.input_method.seat.input_method_grab.take(); + } + + fn send_keymap(&self, kb_state: &KeyboardState) { + let map = match kb_state.create_new_keymap_fd() { + Ok(m) => m, + Err(e) => { + log::error!("Could not create new keymap fd: {}", ErrorFmt(e)); + return; + } + }; + self.client.event(Keymap { + self_id: self.id, + format: wl_keyboard::XKB_V1, + fd: map, + size: kb_state.map_len as _, + }); + } + + fn update_state(&self, serial: u32, kb_state: &KeyboardState) { + self.send_keymap(kb_state); + self.send_modifiers(serial, kb_state); + self.kb_state_id.set(kb_state.id); + } + + pub fn on_key(&self, time_usec: u64, key: u32, state: u32, kb_state: &KeyboardState) { + let serial = self.client.next_serial(); + if self.kb_state_id.get() != kb_state.id { + self.update_state(serial, kb_state); + } + self.send_key(serial, time_usec, key, state); + } + + fn send_key(&self, serial: u32, time_usec: u64, key: u32, state: u32) { + self.client.event(Key { + self_id: self.id, + serial, + time: (time_usec / 1000) as _, + key, + state, + }) + } + + pub fn on_modifiers(&self, kb_state: &KeyboardState) { + let serial = self.client.next_serial(); + if self.kb_state_id.get() != kb_state.id { + self.update_state(serial, kb_state); + } + self.send_modifiers(serial, kb_state); + } + + fn send_modifiers(&self, serial: u32, kb_state: &KeyboardState) { + self.client.event(Modifiers { + self_id: self.id, + serial, + mods_depressed: kb_state.mods.mods_depressed, + mods_latched: kb_state.mods.mods_latched, + mods_locked: kb_state.mods.mods_locked, + group: kb_state.mods.group, + }) + } + + pub fn send_repeat_info(&self) { + let (rate, delay) = self.input_method.seat.repeat_rate.get(); + self.client.event(RepeatInfo { + self_id: self.id, + rate, + delay, + }) + } +} + +impl ZwpInputMethodKeyboardGrabV2RequestHandler for ZwpInputMethodKeyboardGrabV2 { + type Error = ZwpInputMethodKeyboardGrabV2Error; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = ZwpInputMethodKeyboardGrabV2; + version = self.version; +} + +impl Object for ZwpInputMethodKeyboardGrabV2 { + fn break_loops(&self) { + self.detach(); + } +} + +simple_add_obj!(ZwpInputMethodKeyboardGrabV2); + +#[derive(Debug, Error)] +pub enum ZwpInputMethodKeyboardGrabV2Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ZwpInputMethodKeyboardGrabV2Error, ClientError); diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs new file mode 100644 index 00000000..d2b03ab4 --- /dev/null +++ b/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs @@ -0,0 +1,116 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::{Global, GlobalName}, + ifs::wl_seat::text_input::{zwp_input_method_v2::ZwpInputMethodV2, TextConnectReason}, + leaks::Tracker, + object::{Object, Version}, + wire::{zwp_input_method_manager_v2::*, ZwpInputMethodManagerV2Id}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct ZwpInputMethodManagerV2Global { + pub name: GlobalName, +} + +pub struct ZwpInputMethodManagerV2 { + pub id: ZwpInputMethodManagerV2Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, +} + +impl ZwpInputMethodManagerV2Global { + pub fn new(name: GlobalName) -> Self { + Self { name } + } + + fn bind_( + self: Rc, + id: ZwpInputMethodManagerV2Id, + client: &Rc, + version: Version, + ) -> Result<(), ZwpTextInputManagerV3Error> { + let obj = Rc::new(ZwpInputMethodManagerV2 { + id, + client: client.clone(), + tracker: Default::default(), + version, + }); + track!(client, obj); + client.add_client_obj(&obj)?; + Ok(()) + } +} + +global_base!( + ZwpInputMethodManagerV2Global, + ZwpInputMethodManagerV2, + ZwpTextInputManagerV3Error +); + +impl Global for ZwpInputMethodManagerV2Global { + fn singleton(&self) -> bool { + true + } + + fn version(&self) -> u32 { + 1 + } +} + +simple_add_global!(ZwpInputMethodManagerV2Global); + +impl ZwpInputMethodManagerV2RequestHandler for ZwpInputMethodManagerV2 { + type Error = ZwpTextInputManagerV3Error; + + fn get_input_method(&self, req: GetInputMethod, _slf: &Rc) -> Result<(), Self::Error> { + let seat = self.client.lookup(req.seat)?; + let inert = seat.global.input_method.is_some(); + let im = Rc::new(ZwpInputMethodV2 { + id: req.input_method, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + seat: seat.global.clone(), + popups: Default::default(), + connection: Default::default(), + inert, + num_done: Default::default(), + pending: Default::default(), + }); + track!(self.client, im); + self.client.add_client_obj(&im)?; + if inert { + im.send_unavailable(); + } else { + seat.global.input_method.set(Some(im)); + seat.global + .create_text_input_connection(TextConnectReason::InputMethodCreated); + } + Ok(()) + } + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = ZwpInputMethodManagerV2; + version = self.version; +} + +impl Object for ZwpInputMethodManagerV2 {} + +simple_add_obj!(ZwpInputMethodManagerV2); + +#[derive(Debug, Error)] +pub enum ZwpTextInputManagerV3Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ZwpTextInputManagerV3Error, ClientError); diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs new file mode 100644 index 00000000..788b5ae9 --- /dev/null +++ b/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs @@ -0,0 +1,234 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::{ + wl_seat::{ + text_input::{ + zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2, + TextDisconnectReason, TextInputConnection, MAX_TEXT_SIZE, + }, + WlSeatGlobal, + }, + wl_surface::zwp_input_popup_surface_v2::{ + ZwpInputPopupSurfaceV2, ZwpInputPopupSurfaceV2Error, + }, + }, + leaks::Tracker, + object::{Object, Version}, + utils::{clonecell::CloneCell, numcell::NumCell, smallmap::SmallMap}, + wire::{zwp_input_method_v2::*, ZwpInputMethodV2Id, ZwpInputPopupSurfaceV2Id}, + xkbcommon::KeyboardStateId, + }, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + }, + thiserror::Error, +}; + +pub struct ZwpInputMethodV2 { + pub id: ZwpInputMethodV2Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + pub seat: Rc, + pub popups: SmallMap, 1>, + pub connection: CloneCell>>, + pub inert: bool, + pub num_done: NumCell, + pub pending: RefCell, +} + +#[derive(Default)] +pub struct Pending { + commit_string: Option, + delete_surrounding_text: Option<(u32, u32)>, + preedit_string: Option<(String, i32, i32)>, +} + +impl ZwpInputMethodV2 { + fn detach(&self) { + if let Some(con) = self.connection.get() { + con.disconnect(TextDisconnectReason::InputMethodDestroyed); + } + self.popups.clear(); + if !self.inert { + self.seat.input_method.take(); + } + } + + pub fn activate(&self) { + self.pending.take(); + self.send_activate(); + } + + pub fn send_activate(&self) { + self.client.event(Activate { self_id: self.id }); + } + + pub fn send_deactivate(&self) { + self.client.event(Deactivate { self_id: self.id }); + } + + pub fn send_surrounding_text(&self, text: &str, cursor: u32, anchor: u32) { + self.client.event(SurroundingText { + self_id: self.id, + text, + cursor, + anchor, + }); + } + + pub fn send_text_change_cause(&self, cause: u32) { + self.client.event(TextChangeCause { + self_id: self.id, + cause, + }); + } + + pub fn send_content_type(&self, hint: u32, purpose: u32) { + self.client.event(ContentType { + self_id: self.id, + hint, + purpose, + }); + } + + pub fn send_done(&self) { + self.num_done.fetch_add(1); + self.client.event(Done { self_id: self.id }); + } + + pub fn send_unavailable(&self) { + self.client.event(Unavailable { self_id: self.id }); + } +} + +impl ZwpInputMethodV2RequestHandler for ZwpInputMethodV2 { + type Error = ZwpInputMethodV2Error; + + fn commit_string(&self, req: CommitString<'_>, _slf: &Rc) -> Result<(), Self::Error> { + if req.text.len() > MAX_TEXT_SIZE { + return Err(ZwpInputMethodV2Error::TooLarge); + } + self.pending.borrow_mut().commit_string = Some(req.text.to_string()); + Ok(()) + } + + fn set_preedit_string( + &self, + req: SetPreeditString<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + if req.text.len() > MAX_TEXT_SIZE { + return Err(ZwpInputMethodV2Error::TooLarge); + } + self.pending.borrow_mut().preedit_string = + Some((req.text.to_string(), req.cursor_begin, req.cursor_end)); + Ok(()) + } + + fn delete_surrounding_text( + &self, + req: DeleteSurroundingText, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.pending.borrow_mut().delete_surrounding_text = + Some((req.before_length, req.after_length)); + Ok(()) + } + + fn commit(&self, req: Commit, _slf: &Rc) -> Result<(), Self::Error> { + if req.serial != self.num_done.get() { + return Ok(()); + } + let pending = self.pending.take(); + let Some(con) = self.connection.get() else { + return Ok(()); + }; + if let Some(dst) = pending.delete_surrounding_text { + con.text_input.send_delete_surrounding_text(dst.0, dst.1); + } + if let Some(dst) = pending.preedit_string { + con.text_input + .send_preedit_string(Some(&dst.0), dst.1, dst.2); + } + if let Some(dst) = pending.commit_string { + con.text_input.send_commit_string(Some(&dst)); + } + con.text_input.send_done(); + Ok(()) + } + + fn get_input_popup_surface( + &self, + req: GetInputPopupSurface, + slf: &Rc, + ) -> Result<(), Self::Error> { + let surface = self.client.lookup(req.surface)?; + let popup = Rc::new(ZwpInputPopupSurfaceV2 { + id: req.id, + client: self.client.clone(), + input_method: slf.clone(), + surface, + version: self.version, + tracker: Default::default(), + positioning_scheduled: Cell::new(false), + }); + track!(self.client, popup); + self.client.add_client_obj(&popup)?; + popup.install()?; + Ok(()) + } + + fn grab_keyboard(&self, req: GrabKeyboard, slf: &Rc) -> Result<(), Self::Error> { + if self.seat.input_method_grab.is_some() { + return Err(ZwpInputMethodV2Error::HasGrab); + } + let grab = Rc::new(ZwpInputMethodKeyboardGrabV2 { + id: req.keyboard, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + input_method: slf.clone(), + kb_state_id: Cell::new(KeyboardStateId::from_raw(0)), + }); + track!(self.client, grab); + self.client.add_client_obj(&grab)?; + grab.send_repeat_info(); + self.seat.input_method_grab.set(Some(grab)); + Ok(()) + } + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = ZwpInputMethodV2; + version = self.version; +} + +impl Object for ZwpInputMethodV2 { + fn break_loops(&self) { + self.detach(); + } +} + +simple_add_obj!(ZwpInputMethodV2); + +#[derive(Debug, Error)] +pub enum ZwpInputMethodV2Error { + #[error(transparent)] + ClientError(Box), + #[error(transparent)] + ZwpInputPopupSurfaceV2Error(#[from] ZwpInputPopupSurfaceV2Error), + #[error("Text is larger than {} bytes", MAX_TEXT_SIZE)] + TooLarge, + #[error("Seat already has a grab")] + HasGrab, +} +efrom!(ZwpInputMethodV2Error, ClientError); diff --git a/src/ifs/wl_seat/text_input/zwp_text_input_manager_v3.rs b/src/ifs/wl_seat/text_input/zwp_text_input_manager_v3.rs new file mode 100644 index 00000000..0f30bc68 --- /dev/null +++ b/src/ifs/wl_seat/text_input/zwp_text_input_manager_v3.rs @@ -0,0 +1,114 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::{Global, GlobalName}, + ifs::wl_seat::text_input::zwp_text_input_v3::ZwpTextInputV3, + leaks::Tracker, + object::{Object, Version}, + wire::{zwp_text_input_manager_v3::*, ZwpTextInputManagerV3Id}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct ZwpTextInputManagerV3Global { + pub name: GlobalName, +} + +pub struct ZwpTextInputManagerV3 { + pub id: ZwpTextInputManagerV3Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, +} + +impl ZwpTextInputManagerV3Global { + pub fn new(name: GlobalName) -> Self { + Self { name } + } + + fn bind_( + self: Rc, + id: ZwpTextInputManagerV3Id, + client: &Rc, + version: Version, + ) -> Result<(), ZwpTextInputManagerV3Error> { + let obj = Rc::new(ZwpTextInputManagerV3 { + id, + client: client.clone(), + tracker: Default::default(), + version, + }); + track!(client, obj); + client.add_client_obj(&obj)?; + Ok(()) + } +} + +global_base!( + ZwpTextInputManagerV3Global, + ZwpTextInputManagerV3, + ZwpTextInputManagerV3Error +); + +impl Global for ZwpTextInputManagerV3Global { + fn singleton(&self) -> bool { + true + } + + fn version(&self) -> u32 { + 1 + } +} + +simple_add_global!(ZwpTextInputManagerV3Global); + +impl ZwpTextInputManagerV3RequestHandler for ZwpTextInputManagerV3 { + type Error = ZwpTextInputManagerV3Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn get_text_input(&self, req: GetTextInput, _slf: &Rc) -> Result<(), Self::Error> { + let seat = self.client.lookup(req.seat)?; + let ti = Rc::new(ZwpTextInputV3::new( + req.id, + &self.client, + &seat.global, + self.version, + )); + track!(self.client, ti); + self.client.add_client_obj(&ti)?; + seat.global + .text_inputs + .borrow_mut() + .entry(self.client.id) + .or_default() + .set(req.id, ti.clone()); + if let Some(surface) = seat.global.keyboard_node.get().node_into_surface() { + if surface.client.id == self.client.id { + ti.send_enter(&surface); + ti.send_done(); + } + } + Ok(()) + } +} + +object_base! { + self = ZwpTextInputManagerV3; + version = self.version; +} + +impl Object for ZwpTextInputManagerV3 {} + +simple_add_obj!(ZwpTextInputManagerV3); + +#[derive(Debug, Error)] +pub enum ZwpTextInputManagerV3Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ZwpTextInputManagerV3Error, ClientError); diff --git a/src/ifs/wl_seat/text_input/zwp_text_input_v3.rs b/src/ifs/wl_seat/text_input/zwp_text_input_v3.rs new file mode 100644 index 00000000..6ac51501 --- /dev/null +++ b/src/ifs/wl_seat/text_input/zwp_text_input_v3.rs @@ -0,0 +1,320 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::{ + wl_seat::{ + text_input::{ + zwp_input_method_v2::ZwpInputMethodV2, TextConnectReason, TextDisconnectReason, + TextInputConnection, MAX_TEXT_SIZE, + }, + WlSeatGlobal, + }, + wl_surface::WlSurface, + }, + leaks::Tracker, + object::{Object, Version}, + rect::Rect, + utils::{clonecell::CloneCell, numcell::NumCell}, + wire::{zwp_text_input_v3::*, ZwpTextInputV3Id}, + }, + std::{cell::RefCell, collections::hash_map::Entry, mem, rc::Rc}, + thiserror::Error, +}; + +pub struct ZwpTextInputV3 { + pub id: ZwpTextInputV3Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + seat: Rc, + num_commits: NumCell, + + state: RefCell, + pending: RefCell, + + pub connection: CloneCell>>, +} + +impl ZwpTextInputV3 { + pub fn cursor_rect(&self) -> Rect { + self.state.borrow().cursor_rectangle + } + + pub fn new( + id: ZwpTextInputV3Id, + client: &Rc, + seat: &Rc, + version: Version, + ) -> Self { + Self { + id, + client: client.clone(), + tracker: Default::default(), + version, + seat: seat.clone(), + num_commits: Default::default(), + state: Default::default(), + pending: Default::default(), + connection: Default::default(), + } + } + + fn detach(&self) { + self.do_disable(); + { + let tis = &mut *self.seat.text_inputs.borrow_mut(); + if let Entry::Occupied(mut oe) = tis.entry(self.client.id) { + oe.get_mut().remove(&self.id); + if oe.get().is_empty() { + oe.remove(); + } + } + } + } + + pub fn send_all_to(&self, im: &ZwpInputMethodV2) { + let state = &*self.state.borrow(); + { + let (a, b, c) = &state.surrounding_text; + im.send_surrounding_text(a, *b, *c); + } + im.send_content_type(state.content_type.0, state.content_type.1); + } + + pub fn send_enter(&self, surface: &WlSurface) { + self.client.event(Enter { + self_id: self.id, + surface: surface.id, + }); + } + + pub fn send_leave(&self, surface: &WlSurface) { + self.client.event(Leave { + self_id: self.id, + surface: surface.id, + }); + } + + pub fn send_preedit_string(&self, text: Option<&str>, cursor_begin: i32, cursor_end: i32) { + self.client.event(PreeditString { + self_id: self.id, + text, + cursor_begin, + cursor_end, + }); + } + + pub fn send_commit_string(&self, text: Option<&str>) { + self.client.event(CommitString { + self_id: self.id, + text, + }); + } + + pub fn send_delete_surrounding_text(&self, before_length: u32, after_length: u32) { + self.client.event(DeleteSurroundingText { + self_id: self.id, + before_length, + after_length, + }); + } + + pub fn send_done(&self) { + self.client.event(Done { + self_id: self.id, + serial: self.num_commits.get(), + }); + } + + fn do_enable(self: &Rc) { + if self.seat.text_input.is_some() { + return; + } + let Some(surface) = self.seat.keyboard_node.get().node_into_surface() else { + return; + }; + if surface.client.id != self.client.id { + return; + } + self.seat.text_input.set(Some(self.clone())); + self.seat + .create_text_input_connection(TextConnectReason::TextInputEnabled); + } + + fn do_disable(&self) { + if let Some(con) = self.connection.take() { + con.disconnect(TextDisconnectReason::TextInputDisabled); + self.seat.text_input.take(); + } + } +} + +#[derive(Default)] +struct State { + enabled: bool, + surrounding_text: (String, u32, u32), + text_change_cause: u32, + content_type: (u32, u32), + cursor_rectangle: Rect, +} + +#[derive(Default)] +struct Pending { + enabled: Option, + cursor_rect: Option, + content_type: Option<(u32, u32)>, + text_change_cause: Option, + surrounding_text: Option<(String, u32, u32)>, +} + +impl ZwpTextInputV3RequestHandler for ZwpTextInputV3 { + type Error = ZwpTextInputV3Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } + + fn enable(&self, _req: Enable, _slf: &Rc) -> Result<(), Self::Error> { + self.pending.borrow_mut().enabled = Some(true); + Ok(()) + } + + fn disable(&self, _req: Disable, _slf: &Rc) -> Result<(), Self::Error> { + self.pending.borrow_mut().enabled = Some(false); + Ok(()) + } + + fn set_surrounding_text( + &self, + req: SetSurroundingText<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + if req.text.len() > MAX_TEXT_SIZE { + return Err(ZwpTextInputV3Error::TooLarge); + } + if !req.text.is_char_boundary(req.cursor as usize) { + return Err(ZwpTextInputV3Error::CursorNotCharBoundary); + } + if !req.text.is_char_boundary(req.anchor as usize) { + return Err(ZwpTextInputV3Error::AnchorNotCharBoundary); + } + self.pending.borrow_mut().surrounding_text = + Some((req.text.to_string(), req.cursor as _, req.anchor as _)); + Ok(()) + } + + fn set_text_change_cause( + &self, + req: SetTextChangeCause, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.pending.borrow_mut().text_change_cause = Some(req.cause); + Ok(()) + } + + fn set_content_type(&self, req: SetContentType, _slf: &Rc) -> Result<(), Self::Error> { + self.pending.borrow_mut().content_type = Some((req.hint, req.purpose)); + Ok(()) + } + + fn set_cursor_rectangle( + &self, + req: SetCursorRectangle, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let Some(rect) = Rect::new_sized(req.x, req.y, req.width, req.height) else { + return Err(ZwpTextInputV3Error::InvalidRectangle); + }; + self.pending.borrow_mut().cursor_rect = Some(rect); + Ok(()) + } + + fn commit(&self, _req: Commit, slf: &Rc) -> Result<(), Self::Error> { + self.num_commits.fetch_add(1); + let pending = self.pending.take(); + let state = &mut *self.state.borrow_mut(); + let mut sent_any = false; + if let Some(val) = pending.enabled { + sent_any = true; + if val { + mem::take(state); + if let Some(con) = self.connection.get() { + con.input_method.activate(); + } else { + slf.do_enable(); + } + } else { + self.do_disable(); + } + state.enabled = val; + } + let con = self.connection.get(); + if let Some(val) = pending.cursor_rect { + if state.cursor_rectangle != val { + if let Some(con) = &con { + for (_, popup) in &con.input_method.popups { + popup.schedule_positioning(); + } + } + } + state.cursor_rectangle = val; + } + if let Some(val) = pending.content_type { + if let Some(con) = &con { + sent_any = true; + con.input_method.send_content_type(val.0, val.1); + } + state.content_type = val; + } + if let Some(val) = pending.text_change_cause { + if let Some(con) = &con { + sent_any = true; + con.input_method.send_text_change_cause(val); + } + state.text_change_cause = val; + } + if let Some(val) = pending.surrounding_text { + if let Some(con) = &con { + sent_any = true; + con.input_method.send_surrounding_text(&val.0, val.1, val.2); + } + state.surrounding_text = val; + } + if sent_any { + if let Some(con) = &con { + con.input_method.send_done(); + } + } + Ok(()) + } +} + +object_base! { + self = ZwpTextInputV3; + version = self.version; +} + +impl Object for ZwpTextInputV3 { + fn break_loops(&self) { + self.detach(); + } +} + +simple_add_obj!(ZwpTextInputV3); + +#[derive(Debug, Error)] +pub enum ZwpTextInputV3Error { + #[error(transparent)] + ClientError(Box), + #[error("Rectangle is invalid")] + InvalidRectangle, + #[error("The cursor is not at a char boundary")] + CursorNotCharBoundary, + #[error("The anchor is not at a char boundary")] + AnchorNotCharBoundary, + #[error("Text is larger than {} bytes", MAX_TEXT_SIZE)] + TooLarge, +} +efrom!(ZwpTextInputV3Error, ClientError); diff --git a/src/ifs/wl_seat/wl_keyboard.rs b/src/ifs/wl_seat/wl_keyboard.rs index b104404b..bce6aca6 100644 --- a/src/ifs/wl_seat/wl_keyboard.rs +++ b/src/ifs/wl_seat/wl_keyboard.rs @@ -4,9 +4,9 @@ use { ifs::wl_seat::WlSeat, leaks::Tracker, object::{Object, Version}, - utils::{errorfmt::ErrorFmt, oserror::OsError}, + utils::errorfmt::ErrorFmt, wire::{wl_keyboard::*, WlKeyboardId, WlSurfaceId}, - xkbcommon::{KeyboardState, KeyboardStateId, ModifierState}, + xkbcommon::{KeyboardState, KeyboardStateId, ModifierState, XkbCommonError}, }, std::{cell::Cell, rc::Rc}, thiserror::Error, @@ -180,9 +180,7 @@ simple_add_obj!(WlKeyboard); pub enum WlKeyboardError { #[error(transparent)] ClientError(Box), - #[error("Could not create a keymap memfd")] - KeymapMemfd(#[source] OsError), - #[error("Could not copy the keymap")] - KeymapCopy(#[source] OsError), + #[error(transparent)] + XkbCommonError(#[from] XkbCommonError), } efrom!(WlKeyboardError, ClientError); diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 8e996c94..63f12004 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -12,6 +12,7 @@ pub mod xdg_surface; pub mod xwayland_shell_v1; pub mod zwlr_layer_surface_v1; pub mod zwp_idle_inhibitor_v1; +pub mod zwp_input_popup_surface_v2; use { crate::{ @@ -24,8 +25,9 @@ use { wl_buffer::WlBuffer, wl_callback::WlCallback, wl_seat::{ - wl_pointer::PendingScroll, zwp_pointer_constraints_v1::SeatConstraint, Dnd, - NodeSeatState, SeatId, WlSeatGlobal, + text_input::TextInputConnection, wl_pointer::PendingScroll, + zwp_pointer_constraints_v1::SeatConstraint, Dnd, NodeSeatState, SeatId, + WlSeatGlobal, }, wl_surface::{ commit_timeline::{ClearReason, CommitTimeline, CommitTimelineError}, @@ -104,6 +106,7 @@ pub enum SurfaceRole { ZwlrLayerSurface, XSurface, ExtSessionLockSurface, + InputPopup, } impl SurfaceRole { @@ -117,6 +120,7 @@ impl SurfaceRole { SurfaceRole::ZwlrLayerSurface => "zwlr_layer_surface", SurfaceRole::XSurface => "xwayland surface", SurfaceRole::ExtSessionLockSurface => "ext_session_lock_surface", + SurfaceRole::InputPopup => "input_popup_surface", } } } @@ -249,6 +253,7 @@ pub struct WlSurface { commit_timeline: CommitTimeline, alpha_modifier: CloneCell>>, alpha: Cell>, + pub text_input_connections: SmallMap, 1>, } impl Debug for WlSurface { @@ -533,6 +538,7 @@ impl WlSurface { commit_timeline: client.commit_timelines.create_timeline(), alpha_modifier: Default::default(), alpha: Default::default(), + text_input_connections: Default::default(), } } @@ -604,6 +610,11 @@ impl WlSurface { .set_absolute_position(x1 + pos.x1(), y1 + pos.y1()); } } + for (_, con) in &self.text_input_connections { + for (_, popup) in &con.input_method.popups { + popup.schedule_positioning(); + } + } } pub fn add_presentation_feedback(&self, fb: &Rc) { diff --git a/src/ifs/wl_surface/zwp_input_popup_surface_v2.rs b/src/ifs/wl_surface/zwp_input_popup_surface_v2.rs new file mode 100644 index 00000000..022a35c6 --- /dev/null +++ b/src/ifs/wl_surface/zwp_input_popup_surface_v2.rs @@ -0,0 +1,174 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::{ + wl_seat::text_input::zwp_input_method_v2::ZwpInputMethodV2, + wl_surface::{SurfaceExt, SurfaceRole, WlSurface, WlSurfaceError}, + }, + leaks::Tracker, + object::{Object, Version}, + rect::Rect, + state::State, + wire::{zwp_input_popup_surface_v2::*, WlSurfaceId, ZwpInputPopupSurfaceV2Id}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct ZwpInputPopupSurfaceV2 { + pub id: ZwpInputPopupSurfaceV2Id, + pub client: Rc, + pub input_method: Rc, + pub surface: Rc, + pub version: Version, + pub tracker: Tracker, + pub positioning_scheduled: Cell, +} + +impl SurfaceExt for ZwpInputPopupSurfaceV2 { + fn after_apply_commit(self: Rc) { + self.update_visible(); + if self.surface.visible.get() { + self.schedule_positioning(); + } + } +} + +pub async fn input_popup_positioning(state: Rc) { + loop { + let popup = state.pending_input_popup_positioning.pop().await; + if popup.positioning_scheduled.get() { + popup.position(); + } + } +} + +impl ZwpInputPopupSurfaceV2 { + pub fn update_visible(self: &Rc) { + let was_visible = self.surface.visible.get(); + let is_visible = self.surface.buffer.is_some() + && self.input_method.connection.is_some() + && self.client.state.root_visible(); + self.surface.set_visible(is_visible); + if was_visible || is_visible { + self.client.state.damage(); + } + if !was_visible && is_visible { + self.schedule_positioning(); + } + } + + pub fn schedule_positioning(self: &Rc) { + if self.surface.visible.get() { + if !self.positioning_scheduled.replace(true) { + self.client + .state + .pending_input_popup_positioning + .push(self.clone()); + } + } + } + + fn position(&self) { + self.positioning_scheduled.set(false); + if !self.surface.visible.get() { + return; + } + let Some(con) = self.input_method.connection.get() else { + log::warn!("Popup has no connection but is visible"); + return; + }; + let output = con.surface.output.get().global.pos.get(); + let surface_rect = con.surface.buffer_abs_pos.get(); + let cursor_rect = con + .text_input + .cursor_rect() + .move_(surface_rect.x1(), surface_rect.y1()); + let extents = self.surface.extents.get(); + let mut rect = extents.at_point(cursor_rect.x1(), cursor_rect.y2()); + let overflow = output.get_overflow(&rect); + if overflow.right > 0 { + let dx = -overflow.right.min(rect.width()); + let rect2 = rect.move_(dx, 0); + if !output.get_overflow(&rect2).x_overflow() { + rect = rect2; + } + } + if overflow.bottom > 0 { + let rect2 = rect.move_(0, -(cursor_rect.height() + rect.height())); + if !output.get_overflow(&rect2).y_overflow() { + rect = rect2; + } + } + self.surface.buffer_abs_pos.set( + self.surface + .buffer_abs_pos + .get() + .at_point(rect.x1() - extents.x1(), rect.y1() - extents.y1()), + ); + } + + pub fn install(self: &Rc) -> Result<(), ZwpInputPopupSurfaceV2Error> { + self.surface.set_role(SurfaceRole::InputPopup)?; + if self.surface.ext.get().is_some() { + return Err(ZwpInputPopupSurfaceV2Error::AlreadyAttached( + self.surface.id, + )); + } + self.surface.ext.set(self.clone()); + self.input_method.popups.insert(self.id, self.clone()); + Ok(()) + } + + #[allow(dead_code)] + pub fn send_text_input_rectangle(&self, rect: Rect) { + self.client.event(TextInputRectangle { + self_id: self.id, + x: rect.x1(), + y: rect.y1(), + width: rect.width(), + height: rect.height(), + }); + } + + fn detach(&self) { + self.surface.destroy_node(); + self.surface.unset_ext(); + self.input_method.popups.remove(&self.id); + } +} + +impl ZwpInputPopupSurfaceV2RequestHandler for ZwpInputPopupSurfaceV2 { + type Error = ZwpInputPopupSurfaceV2Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = ZwpInputPopupSurfaceV2; + version = self.version; +} + +impl Object for ZwpInputPopupSurfaceV2 { + fn break_loops(&self) { + self.detach(); + } +} + +simple_add_obj!(ZwpInputPopupSurfaceV2); + +#[derive(Debug, Error)] +pub enum ZwpInputPopupSurfaceV2Error { + #[error(transparent)] + ClientError(Box), + #[error(transparent)] + WlSurfaceError(Box), + #[error("Surface {0} cannot be turned into a zwp_input_popup_surface_v2 because it already has an attached zwp_input_popup_surface_v2")] + AlreadyAttached(WlSurfaceId), +} +efrom!(ZwpInputPopupSurfaceV2Error, WlSurfaceError); +efrom!(ZwpInputPopupSurfaceV2Error, ClientError); diff --git a/src/it/test_client.rs b/src/it/test_client.rs index bd178f9f..f08ac1a7 100644 --- a/src/it/test_client.rs +++ b/src/it/test_client.rs @@ -15,7 +15,7 @@ use { test_xdg_activation::TestXdgActivation, test_xdg_base::TestXdgWmBase, }, test_transport::TestTransport, - test_utils::test_window::TestWindow, + test_utils::{test_surface_ext::TestSurfaceExt, test_window::TestWindow}, testrun::TestRun, }, theme::Color, @@ -124,21 +124,24 @@ impl TestClient { Ok(()) } - pub async fn create_window(&self) -> Result, TestError> { + pub async fn create_surface_ext(&self) -> Result { let surface = self.comp.create_surface().await?; let viewport = self.viewporter.get_viewport(&surface)?; - let xdg = self.xdg.create_xdg_surface(surface.id).await?; - let tl = xdg.create_toplevel().await?; - surface.commit()?; - self.sync().await; - Ok(Rc::new(TestWindow { + Ok(TestSurfaceExt { surface, spbm: self.spbm.clone(), viewport, - xdg, - tl, color: Cell::new(Color::SOLID_BLACK), - })) + }) + } + + pub async fn create_window(&self) -> Result, TestError> { + let surface = self.create_surface_ext().await?; + let xdg = self.xdg.create_xdg_surface(surface.surface.id).await?; + let tl = xdg.create_toplevel().await?; + surface.surface.commit()?; + self.sync().await; + Ok(Rc::new(TestWindow { surface, xdg, tl })) } } diff --git a/src/it/test_ifs.rs b/src/it/test_ifs.rs index 65c57bd4..ebd29ce9 100644 --- a/src/it/test_ifs.rs +++ b/src/it/test_ifs.rs @@ -20,6 +20,10 @@ pub mod test_dmabuf; pub mod test_dmabuf_feedback; pub mod test_ext_foreign_toplevel_handle; pub mod test_ext_foreign_toplevel_list; +pub mod test_input_method; +pub mod test_input_method_keyboard_grab; +pub mod test_input_method_manager; +pub mod test_input_popup_surface; pub mod test_jay_compositor; pub mod test_keyboard; pub mod test_pointer; @@ -37,6 +41,8 @@ pub mod test_surface; pub mod test_syncobj_manager; pub mod test_syncobj_surface; pub mod test_syncobj_timeline; +pub mod test_text_input; +pub mod test_text_input_manager; pub mod test_toplevel_drag; pub mod test_toplevel_drag_manager; pub mod test_viewport; diff --git a/src/it/test_ifs/test_input_method.rs b/src/it/test_ifs/test_input_method.rs new file mode 100644 index 00000000..1d97182b --- /dev/null +++ b/src/it/test_ifs/test_input_method.rs @@ -0,0 +1,119 @@ +use { + crate::{ + it::{ + test_error::{TestError, TestResult}, + test_ifs::{ + test_input_method_keyboard_grab::TestInputMethodKeyboardGrab, + test_input_popup_surface::TestInputPopupSurface, test_surface::TestSurface, + }, + test_object::TestObject, + test_transport::TestTransport, + test_utils::test_expected_event::TEEH, + testrun::ParseFull, + }, + utils::{buffd::MsgParser, numcell::NumCell}, + wire::{zwp_input_method_v2::*, ZwpInputMethodV2Id}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestInputMethod { + pub id: ZwpInputMethodV2Id, + pub tran: Rc, + pub destroyed: Cell, + pub activate: TEEH, + pub done: TEEH<()>, + pub done_received: NumCell, +} + +impl TestInputMethod { + pub fn commit_string(&self, s: &str) -> TestResult { + self.tran.send(CommitString { + self_id: self.id, + text: s, + }) + } + + pub fn commit(&self) -> TestResult { + self.tran.send(Commit { + self_id: self.id, + serial: self.done_received.get(), + }) + } + + #[allow(dead_code)] + pub fn grab(&self) -> TestResult> { + let obj = Rc::new(TestInputMethodKeyboardGrab { + id: self.tran.id(), + tran: self.tran.clone(), + destroyed: Cell::new(false), + keymap: Rc::new(Default::default()), + key: Rc::new(Default::default()), + modifiers: Rc::new(Default::default()), + repeat_info: Rc::new(Default::default()), + }); + self.tran.add_obj(obj.clone())?; + self.tran.send(GrabKeyboard { + self_id: self.id, + keyboard: obj.id, + })?; + Ok(obj) + } + + pub fn get_popup(&self, surface: &TestSurface) -> TestResult> { + let obj = Rc::new(TestInputPopupSurface { + id: self.tran.id(), + tran: self.tran.clone(), + destroyed: Cell::new(false), + }); + self.tran.add_obj(obj.clone())?; + self.tran.send(GetInputPopupSurface { + self_id: self.id, + id: obj.id, + surface: surface.id, + })?; + Ok(obj) + } + + pub fn destroy(&self) -> Result<(), TestError> { + if !self.destroyed.replace(true) { + self.tran.send(Destroy { self_id: self.id })?; + } + Ok(()) + } + + fn handle_activate(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Activate::parse_full(parser)?; + self.activate.push(true); + Ok(()) + } + + fn handle_deactivate(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Deactivate::parse_full(parser)?; + self.activate.push(false); + Ok(()) + } + + fn handle_done(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Done::parse_full(parser)?; + self.done.push(()); + self.done_received.fetch_add(1); + Ok(()) + } +} + +impl Drop for TestInputMethod { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +test_object! { + TestInputMethod, ZwpInputMethodV2; + + ACTIVATE => handle_activate, + DEACTIVATE => handle_deactivate, + DONE => handle_done, +} + +impl TestObject for TestInputMethod {} diff --git a/src/it/test_ifs/test_input_method_keyboard_grab.rs b/src/it/test_ifs/test_input_method_keyboard_grab.rs new file mode 100644 index 00000000..7a486081 --- /dev/null +++ b/src/it/test_ifs/test_input_method_keyboard_grab.rs @@ -0,0 +1,71 @@ +use { + crate::{ + it::{ + test_error::TestError, test_object::TestObject, test_transport::TestTransport, + test_utils::test_expected_event::TEEH, testrun::ParseFull, + }, + utils::buffd::MsgParser, + wire::{zwp_input_method_keyboard_grab_v2::*, ZwpInputMethodKeyboardGrabV2Id}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestInputMethodKeyboardGrab { + pub id: ZwpInputMethodKeyboardGrabV2Id, + pub tran: Rc, + pub destroyed: Cell, + pub keymap: TEEH, + pub key: TEEH, + pub modifiers: TEEH, + pub repeat_info: TEEH, +} + +impl TestInputMethodKeyboardGrab { + pub fn destroy(&self) -> Result<(), TestError> { + if !self.destroyed.replace(true) { + self.tran.send(Release { self_id: self.id })?; + } + Ok(()) + } + + fn handle_keymap(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Keymap::parse_full(parser)?; + self.keymap.push(ev); + Ok(()) + } + + fn handle_key(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Key::parse_full(parser)?; + self.key.push(ev); + Ok(()) + } + + fn handle_modifiers(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Modifiers::parse_full(parser)?; + self.modifiers.push(ev); + Ok(()) + } + + fn handle_repeat_info(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = RepeatInfo::parse_full(parser)?; + self.repeat_info.push(ev); + Ok(()) + } +} + +impl Drop for TestInputMethodKeyboardGrab { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +test_object! { + TestInputMethodKeyboardGrab, ZwpInputMethodKeyboardGrabV2; + + KEYMAP => handle_keymap, + KEY => handle_key, + MODIFIERS => handle_modifiers, + REPEAT_INFO => handle_repeat_info, +} + +impl TestObject for TestInputMethodKeyboardGrab {} diff --git a/src/it/test_ifs/test_input_method_manager.rs b/src/it/test_ifs/test_input_method_manager.rs new file mode 100644 index 00000000..2dd031d7 --- /dev/null +++ b/src/it/test_ifs/test_input_method_manager.rs @@ -0,0 +1,50 @@ +use { + crate::{ + it::{ + test_error::TestResult, + test_ifs::{test_input_method::TestInputMethod, test_seat::TestSeat}, + test_object::TestObject, + test_transport::TestTransport, + }, + wire::{zwp_input_method_manager_v2::GetInputMethod, ZwpInputMethodManagerV2Id}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestInputMethodManager { + pub id: ZwpInputMethodManagerV2Id, + pub tran: Rc, +} + +impl TestInputMethodManager { + pub fn new(tran: &Rc) -> Self { + Self { + id: tran.id(), + tran: tran.clone(), + } + } + + pub fn get_input_method(&self, seat: &TestSeat) -> TestResult> { + let obj = Rc::new(TestInputMethod { + id: self.tran.id(), + tran: self.tran.clone(), + destroyed: Cell::new(false), + activate: Rc::new(Default::default()), + done: Rc::new(Default::default()), + done_received: Default::default(), + }); + self.tran.add_obj(obj.clone())?; + self.tran.send(GetInputMethod { + self_id: self.id, + seat: seat.id, + input_method: obj.id, + })?; + Ok(obj) + } +} + +test_object! { + TestInputMethodManager, ZwpInputMethodManagerV2; +} + +impl TestObject for TestInputMethodManager {} diff --git a/src/it/test_ifs/test_input_popup_surface.rs b/src/it/test_ifs/test_input_popup_surface.rs new file mode 100644 index 00000000..c97f2606 --- /dev/null +++ b/src/it/test_ifs/test_input_popup_surface.rs @@ -0,0 +1,34 @@ +use { + crate::{ + it::{test_error::TestError, test_object::TestObject, test_transport::TestTransport}, + wire::{zwp_input_popup_surface_v2::*, ZwpInputPopupSurfaceV2Id}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestInputPopupSurface { + pub id: ZwpInputPopupSurfaceV2Id, + pub tran: Rc, + pub destroyed: Cell, +} + +impl TestInputPopupSurface { + pub fn destroy(&self) -> Result<(), TestError> { + if !self.destroyed.replace(true) { + self.tran.send(Destroy { self_id: self.id })?; + } + Ok(()) + } +} + +impl Drop for TestInputPopupSurface { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +test_object! { + TestInputPopupSurface, ZwpInputPopupSurfaceV2; +} + +impl TestObject for TestInputPopupSurface {} diff --git a/src/it/test_ifs/test_registry.rs b/src/it/test_ifs/test_registry.rs index 3fb385b4..5d58d8db 100644 --- a/src/it/test_ifs/test_registry.rs +++ b/src/it/test_ifs/test_registry.rs @@ -11,9 +11,11 @@ use { test_data_control_manager::TestDataControlManager, test_data_device_manager::TestDataDeviceManager, test_dmabuf::TestDmabuf, test_ext_foreign_toplevel_list::TestExtForeignToplevelList, + test_input_method_manager::TestInputMethodManager, test_jay_compositor::TestJayCompositor, test_shm::TestShm, test_single_pixel_buffer_manager::TestSinglePixelBufferManager, test_subcompositor::TestSubcompositor, test_syncobj_manager::TestSyncobjManager, + test_text_input_manager::TestTextInputManager, test_toplevel_drag_manager::TestToplevelDragManager, test_viewporter::TestViewporter, test_virtual_keyboard_manager::TestVirtualKeyboardManager, @@ -54,6 +56,8 @@ pub struct TestRegistrySingletons { pub xdg_toplevel_drag_manager_v1: u32, pub wp_alpha_modifier_v1: u32, pub zwp_virtual_keyboard_manager_v1: u32, + pub zwp_input_method_manager_v2: u32, + pub zwp_text_input_manager_v3: u32, } pub struct TestRegistry { @@ -79,6 +83,8 @@ pub struct TestRegistry { pub drag_manager: CloneCell>>, pub alpha_modifier: CloneCell>>, pub virtual_keyboard_manager: CloneCell>>, + pub input_method_manager: CloneCell>>, + pub text_input_manager: CloneCell>>, pub seats: CopyHashMap>, } @@ -148,6 +154,8 @@ impl TestRegistry { xdg_toplevel_drag_manager_v1, wp_alpha_modifier_v1, zwp_virtual_keyboard_manager_v1, + zwp_input_method_manager_v2, + zwp_text_input_manager_v3, }; self.singletons.set(Some(singletons.clone())); Ok(singletons) @@ -249,6 +257,20 @@ impl TestRegistry { 1, TestVirtualKeyboardManager ); + create_singleton!( + get_input_method_manager, + input_method_manager, + zwp_input_method_manager_v2, + 1, + TestInputMethodManager + ); + create_singleton!( + get_text_input_manager, + text_input_manager, + zwp_text_input_manager_v3, + 1, + TestTextInputManager + ); pub fn bind( &self, diff --git a/src/it/test_ifs/test_text_input.rs b/src/it/test_ifs/test_text_input.rs new file mode 100644 index 00000000..80d9ad4a --- /dev/null +++ b/src/it/test_ifs/test_text_input.rs @@ -0,0 +1,97 @@ +use { + crate::{ + it::{ + test_error::{TestError, TestResult}, + test_object::TestObject, + test_transport::TestTransport, + test_utils::test_expected_event::TEEH, + testrun::ParseFull, + }, + utils::buffd::MsgParser, + wire::{zwp_text_input_v3::*, ZwpTextInputV3Id}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestTextInput { + pub id: ZwpTextInputV3Id, + pub tran: Rc, + pub destroyed: Cell, + pub enter: TEEH, + pub leave: TEEH, + pub commit_string: TEEH, + pub done: TEEH, +} + +impl TestTextInput { + pub fn destroy(&self) -> Result<(), TestError> { + if !self.destroyed.replace(true) { + self.tran.send(Destroy { self_id: self.id })?; + } + Ok(()) + } + + pub fn enable(&self) -> TestResult { + self.tran.send(Enable { self_id: self.id }) + } + + pub fn disable(&self) -> TestResult { + self.tran.send(Disable { self_id: self.id }) + } + + pub fn set_cursor_rectangle(&self, x: i32, y: i32, width: i32, height: i32) -> TestResult { + self.tran.send(SetCursorRectangle { + self_id: self.id, + x, + y, + width, + height, + }) + } + + pub fn commit(&self) -> TestResult { + self.tran.send(Commit { self_id: self.id }) + } + + fn handle_enter(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Enter::parse_full(parser)?; + self.enter.push(ev); + Ok(()) + } + + fn handle_leave(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Leave::parse_full(parser)?; + self.leave.push(ev); + Ok(()) + } + + fn handle_commit_string(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = CommitString::parse_full(parser)?; + self.commit_string + .push(ev.text.unwrap_or_default().to_string()); + Ok(()) + } + + fn handle_done(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Done::parse_full(parser)?; + self.done.push(ev); + Ok(()) + } +} + +impl Drop for TestTextInput { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +test_object! { + TestTextInput, ZwpTextInputV3; + + ENTER => handle_enter, + LEAVE => handle_leave, + COMMIT_STRING => handle_commit_string, + DONE => handle_done, +} + +impl TestObject for TestTextInput {} diff --git a/src/it/test_ifs/test_text_input_manager.rs b/src/it/test_ifs/test_text_input_manager.rs new file mode 100644 index 00000000..40ba33f2 --- /dev/null +++ b/src/it/test_ifs/test_text_input_manager.rs @@ -0,0 +1,51 @@ +use { + crate::{ + it::{ + test_error::TestResult, + test_ifs::{test_seat::TestSeat, test_text_input::TestTextInput}, + test_object::TestObject, + test_transport::TestTransport, + }, + wire::{zwp_text_input_manager_v3::*, ZwpTextInputManagerV3Id}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestTextInputManager { + pub id: ZwpTextInputManagerV3Id, + pub tran: Rc, +} + +impl TestTextInputManager { + pub fn new(tran: &Rc) -> Self { + Self { + id: tran.id(), + tran: tran.clone(), + } + } + + pub fn get_text_input(&self, seat: &TestSeat) -> TestResult> { + let obj = Rc::new(TestTextInput { + id: self.tran.id(), + tran: self.tran.clone(), + destroyed: Cell::new(false), + enter: Rc::new(Default::default()), + leave: Rc::new(Default::default()), + commit_string: Rc::new(Default::default()), + done: Rc::new(Default::default()), + }); + self.tran.add_obj(obj.clone())?; + self.tran.send(GetTextInput { + self_id: self.id, + id: obj.id, + seat: seat.id, + })?; + Ok(obj) + } +} + +test_object! { + TestTextInputManager, ZwpTextInputManagerV3; +} + +impl TestObject for TestTextInputManager {} diff --git a/src/it/test_transport.rs b/src/it/test_transport.rs index d64b7a19..d0bb3063 100644 --- a/src/it/test_transport.rs +++ b/src/it/test_transport.rs @@ -70,6 +70,8 @@ impl TestTransport { drag_manager: Default::default(), alpha_modifier: Default::default(), virtual_keyboard_manager: Default::default(), + input_method_manager: Default::default(), + text_input_manager: Default::default(), seats: Default::default(), }); self.send(wl_display::GetRegistry { diff --git a/src/it/test_utils.rs b/src/it/test_utils.rs index 2cdb494f..cb047de7 100644 --- a/src/it/test_utils.rs +++ b/src/it/test_utils.rs @@ -3,6 +3,7 @@ pub mod test_expected_event; pub mod test_object_ext; pub mod test_ouput_node_ext; pub mod test_rect_ext; +pub mod test_surface_ext; pub mod test_toplevel_node_ext; pub mod test_window; pub mod test_workspace_node_ext; diff --git a/src/it/test_utils/test_surface_ext.rs b/src/it/test_utils/test_surface_ext.rs new file mode 100644 index 00000000..315b3576 --- /dev/null +++ b/src/it/test_utils/test_surface_ext.rs @@ -0,0 +1,45 @@ +use { + crate::{ + it::{ + test_error::TestError, + test_ifs::{ + test_single_pixel_buffer_manager::TestSinglePixelBufferManager, + test_surface::TestSurface, test_viewport::TestViewport, + }, + }, + theme::Color, + }, + std::{cell::Cell, ops::Deref, rc::Rc}, +}; + +pub struct TestSurfaceExt { + pub surface: Rc, + pub spbm: Rc, + pub viewport: Rc, + pub color: Cell, +} + +impl Deref for TestSurfaceExt { + type Target = TestSurface; + + fn deref(&self) -> &Self::Target { + &self.surface + } +} + +impl TestSurfaceExt { + pub async fn map(&self, width: i32, height: i32) -> Result<(), TestError> { + let buffer = self.spbm.create_buffer(self.color.get())?; + self.surface.attach(buffer.id)?; + self.viewport.set_source(0, 0, 1, 1)?; + self.viewport.set_destination(width, height)?; + self.surface.commit()?; + self.surface.tran.sync().await; + Ok(()) + } + + #[allow(dead_code)] + pub fn set_color(&self, r: u8, g: u8, b: u8, a: u8) { + self.color.set(Color::from_rgba_straight(r, g, b, a)); + } +} diff --git a/src/it/test_utils/test_window.rs b/src/it/test_utils/test_window.rs index 2363f78a..70dc7644 100644 --- a/src/it/test_utils/test_window.rs +++ b/src/it/test_utils/test_window.rs @@ -1,37 +1,24 @@ use { - crate::{ - it::{ - test_error::{TestError, TestResult}, - test_ifs::{ - test_single_pixel_buffer_manager::TestSinglePixelBufferManager, - test_surface::TestSurface, test_viewport::TestViewport, - test_xdg_surface::TestXdgSurface, test_xdg_toplevel::TestXdgToplevel, - }, - }, - theme::Color, + crate::it::{ + test_error::{TestError, TestResult}, + test_ifs::{test_xdg_surface::TestXdgSurface, test_xdg_toplevel::TestXdgToplevel}, + test_utils::test_surface_ext::TestSurfaceExt, }, - std::{cell::Cell, rc::Rc}, + std::rc::Rc, }; pub struct TestWindow { - pub surface: Rc, - pub spbm: Rc, - pub viewport: Rc, + pub surface: TestSurfaceExt, pub xdg: Rc, pub tl: Rc, - pub color: Cell, } impl TestWindow { pub async fn map(&self) -> Result<(), TestError> { - let buffer = self.spbm.create_buffer(self.color.get())?; - self.surface.attach(buffer.id)?; - self.viewport.set_source(0, 0, 1, 1)?; - self.viewport - .set_destination(self.tl.core.width.get(), self.tl.core.height.get())?; self.xdg.ack_configure(self.xdg.last_serial.get())?; - self.surface.commit()?; - self.surface.tran.sync().await; + self.surface + .map(self.tl.core.width.get(), self.tl.core.height.get()) + .await?; Ok(()) } @@ -40,8 +27,7 @@ impl TestWindow { self.map().await } - #[allow(dead_code)] pub fn set_color(&self, r: u8, g: u8, b: u8, a: u8) { - self.color.set(Color::from_rgba_straight(r, g, b, a)); + self.surface.set_color(r, g, b, a); } } diff --git a/src/it/tests.rs b/src/it/tests.rs index e669a77a..acc40f31 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -72,6 +72,7 @@ mod t0037_toplevel_drag; mod t0038_subsurface_parent_state; mod t0039_alpha_modifier; mod t0040_virtual_keyboard; +mod t0041_input_method; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -131,5 +132,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0038_subsurface_parent_state, t0039_alpha_modifier, t0040_virtual_keyboard, + t0041_input_method, } } diff --git a/src/it/tests/t0041_input_method.rs b/src/it/tests/t0041_input_method.rs new file mode 100644 index 00000000..e8575303 --- /dev/null +++ b/src/it/tests/t0041_input_method.rs @@ -0,0 +1,136 @@ +use { + crate::{ + it::{ + test_client::{DefaultSeat, TestClient}, + test_error::TestResult, + test_ifs::{ + test_input_method::TestInputMethod, + test_input_popup_surface::TestInputPopupSurface, test_text_input::TestTextInput, + }, + test_utils::{ + test_expected_event::TestExpectedEvent, test_surface_ext::TestSurfaceExt, + test_window::TestWindow, + }, + testrun::TestRun, + }, + wire::zwp_text_input_v3, + }, + std::rc::Rc, +}; + +testcase!(); + +async fn test(run: Rc) -> TestResult { + let _ds = run.create_default_setup().await?; + + let consumer = create_consumer(&run).await?; + let supplier = create_supplier(&run).await?; + + consumer.client.compare_screenshot("1", false).await?; + + supplier.client.sync().await; + tassert!(supplier.activate.next().is_err()); + + consumer.text.enable()?; + consumer.text.set_cursor_rectangle(100, 100, 100, 100)?; + consumer.text.commit()?; + consumer.client.sync().await; + + supplier.client.sync().await; + tassert!(matches!(supplier.activate.next(), Ok(true))); + tassert!(supplier.done.next().is_ok()); + + consumer.client.compare_screenshot("1", false).await?; + + supplier.surface.commit()?; + supplier.client.sync().await; + + consumer.client.compare_screenshot("2", false).await?; + + supplier.im.commit_string("hello world")?; + supplier.im.commit()?; + supplier.client.sync().await; + + consumer.client.sync().await; + tassert_eq!( + consumer.commit_string.next().expect("commit string"), + "hello world" + ); + tassert!(consumer.done.next().is_ok()); + + consumer.text.disable()?; + consumer.text.commit()?; + consumer.client.sync().await; + + consumer.client.compare_screenshot("3", false).await?; + + Ok(()) +} + +struct Consumer { + client: Rc, + _seat: DefaultSeat, + _window: Rc, + text: Rc, + _enter: TestExpectedEvent, + _leave: TestExpectedEvent, + commit_string: TestExpectedEvent, + done: TestExpectedEvent, +} + +async fn create_consumer(run: &Rc) -> TestResult { + let client = run.create_client().await?; + let seat = client.get_default_seat().await?; + let text = client + .registry + .get_text_input_manager() + .await? + .get_text_input(&seat.seat)?; + let window = client.create_window().await?; + window.map2().await?; + client.sync().await; + Ok(Consumer { + _enter: text.enter.expect()?, + _leave: text.leave.expect()?, + commit_string: text.commit_string.expect()?, + done: text.done.expect()?, + client, + _seat: seat, + _window: window, + text, + }) +} + +struct Supplier { + client: Rc, + _seat: DefaultSeat, + im: Rc, + surface: TestSurfaceExt, + _popup: Rc, + activate: TestExpectedEvent, + done: TestExpectedEvent<()>, +} + +async fn create_supplier(run: &Rc) -> TestResult { + let client = run.create_client().await?; + let seat = client.get_default_seat().await?; + let im = client + .registry + .get_input_method_manager() + .await? + .get_input_method(&seat.seat)?; + let surface = client.create_surface_ext().await?; + surface.set_color(255, 0, 0, 255); + surface.map(100, 100).await?; + let popup = im.get_popup(&surface)?; + client.sync().await; + Ok(Supplier { + activate: im.activate.expect()?, + done: im.done.expect()?, + client, + _seat: seat, + im, + surface, + _popup: popup, + }) +} diff --git a/src/it/tests/t0041_input_method/screenshot_1.qoi b/src/it/tests/t0041_input_method/screenshot_1.qoi new file mode 100644 index 00000000..d25fcf64 Binary files /dev/null and b/src/it/tests/t0041_input_method/screenshot_1.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_2.qoi b/src/it/tests/t0041_input_method/screenshot_2.qoi new file mode 100644 index 00000000..7f93231a Binary files /dev/null and b/src/it/tests/t0041_input_method/screenshot_2.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_3.qoi b/src/it/tests/t0041_input_method/screenshot_3.qoi new file mode 100644 index 00000000..d25fcf64 Binary files /dev/null and b/src/it/tests/t0041_input_method/screenshot_3.qoi differ diff --git a/src/screenshoter.rs b/src/screenshoter.rs index 28e80129..4f9454c9 100644 --- a/src/screenshoter.rs +++ b/src/screenshoter.rs @@ -75,9 +75,10 @@ pub fn take_screenshot( fb.render_node( state.root.deref(), state, - include_cursor.then_some(state.root.extents.get()), + Some(state.root.extents.get()), None, Scale::from_int(1), + include_cursor, true, false, Transform::None, diff --git a/src/state.rs b/src/state.rs index d99e9729..6b7b6b3f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -36,6 +36,7 @@ use { wl_surface::{ wl_subsurface::SubsurfaceIds, zwp_idle_inhibitor_v1::{IdleInhibitorId, IdleInhibitorIds, ZwpIdleInhibitorV1}, + zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, NoneSurfaceExt, WlSurface, }, wp_linux_drm_syncobj_manager_v1::WpLinuxDrmSyncobjManagerV1Global, @@ -134,6 +135,7 @@ pub struct State { pub pending_output_render_data: AsyncQueue>, pub pending_float_layout: AsyncQueue>, pub pending_float_titles: AsyncQueue>, + pub pending_input_popup_positioning: AsyncQueue>, pub dbus: Dbus, pub fdcloser: Arc, pub logger: Option>, diff --git a/src/xkbcommon.rs b/src/xkbcommon.rs index ab033913..bec32b9e 100644 --- a/src/xkbcommon.rs +++ b/src/xkbcommon.rs @@ -6,6 +6,9 @@ include!(concat!(env!("OUT_DIR"), "/xkbcommon_tys.rs")); pub use consts::*; use { + crate::utils::{ + errorfmt::ErrorFmt, oserror::OsError, ptr_ext::PtrExt, trim::AsciiTrim, vecset::VecSet, + }, bstr::{BStr, ByteSlice}, isnt::std_1::primitive::IsntConstPtrExt, std::{ @@ -16,12 +19,8 @@ use { ptr, rc::Rc, }, -}; - -use { - crate::utils::{errorfmt::ErrorFmt, ptr_ext::PtrExt, trim::AsciiTrim, vecset::VecSet}, thiserror::Error, - uapi::{c, OwnedFd}, + uapi::{c, Errno, OwnedFd}, }; #[derive(Debug, Error)] @@ -34,6 +33,10 @@ pub enum XkbCommonError { KeymapFromBuffer, #[error("Could not convert the keymap to a string")] AsStr, + #[error("Could not create a keymap memfd")] + KeymapMemfd(#[source] OsError), + #[error("Could not copy the keymap")] + KeymapCopy(#[source] OsError), } struct xkb_context; @@ -295,6 +298,26 @@ impl DynKeyboardState for RefCell { } } +impl KeyboardState { + pub fn create_new_keymap_fd(&self) -> Result, XkbCommonError> { + let fd = match uapi::memfd_create("shared-keymap", c::MFD_CLOEXEC) { + Ok(fd) => fd, + Err(e) => return Err(XkbCommonError::KeymapMemfd(e.into())), + }; + let target = self.map_len as c::off_t; + let mut pos = 0; + while pos < target { + let rem = target - pos; + let res = uapi::sendfile(fd.raw(), self.map.raw(), Some(&mut pos), rem as usize); + match res { + Ok(_) | Err(Errno(c::EINTR)) => {} + Err(e) => return Err(XkbCommonError::KeymapCopy(e.into())), + } + } + Ok(Rc::new(fd)) + } +} + impl XkbState { pub fn mods(&self) -> ModifierState { self.kb_state.mods diff --git a/wire/zwp_input_method_keyboard_grab_v2.txt b/wire/zwp_input_method_keyboard_grab_v2.txt new file mode 100644 index 00000000..d17a89ba --- /dev/null +++ b/wire/zwp_input_method_keyboard_grab_v2.txt @@ -0,0 +1,29 @@ +request release { + +} + +event keymap { + format: u32, + fd: fd, + size: u32, +} + +event key { + serial: u32, + time: u32, + key: u32, + state: u32, +} + +event modifiers { + serial: u32, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + group: u32, +} + +event repeat_info { + rate: i32, + delay: i32, +} diff --git a/wire/zwp_input_method_manager_v2.txt b/wire/zwp_input_method_manager_v2.txt new file mode 100644 index 00000000..4d8746d8 --- /dev/null +++ b/wire/zwp_input_method_manager_v2.txt @@ -0,0 +1,8 @@ +request get_input_method { + seat: id(wl_seat), + input_method: id(zwp_input_method_v2), +} + +request destroy { + +} diff --git a/wire/zwp_input_method_v2.txt b/wire/zwp_input_method_v2.txt new file mode 100644 index 00000000..5cb7641b --- /dev/null +++ b/wire/zwp_input_method_v2.txt @@ -0,0 +1,62 @@ +request commit_string { + text: str, +} + +request set_preedit_string { + text: str, + cursor_begin: i32, + cursor_end: i32, +} + +request delete_surrounding_text { + before_length: u32, + after_length: u32, +} + +request commit { + serial: u32, +} + +request get_input_popup_surface { + id: id(zwp_input_popup_surface_v2), + surface: id(wl_surface), +} + +request grab_keyboard { + keyboard: id(zwp_input_method_keyboard_grab_v2), +} + +request destroy { + +} + +event activate { + +} + +event deactivate { + +} + +event surrounding_text { + text: str, + cursor: u32, + anchor: u32, +} + +event text_change_cause { + cause: u32, +} + +event content_type { + hint: u32, + purpose: u32, +} + +event done { + +} + +event unavailable { + +} diff --git a/wire/zwp_input_popup_surface_v2.txt b/wire/zwp_input_popup_surface_v2.txt new file mode 100644 index 00000000..8b55937a --- /dev/null +++ b/wire/zwp_input_popup_surface_v2.txt @@ -0,0 +1,10 @@ +request destroy { + +} + +event text_input_rectangle { + x: i32, + y: i32, + width: i32, + height: i32, +} diff --git a/wire/zwp_text_input_manager_v3.txt b/wire/zwp_text_input_manager_v3.txt new file mode 100644 index 00000000..c2e261fc --- /dev/null +++ b/wire/zwp_text_input_manager_v3.txt @@ -0,0 +1,7 @@ +request destroy { +} + +request get_text_input { + id: id(zwp_text_input_v3), + seat: id(wl_seat), +} diff --git a/wire/zwp_text_input_v3.txt b/wire/zwp_text_input_v3.txt new file mode 100644 index 00000000..a104a2bc --- /dev/null +++ b/wire/zwp_text_input_v3.txt @@ -0,0 +1,60 @@ +request destroy { +} + +request enable { +} + +request disable { +} + +request set_surrounding_text { + text: str, + cursor: i32, + anchor: i32, +} + +request set_text_change_cause { + cause: u32, +} + +request set_content_type { + hint: u32, + purpose: u32, +} + +request set_cursor_rectangle { + x: i32, + y: i32, + width: i32, + height: i32, +} + +request commit { +} + +event enter { + surface: id(wl_surface), +} + +event leave { + surface: id(wl_surface), +} + +event preedit_string { + text: optstr, + cursor_begin: i32, + cursor_end: i32, +} + +event commit_string { + text: optstr, +} + +event delete_surrounding_text { + before_length: u32, + after_length: u32, +} + +event done { + serial: u32, +}