#[macro_use]
extern crate log;
use std::convert::TryInto;
use std::num::Wrapping;
use std::sync::{Arc, Mutex};
use wayland_client::{protocol::wl_seat::WlSeat, Filter, Main};
use wayland_protocols::unstable::text_input::v3::client::zwp_text_input_v3::{
ChangeCause, ContentHint, ContentPurpose,
};
use zwp_input_method::input_method_unstable_v2::zwp_input_method_manager_v2::ZwpInputMethodManagerV2;
use zwp_input_method::input_method_unstable_v2::zwp_input_method_v2::{
Event as InputMethodEvent, ZwpInputMethodV2,
};
#[derive(Debug, Clone)]
pub enum SubmitError {
NotActive,
}
mod event_enum {
use wayland_client::event_enum;
use zwp_input_method::input_method_unstable_v2::zwp_input_method_v2::ZwpInputMethodV2;
event_enum!(
Events |
InputMethod => ZwpInputMethodV2
);
}
pub trait KeyboardVisibility {
fn show_keyboard(&self);
fn hide_keyboard(&self);
}
pub trait ReceiveSurroundingText {
fn text_changed(&self, string_left_of_cursor: String, string_right_of_cursor: String);
}
pub trait HintPurpose {
fn set_hint_purpose(&self, content_hint: ContentHint, content_purpose: ContentPurpose);
}
#[derive(Clone, Debug)]
struct IMProtocolState {
surrounding_text: String,
cursor: u32,
content_purpose: ContentPurpose,
content_hint: ContentHint,
text_change_cause: ChangeCause,
active: bool,
}
impl Default for IMProtocolState {
fn default() -> IMProtocolState {
IMProtocolState {
surrounding_text: String::new(),
cursor: 0,
content_hint: ContentHint::None,
content_purpose: ContentPurpose::Normal,
text_change_cause: ChangeCause::InputMethod,
active: false,
}
}
}
#[derive(Clone, Debug)]
struct IMServiceArc<
T: 'static + KeyboardVisibility + HintPurpose,
D: 'static + ReceiveSurroundingText,
> {
im: Main<ZwpInputMethodV2>,
ui_connector: T,
content_connector: D,
pending: IMProtocolState,
current: IMProtocolState,
serial: Wrapping<u32>,
}
impl<T: 'static + KeyboardVisibility + HintPurpose, D: 'static + ReceiveSurroundingText>
IMServiceArc<T, D>
{
fn new(
seat: &WlSeat,
im_manager: Main<ZwpInputMethodManagerV2>,
ui_connector: T,
content_connector: D,
) -> Arc<Mutex<IMServiceArc<T, D>>> {
let im = im_manager.get_input_method(seat);
let im_service = IMServiceArc {
im,
ui_connector,
content_connector,
pending: IMProtocolState::default(),
current: IMProtocolState::default(),
serial: Wrapping(0u32),
};
let im_service = Arc::new(Mutex::new(im_service));
let im_service_ref = Arc::clone(&im_service);
im_service.lock().unwrap().assign_filter(im_service_ref);
info!("New IMService was created");
im_service
}
fn assign_filter(&self, im_service: Arc<Mutex<IMServiceArc<T, D>>>) {
let filter = Filter::new(move |event, _, _| match event {
event_enum::Events::InputMethod { event, .. } => match event {
InputMethodEvent::Activate => im_service.lock().unwrap().handle_activate(),
InputMethodEvent::Deactivate => im_service.lock().unwrap().handle_deactivate(),
InputMethodEvent::SurroundingText {
text,
cursor,
anchor,
} => im_service
.lock()
.unwrap()
.handle_surrounding_text(text, cursor, anchor),
InputMethodEvent::TextChangeCause { cause } => {
im_service.lock().unwrap().handle_text_change_cause(cause)
}
InputMethodEvent::ContentType { hint, purpose } => im_service
.lock()
.unwrap()
.handle_content_type(hint, purpose),
InputMethodEvent::Done => im_service.lock().unwrap().handle_done(),
InputMethodEvent::Unavailable => im_service.lock().unwrap().handle_unavailable(),
_ => (),
},
});
self.im.assign(filter);
info!("The filter was assigned to Main<ZwpInputMethodV2>");
}
fn commit_string(&mut self, text: String) -> Result<(), SubmitError> {
info!("Commit string '{}'", text);
match self.current.active {
true => {
let cursor_position = self.pending.cursor.try_into().unwrap();
self.pending
.surrounding_text
.insert_str(cursor_position, &text);
self.pending.cursor += text.len() as u32;
self.im.commit_string(text);
Ok(())
}
false => Err(SubmitError::NotActive),
}
}
fn delete_surrounding_text(&mut self, before: u32, after: u32) -> Result<(), SubmitError> {
info!(
"Send a request to the wayland server to delete {} chars before and {} after the cursor at {} from the surrounding text",
before, after, self.pending.cursor
);
match self.current.active {
true => {
let (before, after) = self.limit_before_after(before, after);
self.update_cursor_and_surrounding_text(before, after);
self.im.delete_surrounding_text(before as u32, after as u32);
Ok(())
}
false => Err(SubmitError::NotActive),
}
}
fn commit(&mut self) -> Result<(), SubmitError> {
info!("Commit the changes");
match self.current.active {
true => {
self.im.commit(self.serial.0);
self.serial += Wrapping(1u32);
self.pending_becomes_current();
Ok(())
}
false => Err(SubmitError::NotActive),
}
}
fn is_active(&self) -> bool {
self.current.active
}
fn get_surrounding_text(&self) -> (String, String) {
info!("Requested surrounding text");
let (left_str, right_str) = self
.pending
.surrounding_text
.split_at(self.current.cursor.try_into().unwrap());
(left_str.to_string(), right_str.to_string())
}
fn handle_activate(&mut self) {
info!("handle_activate() was called");
self.pending = IMProtocolState {
active: true,
..IMProtocolState::default()
};
}
fn handle_deactivate(&mut self) {
info!("handle_deactivate() was called");
self.pending.active = false;
}
fn handle_surrounding_text(&mut self, text: String, cursor: u32, anchor: u32) {
info!("handle_surrounding_text(text: '{}', cursor: {}) was called",text, cursor);
self.pending.surrounding_text = text;
self.pending.cursor = cursor;
}
fn handle_text_change_cause(&mut self, cause: ChangeCause) {
info!("handle_text_change_cause() was called");
self.pending.text_change_cause = cause;
}
fn handle_content_type(&mut self, hint: ContentHint, purpose: ContentPurpose) {
info!("handle_content_type() was called");
self.pending.content_hint = hint;
self.pending.content_purpose = purpose;
}
fn handle_done(&mut self) {
info!("handle_done() was called");
self.pending_becomes_current();
}
fn handle_unavailable(&mut self) {
info!("handle_unavailable() was called");
self.im.destroy();
self.current.active = false;
self.ui_connector.hide_keyboard();
}
fn pending_becomes_current(&mut self) {
info!("The pending protocol state became the current state");
let active_changed = self.current.active ^ self.pending.active;
let text_changed = self.current.surrounding_text != self.pending.surrounding_text;
self.current = self.pending.clone();
if text_changed {
info!(
"The surrounding text changed to '{}'",
self.current.surrounding_text
);
let (left_str, right_str) = self
.current
.surrounding_text
.split_at(self.current.cursor.try_into().unwrap());
let (left_str, right_str) = (left_str.to_string(), right_str.to_string());
self.content_connector.text_changed(left_str, right_str);
}
if active_changed {
if self.current.active {
self.ui_connector.show_keyboard();
self.ui_connector
.set_hint_purpose(self.current.content_hint, self.current.content_purpose);
} else {
self.ui_connector.hide_keyboard();
};
}
}
fn limit_before_after(&self, before: u32, after: u32) -> (u32, u32) {
let cursor_position = self.pending.cursor;
let before = if cursor_position > before {
before
} else {
cursor_position
};
let after = if cursor_position.saturating_add(after)
<= self.pending.surrounding_text.len().try_into().unwrap()
{
after
} else {
self.pending.surrounding_text.len() as u32 - cursor_position
};
(before, after)
}
fn update_cursor_and_surrounding_text(&mut self, before: u32, after: u32) {
let cursor_position = self.pending.cursor as usize;
let (string_left_of_cursor, old_string_right_of_cursor) =
self.pending.surrounding_text.split_at(cursor_position);
let mut string_left_of_cursor = String::from(string_left_of_cursor);
let mut string_right_of_cursor = String::from("");
for _ in 0..before {
string_left_of_cursor.pop();
}
for character in old_string_right_of_cursor.chars().skip(after as usize) {
string_right_of_cursor.push(character);
}
let new_cursor_position = string_left_of_cursor.len() as u32;
let mut new_surrounding_text = string_left_of_cursor;
new_surrounding_text.push_str(&string_right_of_cursor);
self.pending.surrounding_text = new_surrounding_text;
self.pending.cursor = new_cursor_position;
}
}
#[derive(Clone, Debug)]
pub struct IMService<
T: 'static + KeyboardVisibility + HintPurpose,
D: 'static + ReceiveSurroundingText,
> {
im_service_arc: Arc<Mutex<IMServiceArc<T, D>>>,
}
impl<T: 'static + KeyboardVisibility + HintPurpose, D: 'static + ReceiveSurroundingText>
IMService<T, D>
{
pub fn new(
seat: &WlSeat,
im_manager: Main<ZwpInputMethodManagerV2>,
ui_connector: T,
content_connector: D,
) -> IMService<T, D> {
let im_service_arc = IMServiceArc::new(seat, im_manager, ui_connector, content_connector);
IMService { im_service_arc }
}
pub fn commit_string(&self, text: String) -> Result<(), SubmitError> {
self.im_service_arc.lock().unwrap().commit_string(text)
}
pub fn delete_surrounding_text(&self, before: u32, after: u32) -> Result<(), SubmitError> {
info!("Delete surrounding text ");
self.im_service_arc
.lock()
.unwrap()
.delete_surrounding_text(before, after)
}
pub fn commit(&self) -> Result<(), SubmitError> {
self.im_service_arc.lock().unwrap().commit()
}
pub fn is_active(&self) -> bool {
self.im_service_arc.lock().unwrap().is_active()
}
pub fn get_surrounding_text(&self) -> (String, String) {
self.im_service_arc.lock().unwrap().get_surrounding_text()
}
}