1
0
Fork 0
forked from wry/wry

seat: implement input methods

This commit is contained in:
Julian Orth 2024-04-14 20:22:57 +02:00
parent 5e2cdef388
commit daf52299db
44 changed files with 2165 additions and 75 deletions

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
pub input_method: Rc<ZwpInputMethodV2>,
pub kb_state_id: Cell<KeyboardStateId>,
}
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<Self>) -> 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<ClientError>),
}
efrom!(ZwpInputMethodKeyboardGrabV2Error, ClientError);

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
}
impl ZwpInputMethodManagerV2Global {
pub fn new(name: GlobalName) -> Self {
Self { name }
}
fn bind_(
self: Rc<Self>,
id: ZwpInputMethodManagerV2Id,
client: &Rc<Client>,
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<Self>) -> 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<Self>) -> 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<ClientError>),
}
efrom!(ZwpTextInputManagerV3Error, ClientError);

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
pub seat: Rc<WlSeatGlobal>,
pub popups: SmallMap<ZwpInputPopupSurfaceV2Id, Rc<ZwpInputPopupSurfaceV2>, 1>,
pub connection: CloneCell<Option<Rc<TextInputConnection>>>,
pub inert: bool,
pub num_done: NumCell<u32>,
pub pending: RefCell<Pending>,
}
#[derive(Default)]
pub struct Pending {
commit_string: Option<String>,
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<Self>) -> 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<Self>,
) -> 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<Self>,
) -> 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<Self>) -> 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<Self>,
) -> 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<Self>) -> 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<Self>) -> 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<ClientError>),
#[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);

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
}
impl ZwpTextInputManagerV3Global {
pub fn new(name: GlobalName) -> Self {
Self { name }
}
fn bind_(
self: Rc<Self>,
id: ZwpTextInputManagerV3Id,
client: &Rc<Client>,
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<Self>) -> Result<(), Self::Error> {
self.client.remove_obj(self)?;
Ok(())
}
fn get_text_input(&self, req: GetTextInput, _slf: &Rc<Self>) -> 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<ClientError>),
}
efrom!(ZwpTextInputManagerV3Error, ClientError);

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
seat: Rc<WlSeatGlobal>,
num_commits: NumCell<u32>,
state: RefCell<State>,
pending: RefCell<Pending>,
pub connection: CloneCell<Option<Rc<TextInputConnection>>>,
}
impl ZwpTextInputV3 {
pub fn cursor_rect(&self) -> Rect {
self.state.borrow().cursor_rectangle
}
pub fn new(
id: ZwpTextInputV3Id,
client: &Rc<Client>,
seat: &Rc<WlSeatGlobal>,
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<Self>) {
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<bool>,
cursor_rect: Option<Rect>,
content_type: Option<(u32, u32)>,
text_change_cause: Option<u32>,
surrounding_text: Option<(String, u32, u32)>,
}
impl ZwpTextInputV3RequestHandler for ZwpTextInputV3 {
type Error = ZwpTextInputV3Error;
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.detach();
self.client.remove_obj(self)?;
Ok(())
}
fn enable(&self, _req: Enable, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.pending.borrow_mut().enabled = Some(true);
Ok(())
}
fn disable(&self, _req: Disable, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.pending.borrow_mut().enabled = Some(false);
Ok(())
}
fn set_surrounding_text(
&self,
req: SetSurroundingText<'_>,
_slf: &Rc<Self>,
) -> 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<Self>,
) -> Result<(), Self::Error> {
self.pending.borrow_mut().text_change_cause = Some(req.cause);
Ok(())
}
fn set_content_type(&self, req: SetContentType, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.pending.borrow_mut().content_type = Some((req.hint, req.purpose));
Ok(())
}
fn set_cursor_rectangle(
&self,
req: SetCursorRectangle,
_slf: &Rc<Self>,
) -> 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<Self>) -> 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<ClientError>),
#[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);