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 068f281e..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(), @@ -230,6 +231,7 @@ fn start_compositor2( subsurface_ids: Default::default(), wait_for_sync_obj: Rc::new(WaitForSyncObj::new(&ring, &engine)), explicit_sync_enabled: Cell::new(true), + keyboard_state_ids: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -326,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/config/handler.rs b/src/config/handler.rs index ad39226d..79a47237 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -309,13 +309,12 @@ impl ConfigProxyHandler { keymap: Keymap, ) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - if keymap.is_invalid() { - dev.keymap_id.set(None); - dev.keymap.set(None); + let map = if keymap.is_invalid() { + None } else { - let map = self.get_keymap(keymap)?; - dev.set_keymap(&map); + Some(self.get_keymap(keymap)?) }; + dev.set_keymap(map); Ok(()) } @@ -548,7 +547,7 @@ impl ConfigProxyHandler { Some(self.get_seat(seat)?) }; let dev = self.get_device_handler_data(device)?; - dev.seat.set(seat); + dev.set_seat(seat); Ok(()) } 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/jay_input.rs b/src/ifs/jay_input.rs index 28617ee2..f9368f6e 100644 --- a/src/ifs/jay_input.rs +++ b/src/ifs/jay_input.rs @@ -325,7 +325,7 @@ impl JayInputRequestHandler for JayInput { self.or_error(|| { let seat = self.seat(req.seat)?; let dev = self.device(req.id)?; - dev.seat.set(Some(seat)); + dev.set_seat(Some(seat)); Ok(()) }) } @@ -333,7 +333,7 @@ impl JayInputRequestHandler for JayInput { fn detach(&self, req: Detach, _slf: &Rc) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.seat.set(None); + dev.set_seat(None); Ok(()) }) } @@ -374,7 +374,7 @@ impl JayInputRequestHandler for JayInput { fn set_device_keymap(&self, req: SetDeviceKeymap, _slf: &Rc) -> Result<(), Self::Error> { self.set_keymap_impl(&req.keymap, req.keymap_len, |map| { let dev = self.device(req.id)?; - dev.set_keymap(&map); + dev.set_keymap(Some(map.clone())); Ok(()) }) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index d4c7448e..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, @@ -49,7 +54,7 @@ use { leaks::Tracker, object::{Object, Version}, rect::Rect, - state::State, + state::{DeviceHandlerData, State}, time::now_usec, tree::{ generic_node_visitor, ContainerNode, ContainerSplit, Direction, FloatNode, FoundNode, @@ -58,14 +63,14 @@ use { utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, linkedlist::LinkedNode, numcell::NumCell, rc_eq::rc_eq, - smallmap::SmallMap, transform_ext::TransformExt, vecset::VecSet, + smallmap::SmallMap, transform_ext::TransformExt, }, wire::{ wl_seat::*, ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId, WlSeatId, ZwlrDataControlDeviceV1Id, ZwpPrimarySelectionDeviceV1Id, - ZwpRelativePointerV1Id, + ZwpRelativePointerV1Id, ZwpTextInputV3Id, }, - xkbcommon::{KeymapId, XkbKeymap, XkbState}, + xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState}, }, ahash::AHashMap, jay_config::keyboard::mods::Modifiers, @@ -75,10 +80,10 @@ use { collections::hash_map::Entry, mem, ops::{Deref, DerefMut}, - rc::Rc, + rc::{Rc, Weak}, }, thiserror::Error, - uapi::{c, Errno, OwnedFd}, + uapi::OwnedFd, }; pub const POINTER: u32 = 1; @@ -130,7 +135,6 @@ pub struct WlSeatGlobal { pointer_stack_modified: Cell, found_tree: RefCell>, keyboard_node: CloneCell>, - pressed_keys: RefCell>, bindings: RefCell>>>, x_data_devices: SmallMap, 1>, data_devices: RefCell>>>, @@ -144,10 +148,9 @@ pub struct WlSeatGlobal { CopyHashMap<(ClientId, ZwlrDataControlDeviceV1Id), Rc>, repeat_rate: Cell<(i32, i32)>, seat_kb_map: CloneCell>, - seat_kb_map_id: Cell, - effective_kb_map: CloneCell>, - effective_kb_map_id: Cell, - kb_state: RefCell, + seat_xkb_state: CloneCell>>, + latest_kb_state: CloneCell>, + xkb_states: CopyHashMap>>, cursor: CloneCell>>, tree_changed: Rc, selection: CloneCell>>, @@ -168,7 +171,10 @@ pub struct WlSeatGlobal { constraint: CloneCell>>, idle_notifications: CopyHashMap<(ClientId, ExtIdleNotificationV1Id), Rc>, last_input_usec: Cell, - keymap_version: NumCell, + text_inputs: RefCell>>>, + text_input: CloneCell>>, + input_method: CloneCell>>, + input_method_grab: CloneCell>>, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -182,6 +188,13 @@ impl Drop for WlSeatGlobal { impl WlSeatGlobal { pub fn new(name: GlobalName, seat_name: &str, state: &Rc) -> Rc { + let seat_xkb_state = state + .default_keymap + .state(state.keyboard_state_ids.next()) + .map(|s| Rc::new(RefCell::new(s))) + .unwrap(); + let xkb_states = CopyHashMap::new(); + xkb_states.set(state.default_keymap.id, Rc::downgrade(&seat_xkb_state)); let slf = Rc::new(Self { id: state.seat_ids.next(), name, @@ -196,17 +209,15 @@ impl WlSeatGlobal { pointer_stack_modified: Cell::new(false), found_tree: RefCell::new(vec![]), keyboard_node: CloneCell::new(state.root.clone()), - pressed_keys: RefCell::new(Default::default()), bindings: Default::default(), x_data_devices: Default::default(), data_devices: RefCell::new(Default::default()), primary_selection_devices: RefCell::new(Default::default()), repeat_rate: Cell::new((25, 250)), seat_kb_map: CloneCell::new(state.default_keymap.clone()), - seat_kb_map_id: Cell::new(state.default_keymap.id), - effective_kb_map: CloneCell::new(state.default_keymap.clone()), - effective_kb_map_id: Cell::new(state.default_keymap.id), - kb_state: RefCell::new(state.default_keymap.state().unwrap()), + seat_xkb_state: CloneCell::new(seat_xkb_state.clone()), + latest_kb_state: CloneCell::new(seat_xkb_state.clone()), + xkb_states, cursor: Default::default(), tree_changed: Default::default(), selection: Default::default(), @@ -228,7 +239,10 @@ impl WlSeatGlobal { idle_notifications: Default::default(), last_input_usec: Cell::new(now_usec()), wlr_data_devices: Default::default(), - keymap_version: NumCell::new(1), + 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(); @@ -249,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() } @@ -516,25 +534,49 @@ impl WlSeatGlobal { } pub fn set_seat_keymap(&self, keymap: &Rc) { + let Some(xkb_state) = self.get_xkb_state(keymap) else { + return; + }; self.seat_kb_map.set(keymap.clone()); - self.seat_kb_map_id.set(keymap.id); + let old = self.seat_xkb_state.set(xkb_state.clone()); + if !rc_eq(&old, &xkb_state) { + self.handle_xkb_state_change(&old.borrow(), &xkb_state.borrow()); + } } - fn set_effective_keymap(&self, keymap: &Rc) { - let state = match keymap.state() { - Ok(s) => s, - Err(e) => { - log::error!("Could not create keymap state: {}", ErrorFmt(e)); - return; - } + fn handle_xkb_state_change(&self, old: &XkbState, new: &XkbState) { + let Some(surface) = self.keyboard_node.get().node_into_surface() else { + return; }; - self.keyboard_node.get().node_on_unfocus(self); - self.effective_kb_map.set(keymap.clone()); - self.effective_kb_map_id.set(keymap.id); - *self.kb_state.borrow_mut() = state; - self.keymap_version.fetch_add(1); - self.pressed_keys.borrow_mut().clear(); - self.keyboard_node.get().node_on_focus(self); + let serial = surface.client.next_serial(); + self.surface_kb_event(Version::ALL, &surface, |kb| { + if kb.kb_state_id() == old.kb_state.id { + kb.send_leave(serial, surface.id); + kb.enter(serial, surface.id, &new.kb_state); + } + }); + } + + pub fn get_xkb_state(&self, keymap: &Rc) -> Option>> { + if let Some(weak) = self.xkb_states.get(&keymap.id) { + if let Some(state) = weak.upgrade() { + return Some(state); + } + } + self.xkb_states + .lock() + .retain(|_, state| state.strong_count() > 0); + match keymap.state(self.state.keyboard_state_ids.next()) { + Ok(s) => { + let s = Rc::new(RefCell::new(s)); + self.xkb_states.set(keymap.id, Rc::downgrade(&s)); + Some(s) + } + Err(e) => { + log::error!("Could not create xkb state: {}", ErrorFmt(e)); + None + } + } } pub fn prepare_for_lock(self: &Rc) { @@ -706,6 +748,9 @@ impl WlSeatGlobal { } } } + if let Some(grab) = self.input_method_grab.get() { + grab.send_repeat_info(); + } } pub fn close(self: &Rc) { @@ -1023,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 { @@ -1091,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(); + } + } } } @@ -1146,25 +1200,11 @@ impl WlSeat { self.global.move_(node); } - pub fn keymap_fd(&self, keymap: &XkbKeymap) -> Result, WlKeyboardError> { + pub fn keymap_fd(&self, state: &KeyboardState) -> Result, WlKeyboardError> { if self.version >= READ_ONLY_KEYMAP_SINCE { - return Ok(keymap.map.clone()); + 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 = keymap.map_len as c::off_t; - let mut pos = 0; - while pos < target { - let rem = target - pos; - let res = uapi::sendfile(fd.raw(), keymap.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()?) } } @@ -1184,11 +1224,13 @@ impl WlSeatRequestHandler for WlSeat { track!(self.client, p); self.client.add_client_obj(&p)?; self.keyboards.set(req.id, p.clone()); - p.send_keymap(); if let Some(surface) = self.global.keyboard_node.get().node_into_surface() { if surface.client.id == self.client.id { - let serial = self.client.next_serial(); - p.send_enter(serial, surface.id, &self.global.pressed_keys.borrow()) + p.enter( + self.client.next_serial(), + surface.id, + &self.global.seat_xkb_state.get().borrow().kb_state, + ); } } if self.version >= REPEAT_INFO_SINCE { @@ -1269,3 +1311,51 @@ pub fn collect_kb_foci(node: Rc) -> SmallVec<[Rc; 3]> { collect_kb_foci2(node, &mut res); res } + +impl DeviceHandlerData { + pub fn set_seat(&self, seat: Option>) { + let old = self.seat.set(seat.clone()); + if let Some(old) = old { + if let Some(new) = seat { + if old.id() == new.id() { + return; + } + } + let xkb_state = self.get_effective_xkb_state(&old); + let xkb_state = &mut *xkb_state.borrow_mut(); + xkb_state.reset(); + old.handle_xkb_state_change(xkb_state, xkb_state); + } + self.update_xkb_state(); + } + + pub fn set_keymap(&self, keymap: Option>) { + self.keymap.set(keymap); + self.update_xkb_state(); + } + + fn get_effective_xkb_state(&self, seat: &WlSeatGlobal) -> Rc> { + match self.xkb_state.get() { + Some(s) => s, + _ => seat.seat_xkb_state.get(), + } + } + + fn update_xkb_state(&self) { + let Some(seat) = self.seat.get() else { + self.xkb_state.take(); + return; + }; + let old = self.get_effective_xkb_state(&seat); + self.xkb_state.take(); + if let Some(keymap) = self.keymap.get() { + if let Some(state) = seat.get_xkb_state(&keymap) { + self.xkb_state.set(Some(state)); + } + } + let new = self.get_effective_xkb_state(&seat); + if !rc_eq(&old, &new) { + seat.handle_xkb_state_change(&old.borrow(), &new.borrow()); + } + } +} diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 009c279f..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, @@ -32,15 +33,16 @@ use { tree::{Direction, FloatNode, Node, ToplevelNode}, utils::{bitflags::BitflagsExt, smallmap::SmallMap}, wire::WlDataOfferId, - xkbcommon::{ModifierState, XKB_KEY_DOWN, XKB_KEY_UP}, + xkbcommon::{KeyboardState, XkbState, XKB_KEY_DOWN, XKB_KEY_UP}, }, + isnt::std_1::primitive::IsntSlice2Ext, jay_config::keyboard::{ mods::{Modifiers, CAPS, NUM}, syms::KeySym, ModifiedKeySym, }, smallvec::SmallVec, - std::rc::Rc, + std::{cell::RefCell, rc::Rc}, }; #[derive(Default)] @@ -196,20 +198,7 @@ impl WlSeatGlobal { time_usec, key, state, - } => { - let desired_kb_map_id = match dev.keymap_id.get() { - Some(id) => id, - None => self.seat_kb_map_id.get(), - }; - if desired_kb_map_id != self.effective_kb_map_id.get() { - let map = match dev.keymap.get() { - Some(map) => map, - None => self.seat_kb_map.get(), - }; - self.set_effective_keymap(&map); - } - self.key_event(time_usec, key, state) - } + } => self.key_event(time_usec, key, state, || dev.get_effective_xkb_state(self)), InputEvent::ConnectorPosition { time_usec, connector, @@ -356,18 +345,27 @@ impl WlSeatGlobal { self.pointer_owner.button(self, time_usec, button, state); } - pub(super) fn key_event(&self, time_usec: u64, key: u32, key_state: KeyState) { + pub(super) fn key_event( + &self, + time_usec: u64, + key: u32, + key_state: KeyState, + mut get_state: F, + ) where + F: FnMut() -> Rc>, + { + let mut xkb_state_rc = get_state(); + let mut xkb_state = xkb_state_rc.borrow_mut(); let (state, xkb_dir) = { - let mut pk = self.pressed_keys.borrow_mut(); match key_state { KeyState::Released => { - if !pk.remove(&key) { + if xkb_state.kb_state.pressed_keys.not_contains(&key) { return; } (wl_keyboard::RELEASED, XKB_KEY_UP) } KeyState::Pressed => { - if !pk.insert(key) { + if xkb_state.kb_state.pressed_keys.contains(&key) { return; } (wl_keyboard::PRESSED, XKB_KEY_DOWN) @@ -377,10 +375,9 @@ impl WlSeatGlobal { let mut shortcuts = SmallVec::<[_; 1]>::new(); let new_mods; { - let mut kb_state = self.kb_state.borrow_mut(); if !self.state.lock.locked.get() && state == wl_keyboard::PRESSED { - let old_mods = kb_state.mods(); - let keysyms = kb_state.unmodified_keysyms(key); + let old_mods = xkb_state.mods(); + let keysyms = xkb_state.unmodified_keysyms(key); for &sym in keysyms { let mods = old_mods.mods_effective & !(CAPS.0 | NUM.0); if let Some(mods) = self.shortcuts.get(&(mods, sym)) { @@ -391,44 +388,49 @@ impl WlSeatGlobal { } } } - new_mods = kb_state.update(key, xkb_dir); + new_mods = xkb_state.update(key, xkb_dir); } self.state.for_each_seat_tester(|t| { 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); + 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); for shortcut in shortcuts { config.invoke_shortcut(self.id(), &shortcut); } + xkb_state_rc = get_state(); + xkb_state = xkb_state_rc.borrow_mut(); + if id != xkb_state.kb_state.id { + return; + } } - if let Some(mods) = new_mods { + if new_mods { self.state.for_each_seat_tester(|t| { - t.send_modifiers(self.id, &mods); + t.send_modifiers(self.id, &xkb_state.kb_state.mods); }); - node.node_on_mods(self, mods); + match &input_method_grab { + Some(g) => g.on_modifiers(&xkb_state.kb_state), + _ => node.node_on_mods(self, &xkb_state.kb_state), + } } - } - - pub(super) fn set_modifiers( - &self, - mods_depressed: u32, - mods_latched: u32, - mods_locked: u32, - group: u32, - ) { - let new_mods = - self.kb_state - .borrow_mut() - .set(mods_depressed, mods_latched, mods_locked, group); - if let Some(mods) = new_mods { - self.state.for_each_seat_tester(|t| { - t.send_modifiers(self.id, &mods); - }); - self.keyboard_node.get().node_on_mods(self, mods); + match key_state { + KeyState::Released => { + xkb_state.kb_state.pressed_keys.remove(&key); + } + KeyState::Pressed => { + xkb_state.kb_state.pressed_keys.insert(key); + } } + drop(xkb_state); + self.latest_kb_state.set(xkb_state_rc); } } @@ -578,7 +580,7 @@ impl WlSeatGlobal { }); } - fn surface_kb_event(&self, ver: Version, surface: &WlSurface, mut f: F) + pub fn surface_kb_event(&self, ver: Version, surface: &WlSurface, mut f: F) where F: FnMut(&Rc), { @@ -766,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)) } @@ -774,21 +788,11 @@ impl WlSeatGlobal { // Focus callbacks impl WlSeatGlobal { pub fn focus_surface(&self, surface: &WlSurface) { - let pressed_keys = &*self.pressed_keys.borrow(); + let kb_state = self.latest_kb_state.get(); + let kb_state = &*kb_state.borrow(); let serial = surface.client.next_serial(); self.surface_kb_event(Version::ALL, surface, |k| { - k.send_enter(serial, surface.id, pressed_keys) - }); - let ModifierState { - mods_depressed, - mods_latched, - mods_locked, - group, - .. - } = self.kb_state.borrow().mods(); - let serial = surface.client.next_serial(); - self.surface_kb_event(Version::ALL, surface, |k| { - k.send_modifiers(serial, mods_depressed, mods_latched, mods_locked, group) + k.enter(serial, surface.id, kb_state); }); if self.keyboard_node.get().node_client_id() != Some(surface.client.id) { @@ -801,32 +805,40 @@ 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(); + } + } } } // Key callbacks impl WlSeatGlobal { - pub fn key_surface(&self, surface: &WlSurface, time_usec: u64, key: u32, state: u32) { + pub fn key_surface( + &self, + surface: &WlSurface, + time_usec: u64, + key: u32, + state: u32, + kb_state: &KeyboardState, + ) { let serial = surface.client.next_serial(); let time = (time_usec / 1000) as _; self.surface_kb_event(Version::ALL, surface, |k| { - k.send_key(serial, time, key, state) + k.on_key(serial, time, key, state, surface.id, kb_state); }); } } // Modifiers callbacks impl WlSeatGlobal { - pub fn mods_surface(&self, surface: &WlSurface, mods: ModifierState) { + pub fn mods_surface(&self, surface: &WlSurface, kb_state: &KeyboardState) { let serial = surface.client.next_serial(); self.surface_kb_event(Version::ALL, surface, |k| { - k.send_modifiers( - serial, - mods.mods_depressed, - mods.mods_latched, - mods.mods_locked, - mods.group, - ) + k.on_mods_changed(serial, surface.id, kb_state) }); } } 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 091d9b39..bce6aca6 100644 --- a/src/ifs/wl_seat/wl_keyboard.rs +++ b/src/ifs/wl_seat/wl_keyboard.rs @@ -4,10 +4,11 @@ use { ifs::wl_seat::WlSeat, leaks::Tracker, object::{Object, Version}, - utils::{errorfmt::ErrorFmt, numcell::NumCell, oserror::OsError}, + utils::errorfmt::ErrorFmt, wire::{wl_keyboard::*, WlKeyboardId, WlSurfaceId}, + xkbcommon::{KeyboardState, KeyboardStateId, ModifierState, XkbCommonError}, }, - std::rc::Rc, + std::{cell::Cell, rc::Rc}, thiserror::Error, }; @@ -23,7 +24,7 @@ pub const PRESSED: u32 = 1; pub struct WlKeyboard { id: WlKeyboardId, seat: Rc, - pub(super) keymap_version: NumCell, + kb_state_id: Cell, pub tracker: Tracker, } @@ -32,14 +33,33 @@ impl WlKeyboard { Self { id, seat: seat.clone(), - keymap_version: NumCell::new(0), + kb_state_id: Cell::new(KeyboardStateId::from_raw(0)), tracker: Default::default(), } } - pub fn send_keymap(&self) { - let map = self.seat.global.effective_kb_map.get(); - let fd = match self.seat.keymap_fd(&map) { + pub fn kb_state_id(&self) -> KeyboardStateId { + self.kb_state_id.get() + } + + fn send_kb_state( + &self, + serial: u32, + kb_state: &KeyboardState, + surface_id: WlSurfaceId, + send_leave: bool, + ) { + self.kb_state_id.set(kb_state.id); + if send_leave { + self.send_leave(serial, surface_id); + } + self.send_keymap(kb_state); + self.send_enter(serial, surface_id, &kb_state.pressed_keys); + self.send_modifiers(serial, &kb_state.mods); + } + + fn send_keymap(&self, state: &KeyboardState) { + let fd = match self.seat.keymap_fd(state) { Ok(fd) => fd, Err(e) => { log::error!( @@ -54,16 +74,20 @@ impl WlKeyboard { self_id: self.id, format: XKB_V1, fd, - size: map.map_len as _, + size: state.map_len as _, }); - self.keymap_version - .set(self.seat.global.keymap_version.get()); } - pub fn send_enter(self: &Rc, serial: u32, surface: WlSurfaceId, keys: &[u32]) { - if self.keymap_version.get() != self.seat.global.keymap_version.get() { - self.send_keymap(); + pub fn enter(&self, serial: u32, surface: WlSurfaceId, kb_state: &KeyboardState) { + if kb_state.id != self.kb_state_id.get() { + self.send_kb_state(serial, kb_state, surface, false); + } else { + self.send_enter(serial, surface, &kb_state.pressed_keys); + self.send_modifiers(serial, &kb_state.mods); } + } + + fn send_enter(&self, serial: u32, surface: WlSurfaceId, keys: &[u32]) { self.seat.client.event(Enter { self_id: self.id, serial, @@ -72,7 +96,7 @@ impl WlKeyboard { }) } - pub fn send_leave(self: &Rc, serial: u32, surface: WlSurfaceId) { + pub fn send_leave(&self, serial: u32, surface: WlSurfaceId) { self.seat.client.event(Leave { self_id: self.id, serial, @@ -80,7 +104,22 @@ impl WlKeyboard { }) } - pub fn send_key(self: &Rc, serial: u32, time: u32, key: u32, state: u32) { + pub fn on_key( + &self, + serial: u32, + time: u32, + key: u32, + state: u32, + surface: WlSurfaceId, + kb_state: &KeyboardState, + ) { + if self.kb_state_id.get() != kb_state.id { + self.send_kb_state(serial, kb_state, surface, true); + } + self.send_key(serial, time, key, state); + } + + fn send_key(&self, serial: u32, time: u32, key: u32, state: u32) { self.seat.client.event(Key { self_id: self.id, serial, @@ -90,21 +129,22 @@ impl WlKeyboard { }) } - pub fn send_modifiers( - self: &Rc, - serial: u32, - mods_depressed: u32, - mods_latched: u32, - mods_locked: u32, - group: u32, - ) { + pub fn on_mods_changed(&self, serial: u32, surface: WlSurfaceId, kb_state: &KeyboardState) { + if self.kb_state_id.get() != kb_state.id { + self.send_kb_state(serial, kb_state, surface, true); + } else { + self.send_modifiers(serial, &kb_state.mods); + } + } + + fn send_modifiers(&self, serial: u32, mods: &ModifierState) { self.seat.client.event(Modifiers { self_id: self.id, serial, - mods_depressed, - mods_latched, - mods_locked, - group, + mods_depressed: mods.mods_depressed, + mods_latched: mods.mods_latched, + mods_locked: mods.mods_locked, + group: mods.group, }) } @@ -140,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_seat/zwp_virtual_keyboard_manager_v1.rs b/src/ifs/wl_seat/zwp_virtual_keyboard_manager_v1.rs index c92e50f9..717666ca 100644 --- a/src/ifs/wl_seat/zwp_virtual_keyboard_manager_v1.rs +++ b/src/ifs/wl_seat/zwp_virtual_keyboard_manager_v1.rs @@ -6,8 +6,9 @@ use { leaks::Tracker, object::{Object, Version}, wire::{zwp_virtual_keyboard_manager_v1::*, ZwpVirtualKeyboardManagerV1Id}, + xkbcommon::KeyboardState, }, - std::rc::Rc, + std::{cell::RefCell, rc::Rc}, thiserror::Error, }; @@ -76,14 +77,20 @@ impl ZwpVirtualKeyboardManagerV1RequestHandler for ZwpVirtualKeyboardManagerV1 { _slf: &Rc, ) -> Result<(), Self::Error> { let seat = self.client.lookup(req.seat)?; + let seat_keymap = seat.global.seat_kb_map.get(); let kb = Rc::new(ZwpVirtualKeyboardV1 { id: req.id, client: self.client.clone(), seat: seat.global.clone(), tracker: Default::default(), version: self.version, - keymap_id: Default::default(), - keymap: Default::default(), + kb_state: Rc::new(RefCell::new(KeyboardState { + id: self.client.state.keyboard_state_ids.next(), + map: seat_keymap.map.clone(), + map_len: seat_keymap.map_len, + pressed_keys: Default::default(), + mods: Default::default(), + })), }); track!(self.client, kb); self.client.add_client_obj(&kb)?; diff --git a/src/ifs/wl_seat/zwp_virtual_keyboard_v1.rs b/src/ifs/wl_seat/zwp_virtual_keyboard_v1.rs index ae86cbb5..2dde71f4 100644 --- a/src/ifs/wl_seat/zwp_virtual_keyboard_v1.rs +++ b/src/ifs/wl_seat/zwp_virtual_keyboard_v1.rs @@ -1,16 +1,20 @@ use { crate::{ - backend::KeyState, client::{Client, ClientError}, clientmem::{ClientMem, ClientMemError}, - ifs::wl_seat::{wl_keyboard, WlSeatGlobal}, + ifs::{ + wl_seat::{ + wl_keyboard::{self, WlKeyboard}, + WlSeatGlobal, + }, + wl_surface::WlSurface, + }, leaks::Tracker, object::{Object, Version}, - utils::clonecell::CloneCell, wire::{zwp_virtual_keyboard_v1::*, ZwpVirtualKeyboardV1Id}, - xkbcommon::{KeymapId, XkbCommonError, XkbKeymap}, + xkbcommon::{KeyboardState, XkbCommonError}, }, - std::{cell::Cell, rc::Rc}, + std::{cell::RefCell, rc::Rc}, thiserror::Error, }; @@ -20,21 +24,21 @@ pub struct ZwpVirtualKeyboardV1 { pub seat: Rc, pub tracker: Tracker, pub version: Version, - pub keymap_id: Cell>, - pub keymap: CloneCell>>, + pub kb_state: Rc>, } impl ZwpVirtualKeyboardV1 { - fn ensure_keymap(&self) { - if let Some(id) = self.keymap_id.get() { - if id == self.seat.effective_kb_map_id.get() { - return; - } - } - let Some(keymap) = self.keymap.get() else { + fn for_each_kb(&self, mut f: F) + where + F: FnMut(u32, &WlSurface, &WlKeyboard), + { + let Some(surface) = self.seat.keyboard_node.get().node_into_surface() else { return; }; - self.seat.set_effective_keymap(&keymap); + let serial = surface.client.next_serial(); + self.seat.surface_kb_event(Version::ALL, &surface, |kb| { + f(serial, &surface, kb); + }); } } @@ -66,31 +70,48 @@ impl ZwpVirtualKeyboardV1RequestHandler for ZwpVirtualKeyboardV1 { .xkb_ctx .keymap_from_str(&map) .map_err(ZwpVirtualKeyboardV1Error::ParseKeymap)?; - self.keymap_id.set(Some(map.id)); - self.keymap.set(Some(map)); + *self.kb_state.borrow_mut() = KeyboardState { + id: self.client.state.keyboard_state_ids.next(), + map: map.map.clone(), + map_len: map.map_len, + pressed_keys: Default::default(), + mods: Default::default(), + }; Ok(()) } fn key(&self, req: Key, _slf: &Rc) -> Result<(), Self::Error> { - self.ensure_keymap(); - let time_usec = (req.time as u64) * 1000; - let state = match req.state { - wl_keyboard::RELEASED => KeyState::Released, - wl_keyboard::PRESSED => KeyState::Pressed, + let kb_state = &mut *self.kb_state.borrow_mut(); + let contains = kb_state.pressed_keys.contains(&req.key); + let valid = match req.state { + wl_keyboard::RELEASED => contains, + wl_keyboard::PRESSED => !contains, _ => return Err(ZwpVirtualKeyboardV1Error::UnknownState(req.state)), }; - self.seat.key_event(time_usec, req.key, state); + if valid { + self.for_each_kb(|serial, surface, kb| { + kb.on_key(serial, req.time, req.key, req.state, surface.id, kb_state); + }); + match req.state { + wl_keyboard::RELEASED => kb_state.pressed_keys.remove(&req.key), + _ => kb_state.pressed_keys.insert(req.key), + }; + self.seat.latest_kb_state.set(self.kb_state.clone()); + } Ok(()) } fn modifiers(&self, req: Modifiers, _slf: &Rc) -> Result<(), Self::Error> { - self.ensure_keymap(); - self.seat.set_modifiers( - req.mods_depressed, - req.mods_latched, - req.mods_locked, - req.group, - ); + let kb_state = &mut *self.kb_state.borrow_mut(); + kb_state.mods.mods_depressed = req.mods_depressed; + kb_state.mods.mods_latched = req.mods_latched; + kb_state.mods.mods_locked = req.mods_locked; + kb_state.mods.mods_effective = req.mods_depressed | req.mods_latched | req.mods_locked; + kb_state.mods.group = req.group; + self.for_each_kb(|serial, surface, kb| { + kb.on_mods_changed(serial, surface.id, &kb_state); + }); + self.seat.latest_kb_state.set(self.kb_state.clone()); Ok(()) } diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 07a8940f..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}, @@ -65,7 +67,7 @@ use { wl_surface::*, WlOutputId, WlSurfaceId, ZwpIdleInhibitorV1Id, ZwpLinuxDmabufFeedbackV1Id, }, - xkbcommon::ModifierState, + xkbcommon::KeyboardState, xwayland::XWaylandEvent, }, ahash::AHashMap, @@ -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) { @@ -1376,12 +1387,19 @@ impl Node for WlSurface { self.toplevel.get() } - fn node_on_key(&self, seat: &WlSeatGlobal, time_usec: u64, key: u32, state: u32) { - seat.key_surface(self, time_usec, key, state); + fn node_on_key( + &self, + seat: &WlSeatGlobal, + time_usec: u64, + key: u32, + state: u32, + kb_state: &KeyboardState, + ) { + seat.key_surface(self, time_usec, key, state, kb_state); } - fn node_on_mods(&self, seat: &WlSeatGlobal, mods: ModifierState) { - seat.mods_surface(self, mods); + fn node_on_mods(&self, seat: &WlSeatGlobal, kb_state: &KeyboardState) { + seat.mods_surface(self, kb_state); } fn node_on_button( 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/t0040_virtual_keyboard.rs b/src/it/tests/t0040_virtual_keyboard.rs index 433f14bc..bf6519f2 100644 --- a/src/it/tests/t0040_virtual_keyboard.rs +++ b/src/it/tests/t0040_virtual_keyboard.rs @@ -110,7 +110,7 @@ async fn test(run: Rc) -> TestResult { mods.mods_locked, mods.group ), - (0, 0, 0, 0) + (0, 0, 0, 1) ); } 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 0a5522c3..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, @@ -74,7 +75,7 @@ use { ExtForeignToplevelListV1Id, JayRenderCtxId, JaySeatEventsId, JayWorkspaceWatcherId, ZwpLinuxDmabufFeedbackV1Id, }, - xkbcommon::{KeymapId, XkbContext, XkbKeymap}, + xkbcommon::{KeyboardStateIds, XkbContext, XkbKeymap, XkbState}, xwayland::{self, XWaylandEvent}, }, ahash::AHashMap, @@ -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>, @@ -175,6 +177,7 @@ pub struct State { pub subsurface_ids: SubsurfaceIds, pub wait_for_sync_obj: Rc, pub explicit_sync_enabled: Cell, + pub keyboard_state_ids: KeyboardStateIds, } // impl Drop for State { @@ -244,15 +247,8 @@ pub struct DeviceHandlerData { pub device: Rc, pub syspath: Option, pub devnode: Option, - pub keymap_id: Cell>, pub keymap: CloneCell>>, -} - -impl DeviceHandlerData { - pub fn set_keymap(&self, keymap: &Rc) { - self.keymap_id.set(Some(keymap.id)); - self.keymap.set(Some(keymap.clone())); - } + pub xkb_state: CloneCell>>>, } pub struct ConnectorData { diff --git a/src/tasks/input_device.rs b/src/tasks/input_device.rs index f32e8149..08857ad7 100644 --- a/src/tasks/input_device.rs +++ b/src/tasks/input_device.rs @@ -21,8 +21,8 @@ pub fn handle(state: &Rc, dev: Rc) { device: dev.clone(), syspath: props.syspath, devnode: props.devnode, - keymap_id: Default::default(), keymap: Default::default(), + xkb_state: Default::default(), }); let ae = Rc::new(AsyncEvent::default()); let oh = DeviceHandler { @@ -58,7 +58,7 @@ impl DeviceHandler { } for seat in self.state.globals.seats.lock().values() { if seat.seat_name() == DEFAULT_SEAT_NAME { - self.data.seat.set(Some(seat.clone())); + self.data.set_seat(Some(seat.clone())); break; } } diff --git a/src/tree.rs b/src/tree.rs index 975ff0b0..4946497d 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -10,7 +10,7 @@ use { rect::Rect, renderer::Renderer, utils::numcell::NumCell, - xkbcommon::ModifierState, + xkbcommon::KeyboardState, }, jay_config::Direction as JayDirection, std::{ @@ -158,16 +158,24 @@ pub trait Node: 'static { // EVENT HANDLERS - fn node_on_key(&self, seat: &WlSeatGlobal, time_usec: u64, key: u32, state: u32) { + fn node_on_key( + &self, + seat: &WlSeatGlobal, + time_usec: u64, + key: u32, + state: u32, + kb_state: &KeyboardState, + ) { let _ = seat; let _ = time_usec; let _ = key; let _ = state; + let _ = kb_state; } - fn node_on_mods(&self, seat: &WlSeatGlobal, mods: ModifierState) { + fn node_on_mods(&self, seat: &WlSeatGlobal, kb_state: &KeyboardState) { let _ = seat; - let _ = mods; + let _ = kb_state; } fn node_on_button( diff --git a/src/utils/vecset.rs b/src/utils/vecset.rs index 7333ec9b..8351e3b8 100644 --- a/src/utils/vecset.rs +++ b/src/utils/vecset.rs @@ -19,6 +19,7 @@ impl Deref for VecSet { } impl VecSet { + #[allow(dead_code)] pub fn clear(&mut self) { self.vec.clear(); } diff --git a/src/xkbcommon.rs b/src/xkbcommon.rs index c6c1531a..bec32b9e 100644 --- a/src/xkbcommon.rs +++ b/src/xkbcommon.rs @@ -6,15 +6,21 @@ 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::{ffi::CStr, io::Write, ops::Deref, ptr, rc::Rc}, -}; - -use { - crate::utils::{ptr_ext::PtrExt, trim::AsciiTrim}, + std::{ + cell::{Ref, RefCell}, + ffi::CStr, + io::Write, + ops::Deref, + ptr, + rc::Rc, + }, thiserror::Error, - uapi::{c, OwnedFd}, + uapi::{c, Errno, OwnedFd}, }; #[derive(Debug, Error)] @@ -27,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; @@ -202,7 +212,7 @@ pub struct XkbKeymap { } impl XkbKeymap { - pub fn state(self: &Rc) -> Result { + pub fn state(self: &Rc, id: KeyboardStateId) -> Result { let res = unsafe { xkb_state_new(self.keymap) }; if res.is_null() { return Err(XkbCommonError::CreateState); @@ -210,12 +220,12 @@ impl XkbKeymap { Ok(XkbState { map: self.clone(), state: res, - mods: ModifierState { - mods_depressed: 0, - mods_latched: 0, - mods_locked: 0, - mods_effective: 0, - group: 0, + kb_state: KeyboardState { + id, + map: self.map.clone(), + map_len: self.map_len, + pressed_keys: Default::default(), + mods: Default::default(), }, }) } @@ -247,7 +257,7 @@ impl Drop for XkbKeymapStr { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Default)] pub struct ModifierState { pub mods_depressed: u32, pub mods_latched: u32, @@ -256,51 +266,110 @@ pub struct ModifierState { pub group: u32, } +linear_ids!(KeyboardStateIds, KeyboardStateId, u64); + +pub struct KeyboardState { + pub id: KeyboardStateId, + pub map: Rc, + pub map_len: usize, + pub pressed_keys: VecSet, + pub mods: ModifierState, +} + +pub trait DynKeyboardState { + fn borrow(&self) -> Ref<'_, KeyboardState>; +} + +impl DynKeyboardState for RefCell { + fn borrow(&self) -> Ref<'_, KeyboardState> { + self.borrow() + } +} + pub struct XkbState { map: Rc, state: *mut xkb_state, - mods: ModifierState, + pub kb_state: KeyboardState, +} + +impl DynKeyboardState for RefCell { + fn borrow(&self) -> Ref<'_, KeyboardState> { + Ref::map(self.borrow(), |v| &v.kb_state) + } +} + +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.mods + self.kb_state.mods } - fn fetch(&mut self, changes: xkb_state_component) -> Option { + fn fetch(&mut self, changes: xkb_state_component) -> bool { unsafe { if changes != 0 { - self.mods.mods_depressed = + self.kb_state.mods.mods_depressed = xkb_state_serialize_mods(self.state, XKB_STATE_MODS_DEPRESSED.raw() as _); - self.mods.mods_latched = + self.kb_state.mods.mods_latched = xkb_state_serialize_mods(self.state, XKB_STATE_MODS_LATCHED.raw() as _); - self.mods.mods_locked = + self.kb_state.mods.mods_locked = xkb_state_serialize_mods(self.state, XKB_STATE_MODS_LOCKED.raw() as _); - self.mods.mods_effective = - self.mods.mods_depressed | self.mods.mods_latched | self.mods.mods_locked; - self.mods.group = + self.kb_state.mods.mods_effective = self.kb_state.mods.mods_depressed + | self.kb_state.mods.mods_latched + | self.kb_state.mods.mods_locked; + self.kb_state.mods.group = xkb_state_serialize_layout(self.state, XKB_STATE_LAYOUT_EFFECTIVE.raw() as _); - Some(self.mods) + true } else { - None + false } } } - pub fn update(&mut self, key: u32, direction: XkbKeyDirection) -> Option { + pub fn update(&mut self, key: u32, direction: XkbKeyDirection) -> bool { unsafe { let changes = xkb_state_update_key(self.state, key + 8, direction.raw() as _); self.fetch(changes) } } + pub fn reset(&mut self) { + let new_state = match self.map.state(self.kb_state.id) { + Ok(s) => s, + Err(e) => { + log::error!("Could not reset XKB state: {}", ErrorFmt(e)); + return; + } + }; + *self = new_state; + } + + #[allow(dead_code)] pub fn set( &mut self, mods_depressed: u32, mods_latched: u32, mods_locked: u32, group: u32, - ) -> Option { + ) -> bool { unsafe { let changes = xkb_state_update_mask( self.state, @@ -321,7 +390,7 @@ impl XkbState { let num = xkb_keymap_key_get_syms_by_level( self.map.keymap, key + 8, - self.mods.group, + self.kb_state.mods.group, 0, &mut res, ); 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, +}