#![cfg(all(feature = "wayland-native", unix_desktop))]
use crate::compat::{lock, Arc, Mutex, ToString};
use crate::core::ObjectId;
use crate::platform::ime::{ImeBridge, ImeCandidatePosition, ImeComposition};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Connection, QueueHandle};
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{
ContentHint, ContentPurpose, ZwpTextInputV3,
};
#[derive(Clone)]
pub(crate) struct WaylandIme {
inner: Arc<Mutex<Inner>>,
}
struct Channel {
#[allow(dead_code)]
manager: ZwpTextInputManagerV3,
text_input: ZwpTextInputV3,
conn: Connection,
}
struct Inner {
channel: Option<Channel>,
focused_widget: Option<ObjectId>,
candidate: ImeCandidatePosition,
active: bool,
}
impl WaylandIme {
pub(crate) fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(Inner {
channel: None,
focused_widget: None,
candidate: ImeCandidatePosition { x: 0, y: 0 },
active: false,
})),
}
}
pub(crate) fn clear_channel(&self) {
let mut inner = lock(&self.inner);
inner.channel = None;
inner.active = false;
}
pub(crate) fn attach_channel(
&self,
manager: ZwpTextInputManagerV3,
text_input: ZwpTextInputV3,
conn: Connection,
) {
let mut inner = lock(&self.inner);
inner.channel = Some(Channel { manager, text_input, conn });
inner.active = false;
}
pub(crate) fn available(&self) -> bool {
lock(&self.inner).channel.is_some()
}
pub(crate) fn set_active(&self, active: bool) {
lock(&self.inner).active = active;
}
pub(crate) fn preedit(&self, text: &str, cursor_begin: i32, cursor_end: i32) {
let widget = lock(&self.inner).focused_widget;
let Some(widget) = widget else { return };
let cursor = if cursor_begin < 0 { text.len() } else { utf16_to_byte(text, cursor_begin) };
let end = if cursor_end < 0 { cursor } else { utf16_to_byte(text, cursor_end) };
let composition = ImeComposition {
text: text.to_string(),
cursor_position: cursor,
selection_length: end.saturating_sub(cursor),
};
let _ = crate::platform::ime::deliver_composition(widget, &composition);
}
pub(crate) fn commit(&self, text: &str) {
let widget = lock(&self.inner).focused_widget;
let Some(widget) = widget else { return };
let _ = crate::platform::ime::deliver_commit(widget, text);
}
}
fn utf16_to_byte(text: &str, offset: i32) -> usize {
let offset = offset.max(0) as usize;
let mut units = 0usize;
for (byte_index, ch) in text.char_indices() {
if units >= offset {
return byte_index;
}
let next = units + ch.len_utf16();
if next > offset {
return byte_index;
}
units = next;
}
text.len()
}
impl Inner {
fn apply(&mut self, enabled: bool) {
let Some(channel) = self.channel.as_ref() else { return };
if enabled {
channel.text_input.enable();
channel.text_input.set_content_type(ContentHint::None, ContentPurpose::Normal);
channel.text_input.set_cursor_rectangle(self.candidate.x, self.candidate.y, 1, 1);
} else {
channel.text_input.disable();
}
channel.text_input.commit();
if let Err(e) = channel.conn.flush() {
log::warn!("[wayland] text-input flush failed: {e}");
}
}
}
impl ImeBridge for WaylandIme {
fn focus_in(&self, widget_id: ObjectId) {
let mut inner = lock(&self.inner);
inner.focused_widget = Some(widget_id);
inner.apply(true);
}
fn focus_out(&self, _widget_id: ObjectId) {
let mut inner = lock(&self.inner);
inner.focused_widget = None;
inner.active = false;
inner.apply(false);
}
fn commit_text(&self, text: &str) {
let widget = lock(&self.inner).focused_widget;
if let Some(widget) = widget {
let _ = crate::platform::ime::deliver_commit(widget, text);
}
}
fn set_composition(&self, _composition: &ImeComposition) {}
fn set_candidate_window_position(&self, position: ImeCandidatePosition) {
let mut inner = lock(&self.inner);
inner.candidate = position;
let enabled = inner.focused_widget.is_some();
if inner.channel.is_some() {
inner.apply(enabled);
}
}
fn is_active(&self) -> bool {
lock(&self.inner).active
}
}
pub(crate) fn create_text_input(
ime: &WaylandIme,
manager: &ZwpTextInputManagerV3,
seat: &WlSeat,
qh: &QueueHandle<super::platform_impl::WaylandSessionState>,
conn: &Connection,
) -> ZwpTextInputV3 {
let text_input = manager.get_text_input(seat, qh, ());
ime.attach_channel(manager.clone(), text_input.clone(), conn.clone());
text_input
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utf16_offsets_map_to_byte_offsets() {
assert_eq!(utf16_to_byte("abc", 0), 0);
assert_eq!(utf16_to_byte("abc", 2), 2);
assert_eq!(utf16_to_byte("abc", 99), 3);
assert_eq!(utf16_to_byte("你好", 1), 3);
assert_eq!(utf16_to_byte("😀x", 1), 0);
assert_eq!(utf16_to_byte("😀x", 2), 4);
}
#[test]
fn an_unbound_bridge_reports_no_channel() {
let ime = WaylandIme::new();
assert!(!ime.available());
assert!(!ime.is_active());
ime.preedit("あ", 0, 0);
ime.commit("あ");
}
}