1
0
Fork 0
forked from wry/wry

Merge pull request #646 from mahkoh/jorth/simple-im

Add a simple, XCompose based input method
This commit is contained in:
mahkoh 2025-10-16 16:45:37 +02:00 committed by GitHub
commit 091918d10a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 977 additions and 59 deletions

View file

@ -61,7 +61,7 @@ linearize = { version = "0.1.3", features = ["derive"] }
png = "0.18.0"
rustc-demangle = { version = "0.1.24", optional = true }
tracy-client-sys = { version = "0.24.1", features = ["ondemand", "manual-lifetime", "debuginfod", "demangle"], optional = true }
kbvm = "0.1.5"
kbvm = { version = "0.1.5", features = ["compose"] }
tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] }
regex = "1.11.1"
cfg-if = "1.0.0"

View file

@ -1026,6 +1026,24 @@ impl ConfigClient {
self.send(&ClientMessage::SeatCopyMark { seat, src, dst });
}
pub fn seat_set_simple_im_enabled(&self, seat: Seat, enabled: bool) {
self.send(&ClientMessage::SeatSetSimpleImEnabled { seat, enabled });
}
pub fn seat_get_simple_im_enabled(&self, seat: Seat) -> bool {
let res = self.send_with_response(&ClientMessage::SeatGetSimpleImEnabled { seat });
get_response!(res, false, SeatGetSimpleImEnabled { enabled });
enabled
}
pub fn seat_reload_simple_im(&self, seat: Seat) {
self.send(&ClientMessage::SeatReloadSimpleIm { seat });
}
pub fn seat_enable_unicode_input(&self, seat: Seat) {
self.send(&ClientMessage::SeatEnableUnicodeInput { seat });
}
pub fn set_show_float_pin_icon(&self, show: bool) {
self.send(&ClientMessage::SetShowFloatPinIcon { show });
}

View file

@ -788,6 +788,19 @@ pub enum ClientMessage<'a> {
workspace: Workspace,
connector: Connector,
},
SeatSetSimpleImEnabled {
seat: Seat,
enabled: bool,
},
SeatGetSimpleImEnabled {
seat: Seat,
},
SeatReloadSimpleIm {
seat: Seat,
},
SeatEnableUnicodeInput {
seat: Seat,
},
}
#[derive(Serialize, Deserialize, Debug)]
@ -1020,6 +1033,9 @@ pub enum Response {
GetShowBar {
show: bool,
},
SeatGetSimpleImEnabled {
enabled: bool,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -603,6 +603,41 @@ impl Seat {
pub fn copy_mark(self, src: u32, dst: u32) {
get!().seat_copy_mark(self, src, dst);
}
/// Sets whether the simple, XCompose based input method is enabled.
///
/// Regardless of this setting, this input method is not used if an external input
/// method is running.
///
/// The default is `true`.
pub fn set_simple_im_enabled(self, enabled: bool) {
get!().seat_set_simple_im_enabled(self, enabled);
}
/// Returns whether the simple, XCompose based input method is enabled.
pub fn simple_im_enabled(self) -> bool {
get!(true).seat_get_simple_im_enabled(self)
}
/// Toggles whether the simple, XCompose based input method is enabled.
pub fn toggle_simple_im_enabled(self) {
let get = get!();
get.seat_set_simple_im_enabled(self, !get.seat_get_simple_im_enabled(self));
}
/// Reloads the simple, XCompose based input method.
///
/// This is useful if you change the XCompose files after starting the compositor.
pub fn reload_simple_im(self) {
get!().seat_reload_simple_im(self);
}
/// Enables Unicode input in the simple, XCompose based input method.
///
/// This has no effect if the simple IM is not currently active.
pub fn enable_unicode_input(self) {
get!().seat_enable_unicode_input(self);
}
}
/// A focus-follows-mouse mode.

View file

@ -83,6 +83,28 @@ pub enum SeatCommand {
UseHardwareCursor(UseHardwareCursorArgs),
/// Set the size of the cursor.
SetCursorSize(SetCursorSizeArgs),
/// Configure the simple, XCompose based input method.
SimpleIm(SimpleImArgs),
}
#[derive(Args, Debug, Clone)]
pub struct SimpleImArgs {
#[clap(subcommand)]
pub command: SimpleImCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum SimpleImCommand {
/// Enable the simple IM.
///
/// Even if the IM is enabled, it will not be used if an external IM is running.
Enable,
/// Disable the simple IM.
Disable,
/// Reload the simple IM.
///
/// This is useful if you change the XCompose files after starting the compositor.
Reload,
}
impl Default for SeatCommand {
@ -460,6 +482,27 @@ impl Input {
size: a.size,
});
}
SeatCommand::SimpleIm(a) => match a.command {
SimpleImCommand::Enable | SimpleImCommand::Disable => {
self.handle_error(input, |e| {
eprintln!("Could not enable/disable the simple IM: {}", e);
});
tc.send(jay_input::SetSimpleImEnabled {
self_id: input,
seat: &args.seat,
enabled: matches!(a.command, SimpleImCommand::Enable) as _,
});
}
SimpleImCommand::Reload => {
self.handle_error(input, |e| {
eprintln!("Could not reload the simple IM: {}", e);
});
tc.send(jay_input::ReloadSimpleIm {
self_id: input,
seat: &args.seat,
});
}
},
}
tc.round_trip().await;
}

View file

@ -2281,6 +2281,32 @@ impl ConfigProxyHandler {
Ok(())
}
fn handle_seat_set_simple_im_enabled(&self, seat: Seat, enabled: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.set_simple_im_enabled(enabled);
Ok(())
}
fn handle_seat_get_simple_im_enabled(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
self.respond(Response::SeatGetSimpleImEnabled {
enabled: seat.simple_im_enabled(),
});
Ok(())
}
fn handle_seat_reload_simple_im(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.reload_simple_im();
Ok(())
}
fn handle_seat_enable_unicode_input(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.enable_unicode_input();
Ok(())
}
fn spaces_change(&self) {
struct V;
impl NodeVisitorBase for V {
@ -3216,6 +3242,18 @@ impl ConfigProxyHandler {
} => self
.handle_show_workspace(seat, workspace, Some(connector))
.wrn("show_workspace_on")?,
ClientMessage::SeatSetSimpleImEnabled { seat, enabled } => self
.handle_seat_set_simple_im_enabled(seat, enabled)
.wrn("seat_set_simple_im_enabled")?,
ClientMessage::SeatGetSimpleImEnabled { seat } => self
.handle_seat_get_simple_im_enabled(seat)
.wrn("seat_get_simple_im_enabled")?,
ClientMessage::SeatReloadSimpleIm { seat } => self
.handle_seat_reload_simple_im(seat)
.wrn("seat_reload_simple_im")?,
ClientMessage::SeatEnableUnicodeInput { seat } => self
.handle_seat_enable_unicode_input(seat)
.wrn("seat_enable_unicode_input")?,
}
Ok(())
}

View file

@ -955,7 +955,7 @@ pub fn create_render_pass(
for seat in seats.values() {
let (x, y) = seat.pointer_cursor().position_int();
if let Some(im) = seat.input_method() {
for (_, popup) in &im.popups {
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());

View file

@ -79,7 +79,7 @@ impl Global for JayCompositorGlobal {
}
fn version(&self) -> u32 {
21
22
}
fn required_caps(&self) -> ClientCaps {

View file

@ -515,6 +515,30 @@ impl JayInputRequestHandler for JayInput {
Ok(())
})
}
fn set_simple_im_enabled(
&self,
req: SetSimpleImEnabled<'_>,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
self.or_error(|| {
let seat = self.seat(req.seat)?;
seat.set_simple_im_enabled(req.enabled != 0);
Ok(())
})
}
fn reload_simple_im(
&self,
req: ReloadSimpleIm<'_>,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
self.or_error(|| {
let seat = self.seat(req.seat)?;
seat.reload_simple_im();
Ok(())
})
}
}
object_base! {

View file

@ -52,8 +52,8 @@ use {
pointer_owner::PointerOwnerHolder,
tablet::TabletSeatData,
text_input::{
zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2,
zwp_input_method_v2::ZwpInputMethodV2, zwp_text_input_v3::ZwpTextInputV3,
InputMethod, InputMethodKeyboardGrab, simple_im::SimpleIm,
zwp_text_input_v3::ZwpTextInputV3,
},
touch_owner::TouchOwnerHolder,
wl_keyboard::{REPEAT_INFO_SINCE, WlKeyboard, WlKeyboardError},
@ -216,8 +216,8 @@ pub struct WlSeatGlobal {
last_input_usec: Cell<u64>,
text_inputs: RefCell<AHashMap<ClientId, CopyHashMap<ZwpTextInputV3Id, Rc<ZwpTextInputV3>>>>,
text_input: CloneCell<Option<Rc<ZwpTextInputV3>>>,
input_method: CloneCell<Option<Rc<ZwpInputMethodV2>>>,
input_method_grab: CloneCell<Option<Rc<ZwpInputMethodKeyboardGrabV2>>>,
input_method: CloneCell<Option<Rc<dyn InputMethod>>>,
input_method_grab: CloneCell<Option<Rc<dyn InputMethodKeyboardGrab>>>,
forward: Cell<bool>,
focus_follows_mouse: Cell<bool>,
swipe_bindings: PerClientBindings<ZwpPointerGestureSwipeV1>,
@ -238,6 +238,8 @@ pub struct WlSeatGlobal {
marks: CopyHashMap<Keycode, Rc<dyn Node>>,
modifiers_listener: EventListener<dyn LedsListener>,
modifiers_forward: EventSource<dyn LedsListener>,
simple_im: CloneCell<Option<Rc<SimpleIm>>>,
simple_im_enabled: Cell<bool>,
}
#[derive(Copy, Clone)]
@ -259,6 +261,7 @@ impl WlSeatGlobal {
let cursor_user_group = CursorUserGroup::create(state);
let cursor_user = cursor_user_group.create_user();
cursor_user.activate();
let simple_im = SimpleIm::new(&state.kb_ctx.ctx);
let slf = Rc::new_cyclic(|slf: &Weak<WlSeatGlobal>| Self {
id: state.seat_ids.next(),
name,
@ -306,7 +309,7 @@ impl WlSeatGlobal {
data_control_devices: Default::default(),
text_inputs: Default::default(),
text_input: Default::default(),
input_method: Default::default(),
input_method: CloneCell::new(simple_im.clone().map(|im| im as _)),
input_method_grab: Default::default(),
forward: Cell::new(false),
focus_follows_mouse: Cell::new(true),
@ -327,6 +330,8 @@ impl WlSeatGlobal {
marks: Default::default(),
modifiers_listener: EventListener::new(slf.clone()),
modifiers_forward: Default::default(),
simple_im: CloneCell::new(simple_im),
simple_im_enabled: Cell::new(true),
});
slf.pointer_cursor.set_owner(slf.clone());
slf.modifiers_listener
@ -371,7 +376,7 @@ impl WlSeatGlobal {
self.seat_kb_map.get()
}
pub fn input_method(&self) -> Option<Rc<ZwpInputMethodV2>> {
pub fn input_method(&self) -> Option<Rc<dyn InputMethod>> {
self.input_method.get()
}
@ -693,7 +698,7 @@ impl WlSeatGlobal {
}
}
if let Some(grab) = self.input_method_grab.get() {
grab.send_repeat_info();
grab.on_repeat_info();
}
}
@ -1315,7 +1320,7 @@ impl WlSeatGlobal {
tl.tl_set_visible(visible);
}
if let Some(im) = self.input_method.get() {
for (_, popup) in &im.popups {
for (_, popup) in im.popups() {
popup.update_visible();
}
}

View file

@ -981,15 +981,19 @@ impl WlSeatGlobal {
}
}
self.send_components(&mut components_changed, &kbvm_state);
match self.input_method_grab.get() {
Some(g) => g.on_key(time_usec, kc.to_evdev(), key_state, &kbvm_state.kb_state),
_ => self.keyboard_node.get().node_on_key(
let mut forward_to_node = true;
if let Some(g) = self.input_method_grab.get() {
forward_to_node =
g.on_key(time_usec, kc.to_evdev(), key_state, &kbvm_state.kb_state);
}
if forward_to_node {
self.keyboard_node.get().node_on_key(
self,
time_usec,
kc.to_evdev(),
key_state,
&kbvm_state.kb_state,
),
)
}
self.for_each_ei_seat(|ei_seat| {
ei_seat.handle_key(time_usec, kc.to_evdev(), key_state, &kbvm_state.kb_state);
@ -1064,9 +1068,12 @@ impl WlSeatGlobal {
self.state.for_each_seat_tester(|t| {
t.send_modifiers(self.id, &kb_state.mods);
});
match self.input_method_grab.get() {
Some(g) => g.on_modifiers(kb_state),
_ => self.keyboard_node.get().node_on_mods(self, kb_state),
let mut forward_to_node = true;
if let Some(g) = self.input_method_grab.get() {
forward_to_node = g.on_modifiers(kb_state);
}
if forward_to_node {
self.keyboard_node.get().node_on_mods(self, kb_state)
}
}

View file

@ -1,16 +1,21 @@
use {
crate::ifs::{
wl_seat::{
WlSeatGlobal,
text_input::{
zwp_input_method_v2::ZwpInputMethodV2, zwp_text_input_v3::ZwpTextInputV3,
crate::{
backend::KeyState,
ifs::{
wl_seat::{
WlSeatGlobal,
text_input::{simple_im::SimpleIm, zwp_text_input_v3::ZwpTextInputV3},
},
wl_surface::{WlSurface, zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2},
},
wl_surface::WlSurface,
keyboard::KeyboardState,
utils::smallmap::SmallMap,
wire::ZwpInputPopupSurfaceV2Id,
},
std::rc::Rc,
};
pub mod simple_im;
pub mod zwp_input_method_keyboard_grab_v2;
pub mod zwp_input_method_manager_v2;
pub mod zwp_input_method_v2;
@ -22,10 +27,30 @@ const MAX_TEXT_SIZE: usize = 4000;
pub struct TextInputConnection {
pub seat: Rc<WlSeatGlobal>,
pub text_input: Rc<ZwpTextInputV3>,
pub input_method: Rc<ZwpInputMethodV2>,
pub input_method: Rc<dyn InputMethod>,
pub surface: Rc<WlSurface>,
}
pub trait InputMethod {
fn set_connection(&self, con: Option<&Rc<TextInputConnection>>);
fn popups(&self) -> &SmallMap<ZwpInputPopupSurfaceV2Id, Rc<ZwpInputPopupSurfaceV2>, 1>;
fn activate(&self);
fn deactivate(&self);
fn content_type(&self, hint: u32, purpose: u32);
fn text_change_cause(&self, cause: u32);
fn surrounding_text(&self, text: &str, cursor: u32, anchor: u32);
fn done(self: Rc<Self>, seat: &WlSeatGlobal);
fn is_simple(&self) -> bool;
fn cancel_simple(&self, seat: &WlSeatGlobal);
fn enable_unicode_input(&self);
}
pub trait InputMethodKeyboardGrab {
fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) -> bool;
fn on_modifiers(&self, kb_state: &KeyboardState) -> bool;
fn on_repeat_info(&self);
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TextConnectReason {
TextInputEnabled,
@ -40,6 +65,76 @@ pub enum TextDisconnectReason {
}
impl WlSeatGlobal {
pub fn enable_unicode_input(&self) {
if let Some(im) = self.input_method.get() {
im.enable_unicode_input();
}
}
pub fn set_simple_im_enabled(self: &Rc<Self>, enabled: bool) {
if self.simple_im_enabled.replace(enabled) == enabled {
return;
}
if enabled {
if self.input_method.is_none()
&& let Some(im) = self.simple_im.get()
{
self.set_input_method(im);
}
} else {
if let Some(im) = self.input_method.get()
&& im.is_simple()
{
self.input_method.take();
im.cancel_simple(self);
}
}
}
pub fn simple_im_enabled(&self) -> bool {
self.simple_im_enabled.get()
}
pub fn reload_simple_im(self: &Rc<Self>) {
let im = SimpleIm::new(&self.state.kb_ctx.ctx);
self.simple_im.set(im.clone());
if self.simple_im_enabled.get() && self.can_set_new_im() {
if let Some(im) = im {
self.set_input_method(im);
} else if let Some(old) = self.input_method.take() {
old.cancel_simple(self);
}
}
}
fn can_set_new_im(&self) -> bool {
match self.input_method.get() {
None => true,
Some(im) => im.is_simple(),
}
}
fn cannot_set_new_im(&self) -> bool {
!self.can_set_new_im()
}
fn set_input_method(self: &Rc<Self>, im: Rc<dyn InputMethod>) {
if let Some(old) = self.input_method.take() {
old.cancel_simple(self);
}
self.input_method.set(Some(im));
self.create_text_input_connection(TextConnectReason::InputMethodCreated);
}
fn remove_input_method(self: &Rc<Self>) {
self.input_method.take();
if self.simple_im_enabled.get()
&& let Some(im) = self.simple_im.get()
{
self.set_input_method(im);
}
}
fn create_text_input_connection(self: &Rc<Self>, text_connect_reason: TextConnectReason) {
let Some(im) = self.input_method.get() else {
return;
@ -67,7 +162,7 @@ impl WlSeatGlobal {
impl TextInputConnection {
fn connect(self: &Rc<Self>, reason: TextConnectReason) {
self.input_method.connection.set(Some(self.clone()));
self.input_method.set_connection(Some(self));
self.text_input.connection.set(Some(self.clone()));
self.surface
.text_input_connections
@ -75,22 +170,27 @@ impl TextInputConnection {
self.input_method.activate();
if reason == TextConnectReason::InputMethodCreated {
self.text_input.send_all_to(&self.input_method);
self.input_method.send_done();
self.text_input.send_all_to(&*self.input_method);
self.input_method.clone().done(&self.seat);
}
}
pub fn disconnect(&self, reason: TextDisconnectReason) {
self.text_input.connection.take();
self.input_method.connection.take();
self.input_method.set_connection(None);
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 {
self.input_method.deactivate();
self.input_method.clone().done(&self.seat);
for (_, popup) in self.input_method.popups() {
popup.update_visible();
}
}
if reason != TextDisconnectReason::TextInputDisabled {
self.text_input.send_preedit_string(None, 0, 0);
self.text_input.send_commit_string(None);
self.text_input.send_done();
}
}
}

View file

@ -0,0 +1,309 @@
use {
crate::{
backend::KeyState,
ifs::{
wl_seat::{
WlSeatGlobal,
text_input::{
InputMethod, InputMethodKeyboardGrab, TextDisconnectReason, TextInputConnection,
},
},
wl_surface::zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2,
},
keyboard::KeyboardState,
utils::{clonecell::CloneCell, smallmap::SmallMap},
wire::ZwpInputPopupSurfaceV2Id,
},
kbvm::{
Keycode, ModifierMask, syms,
xkb::{
self,
compose::{self, FeedResult},
diagnostic::WriteToLog,
},
},
std::{
cell::{Cell, RefCell},
fmt::Write,
rc::Rc,
},
};
pub struct SimpleIm {
con: CloneCell<Option<Rc<TextInputConnection>>>,
popups: SmallMap<ZwpInputPopupSurfaceV2Id, Rc<ZwpInputPopupSurfaceV2>, 1>,
active: Cell<bool>,
activate: Cell<Option<bool>>,
table: compose::ComposeTable,
initial_state: compose::State,
states: RefCell<Vec<State>>,
unicode_input: RefCell<UnicodeInput>,
unicode_input_enabled: Cell<bool>,
}
struct State {
state: compose::State,
char: char,
}
#[derive(Default)]
struct UnicodeInput {
text: String,
cp: u32,
cursor: i32,
chars: usize,
}
impl SimpleIm {
pub fn new(ctx: &xkb::Context) -> Option<Rc<Self>> {
let table = ctx.compose_table_builder().build(WriteToLog)?;
Some(Rc::new(Self {
con: Default::default(),
popups: Default::default(),
active: Default::default(),
activate: Default::default(),
states: Default::default(),
initial_state: table.create_state(),
table,
unicode_input: Default::default(),
unicode_input_enabled: Default::default(),
}))
}
}
impl InputMethod for SimpleIm {
fn set_connection(&self, con: Option<&Rc<TextInputConnection>>) {
self.con.set(con.cloned());
}
fn popups(&self) -> &SmallMap<ZwpInputPopupSurfaceV2Id, Rc<ZwpInputPopupSurfaceV2>, 1> {
&self.popups
}
fn activate(&self) {
self.activate.set(Some(true));
}
fn deactivate(&self) {
self.activate.set(Some(false));
}
fn content_type(&self, _hint: u32, _purpose: u32) {
// nothing
}
fn text_change_cause(&self, _cause: u32) {
// nothing
}
fn surrounding_text(&self, _text: &str, _cursor: u32, _anchor: u32) {
// nothing
}
fn done(self: Rc<Self>, seat: &WlSeatGlobal) {
let Some(active) = self.activate.take() else {
return;
};
self.active.set(active);
if active {
self.states.borrow_mut().clear();
self.unicode_input_enabled.set(false);
seat.input_method_grab.set(Some(self));
} else {
seat.input_method_grab.take();
}
}
fn is_simple(&self) -> bool {
true
}
fn cancel_simple(&self, seat: &WlSeatGlobal) {
seat.input_method_grab.take();
if let Some(con) = self.con.get() {
con.disconnect(TextDisconnectReason::InputMethodDestroyed);
}
}
fn enable_unicode_input(&self) {
if !self.active.get() {
return;
}
let Some(con) = self.con.get() else {
return;
};
if self.unicode_input_enabled.replace(true) {
return;
}
self.states.borrow_mut().clear();
let ui = &mut *self.unicode_input.borrow_mut();
ui.cp = 0;
ui.chars = 0;
ui.flush_preedit(&con);
}
}
impl UnicodeInput {
fn update_text(&mut self) {
self.text.clear();
if self.chars == 0 {
let _ = write!(self.text, "U+");
self.cursor = self.text.len() as _;
return;
}
let _ = write!(self.text, "U+{:x}", self.cp);
self.cursor = self.text.len() as _;
if let Some(char) = char::from_u32(self.cp) {
let _ = write!(self.text, " = {}", char);
}
}
fn flush_preedit(&mut self, con: &TextInputConnection) {
self.update_text();
con.text_input
.send_preedit_string(Some(&self.text), self.cursor, self.cursor);
con.text_input.send_done();
}
}
impl InputMethodKeyboardGrab for SimpleIm {
fn on_key(&self, _time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) -> bool {
if state != KeyState::Pressed {
return true;
}
let Some(con) = self.con.get() else {
return true;
};
let mut buf = [0; 4];
let mut forward_to_node = true;
if self.unicode_input_enabled.get() {
forward_to_node = false;
}
let states = &mut *self.states.borrow_mut();
let ui = &mut self.unicode_input.borrow_mut();
let lookup = kb_state.map.lookup_table.lookup(
kb_state.mods.group,
kb_state.mods.mods,
Keycode::from_evdev(key),
);
let mods = lookup.remaining_mods();
let is_control = mods.contains(ModifierMask::CONTROL);
for sym in lookup {
let sym = sym.keysym();
if self.unicode_input_enabled.get() {
let is_terminator = matches!(
sym,
syms::Return | syms::KP_Enter | syms::space | syms::KP_Space
);
if (is_terminator || (sym == syms::j && is_control))
&& ui.chars > 0
&& let Some(char) = char::from_u32(ui.cp)
{
self.unicode_input_enabled.set(false);
let s = char.encode_utf8(&mut buf);
con.text_input.send_preedit_string(None, 0, 0);
con.text_input.send_commit_string(Some(s));
con.text_input.send_done();
} else if sym == syms::Escape
|| (sym == syms::c && is_control)
|| (ui.chars == 0 && matches!(sym, syms::w | syms::d) && is_control)
{
self.unicode_input_enabled.set(false);
con.text_input.send_preedit_string(None, 0, 0);
con.text_input.send_done();
} else if sym == syms::BackSpace && ui.chars > 0 {
ui.chars -= 1;
ui.cp >>= 4;
ui.flush_preedit(&con);
} else if sym == syms::w && is_control {
ui.chars = 0;
ui.cp = 0;
ui.flush_preedit(&con);
} else if let Some(c) = sym.char()
&& ui.chars < 6
&& !is_control
{
let c = match c {
'0'..='9' => c as u32 - '0' as u32,
'a'..='f' => c as u32 - 'a' as u32 + 10,
'A'..='F' => c as u32 - 'A' as u32 + 10,
_ => continue,
};
ui.cp = (ui.cp << 4) | c;
if ui.cp != 0 {
ui.chars += 1;
ui.flush_preedit(&con);
}
}
continue;
}
let mut new_state = states
.last()
.map(|s| s.state.clone())
.unwrap_or_else(|| self.initial_state.clone());
let Some(fr) = self.table.feed(&mut new_state, sym) else {
continue;
};
forward_to_node = false;
let mut send_preedit = |char: char| {
let s = char.encode_utf8(&mut buf);
let len = s.len() as i32;
con.text_input.send_preedit_string(Some(s), len, len);
};
match fr {
FeedResult::Pending => {
let char = sym.char().unwrap_or('·');
states.push(State {
state: new_state,
char,
});
send_preedit(char);
con.text_input.send_done();
}
FeedResult::Aborted
if sym == syms::Escape || (matches!(sym, syms::c | syms::w) && is_control) =>
{
states.clear();
con.text_input.send_preedit_string(None, 0, 0);
con.text_input.send_done();
}
FeedResult::Aborted if sym == syms::BackSpace => {
states.pop();
if let Some(state) = states.last() {
send_preedit(state.char);
} else {
con.text_input.send_preedit_string(None, 0, 0);
}
con.text_input.send_done();
}
FeedResult::Aborted => {
// nothing
}
FeedResult::Composed { string, keysym } => {
states.clear();
let s = if string.is_some() {
string
} else if let Some(sym) = keysym
&& let Some(char) = sym.char()
{
Some(char.encode_utf8(&mut buf) as &str)
} else {
None
};
con.text_input.send_preedit_string(None, 0, 0);
con.text_input.send_commit_string(s);
con.text_input.send_done();
}
}
}
forward_to_node
}
fn on_modifiers(&self, _kb_state: &KeyboardState) -> bool {
true
}
fn on_repeat_info(&self) {
// nothing
}
}

View file

@ -2,7 +2,10 @@ use {
crate::{
backend::KeyState,
client::{Client, ClientError},
ifs::wl_seat::{text_input::zwp_input_method_v2::ZwpInputMethodV2, wl_keyboard},
ifs::wl_seat::{
text_input::{InputMethodKeyboardGrab, zwp_input_method_v2::ZwpInputMethodV2},
wl_keyboard,
},
keyboard::{KeyboardState, KeyboardStateId},
leaks::Tracker,
object::{Object, Version},
@ -49,7 +52,7 @@ impl ZwpInputMethodKeyboardGrabV2 {
self.kb_state_id.set(kb_state.id);
}
pub fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) {
fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) {
let serial = self.client.next_serial();
if self.kb_state_id.get() != kb_state.id {
self.update_state(serial, kb_state);
@ -70,7 +73,7 @@ impl ZwpInputMethodKeyboardGrabV2 {
})
}
pub fn on_modifiers(&self, kb_state: &KeyboardState) {
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);
@ -99,6 +102,22 @@ impl ZwpInputMethodKeyboardGrabV2 {
}
}
impl InputMethodKeyboardGrab for ZwpInputMethodKeyboardGrabV2 {
fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) -> bool {
self.on_key(time_usec, key, state, kb_state);
false
}
fn on_modifiers(&self, kb_state: &KeyboardState) -> bool {
self.on_modifiers(kb_state);
false
}
fn on_repeat_info(&self) {
self.send_repeat_info();
}
}
impl ZwpInputMethodKeyboardGrabV2RequestHandler for ZwpInputMethodKeyboardGrabV2 {
type Error = ZwpInputMethodKeyboardGrabV2Error;

View file

@ -2,7 +2,7 @@ use {
crate::{
client::{CAP_INPUT_METHOD, Client, ClientCaps, ClientError},
globals::{Global, GlobalName},
ifs::wl_seat::text_input::{TextConnectReason, zwp_input_method_v2::ZwpInputMethodV2},
ifs::wl_seat::text_input::zwp_input_method_v2::ZwpInputMethodV2,
leaks::Tracker,
object::{Object, Version},
wire::{ZwpInputMethodManagerV2Id, zwp_input_method_manager_v2::*},
@ -72,7 +72,7 @@ impl ZwpInputMethodManagerV2RequestHandler for ZwpInputMethodManagerV2 {
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 inert = seat.global.cannot_set_new_im();
let im = Rc::new(ZwpInputMethodV2 {
id: req.input_method,
client: self.client.clone(),
@ -90,9 +90,7 @@ impl ZwpInputMethodManagerV2RequestHandler for ZwpInputMethodManagerV2 {
if inert {
im.send_unavailable();
} else {
seat.global.input_method.set(Some(im));
seat.global
.create_text_input_connection(TextConnectReason::InputMethodCreated);
seat.global.set_input_method(im);
}
Ok(())
}

View file

@ -5,7 +5,7 @@ use {
wl_seat::{
WlSeatGlobal,
text_input::{
MAX_TEXT_SIZE, TextDisconnectReason, TextInputConnection,
InputMethod, MAX_TEXT_SIZE, TextDisconnectReason, TextInputConnection,
zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2,
},
},
@ -53,7 +53,7 @@ impl ZwpInputMethodV2 {
}
self.popups.clear();
if !self.inert {
self.seat.input_method.take();
self.seat.remove_input_method();
}
}
@ -104,6 +104,52 @@ impl ZwpInputMethodV2 {
}
}
impl InputMethod for ZwpInputMethodV2 {
fn set_connection(&self, con: Option<&Rc<TextInputConnection>>) {
self.connection.set(con.cloned());
}
fn popups(&self) -> &SmallMap<ZwpInputPopupSurfaceV2Id, Rc<ZwpInputPopupSurfaceV2>, 1> {
&self.popups
}
fn activate(&self) {
self.activate();
}
fn deactivate(&self) {
self.send_deactivate();
}
fn content_type(&self, hint: u32, purpose: u32) {
self.send_content_type(hint, purpose);
}
fn text_change_cause(&self, cause: u32) {
self.send_text_change_cause(cause);
}
fn surrounding_text(&self, text: &str, cursor: u32, anchor: u32) {
self.send_surrounding_text(text, cursor, anchor);
}
fn done(self: Rc<Self>, _seat: &WlSeatGlobal) {
(*self).send_done();
}
fn is_simple(&self) -> bool {
false
}
fn cancel_simple(&self, _seat: &WlSeatGlobal) {
unreachable!();
}
fn enable_unicode_input(&self) {
// nothing
}
}
impl ZwpInputMethodV2RequestHandler for ZwpInputMethodV2 {
type Error = ZwpInputMethodV2Error;

View file

@ -5,8 +5,8 @@ use {
wl_seat::{
WlSeatGlobal,
text_input::{
MAX_TEXT_SIZE, TextConnectReason, TextDisconnectReason, TextInputConnection,
zwp_input_method_v2::ZwpInputMethodV2,
InputMethod, MAX_TEXT_SIZE, TextConnectReason, TextDisconnectReason,
TextInputConnection,
},
},
wl_surface::WlSurface,
@ -72,13 +72,13 @@ impl ZwpTextInputV3 {
}
}
pub fn send_all_to(&self, im: &ZwpInputMethodV2) {
pub fn send_all_to(&self, im: &dyn InputMethod) {
let state = &*self.state.borrow();
{
let (a, b, c) = &state.surrounding_text;
im.send_surrounding_text(a, *b, *c);
im.surrounding_text(a, *b, *c);
}
im.send_content_type(state.content_type.0, state.content_type.1);
im.content_type(state.content_type.0, state.content_type.1);
}
pub fn send_enter(&self, surface: &WlSurface) {
@ -255,7 +255,7 @@ impl ZwpTextInputV3RequestHandler for ZwpTextInputV3 {
if state.cursor_rectangle != val
&& let Some(con) = &con
{
for (_, popup) in &con.input_method.popups {
for (_, popup) in con.input_method.popups() {
popup.schedule_positioning();
}
}
@ -264,26 +264,26 @@ impl ZwpTextInputV3RequestHandler for ZwpTextInputV3 {
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);
con.input_method.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);
con.input_method.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);
con.input_method.surrounding_text(&val.0, val.1, val.2);
}
state.surrounding_text = val;
}
if sent_any && let Some(con) = &con {
con.input_method.send_done();
con.input_method.clone().done(&self.seat);
}
Ok(())
}

View file

@ -784,7 +784,7 @@ impl WlSurface {
}
}
for (_, con) in &self.text_input_connections {
for (_, popup) in &con.input_method.popups {
for (_, popup) in con.input_method.popups() {
popup.schedule_positioning();
}
}

View file

@ -335,7 +335,7 @@ impl ToolClient {
self_id: s.registry,
name: s.jay_compositor.0,
interface: JayCompositor.name(),
version: s.jay_compositor.1.min(21),
version: s.jay_compositor.1.min(22),
id: id.into(),
});
self.jay_compositor.set(Some(id));

View file

@ -85,6 +85,10 @@ pub enum SimpleCommand {
CreateMark,
JumpToMark,
PopMode(bool),
EnableSimpleIm(bool),
ToggleSimpleImEnabled,
ReloadSimpleIm,
EnableUnicodeInput,
}
#[derive(Debug, Clone)]
@ -446,6 +450,11 @@ pub struct Vrr {
pub cursor_hz: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct SimpleIm {
pub enabled: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct Xwayland {
pub scaling_mode: Option<XScalingMode>,
@ -520,6 +529,7 @@ pub struct Config {
pub middle_click_paste: Option<bool>,
pub input_modes: AHashMap<String, InputMode>,
pub workspace_display_order: Option<WorkspaceDisplayOrder>,
pub simple_im: Option<SimpleIm>,
}
#[derive(Debug, Error)]

View file

@ -39,6 +39,7 @@ mod output;
mod output_match;
mod repeat_rate;
pub mod shortcuts;
mod simple_im;
mod status;
mod tearing;
mod theme;

View file

@ -155,6 +155,11 @@ impl ActionParser<'_> {
"jump-to-mark" => JumpToMark,
"clear-modes" => PopMode(false),
"pop-mode" => PopMode(true),
"enable-simple-im" => EnableSimpleIm(true),
"disable-simple-im" => EnableSimpleIm(false),
"toggle-simple-im-enabled" => ToggleSimpleImEnabled,
"reload-simple-im" => ReloadSimpleIm,
"enable-unicode-input" => EnableUnicodeInput,
_ => {
return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)

View file

@ -30,6 +30,7 @@ use {
ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError,
parse_modified_keysym_str,
},
simple_im::SimpleImParser,
status::StatusParser,
tearing::TearingParser,
theme::ThemeParser,
@ -139,7 +140,13 @@ impl Parser for ConfigParser<'_> {
show_bar,
focus_history_val,
),
(middle_click_paste, input_modes_val, workspace_display_order_val, auto_reload),
(
middle_click_paste,
input_modes_val,
workspace_display_order_val,
auto_reload,
simple_im_val,
),
) = ext.extract((
(
opt(val("keymap")),
@ -194,6 +201,7 @@ impl Parser for ConfigParser<'_> {
opt(val("modes")),
opt(val("workspace-display-order")),
recover(opt(bol("auto-reload"))),
opt(val("simple-im")),
),
))?;
let mut keymap = None;
@ -505,6 +513,15 @@ impl Parser for ConfigParser<'_> {
}
}
}
let mut simple_im = None;
if let Some(value) = simple_im_val {
match value.parse(&mut SimpleImParser(self.0)) {
Ok(v) => simple_im = Some(v),
Err(e) => {
log::warn!("Could not parse simple IM setting: {}", self.0.error(e));
}
}
}
Ok(Config {
keymap,
repeat_rate,
@ -549,6 +566,7 @@ impl Parser for ConfigParser<'_> {
middle_click_paste: middle_click_paste.despan(),
input_modes,
workspace_display_order,
simple_im,
})
}
}

View file

@ -0,0 +1,44 @@
use {
crate::{
config::{
SimpleIm,
context::Context,
extractor::{Extractor, ExtractorError, bol, opt, recover},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum SimpleImParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct SimpleImParser<'a>(pub &'a Context<'a>);
impl Parser for SimpleImParser<'_> {
type Value = SimpleIm;
type Error = SimpleImParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (enabled,) = ext.extract((recover(opt(bol("enabled"))),))?;
Ok(SimpleIm {
enabled: enabled.despan(),
})
}
}

View file

@ -224,6 +224,22 @@ impl Action {
let state = state.clone();
b.new(move || state.pop_mode(pop))
}
SimpleCommand::EnableSimpleIm(v) => {
let persistent = state.persistent.clone();
b.new(move || persistent.seat.set_simple_im_enabled(v))
}
SimpleCommand::ToggleSimpleImEnabled => {
let persistent = state.persistent.clone();
b.new(move || persistent.seat.toggle_simple_im_enabled())
}
SimpleCommand::ReloadSimpleIm => {
let persistent = state.persistent.clone();
b.new(move || persistent.seat.reload_simple_im())
}
SimpleCommand::EnableUnicodeInput => {
let persistent = state.persistent.clone();
b.new(move || persistent.seat.enable_unicode_input())
}
},
Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
@ -1559,6 +1575,11 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
if let Some(v) = config.workspace_display_order {
set_workspace_display_order(v);
}
if let Some(simple_im) = config.simple_im {
if let Some(enabled) = simple_im.enabled {
persistent.seat.set_simple_im_enabled(enabled);
}
}
}
fn create_command(exec: &Exec) -> Command {

View file

@ -361,7 +361,7 @@
]
},
{
"description": "Sets the log level of the compositor..\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-j = { type = \"set-log-level\", level = \"debug\" }\n ```\n",
"description": "Sets the log level of the compositor.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-j = { type = \"set-log-level\", level = \"debug\" }\n ```\n",
"type": "object",
"properties": {
"type": {
@ -1049,6 +1049,10 @@
"workspace-display-order": {
"description": "Configures the order of workspaces displayed.\n\nThe default is `manual`.\n\n- Example:\n\n ```toml\n workspace-display-order = \"sorted\"\n ```\n",
"$ref": "#/$defs/WorkspaceDisplayOrder"
},
"simple-im": {
"description": "Configures the simple, XCompose based input method.\n\nBy default, the input method is enabled. \n\nEven if the input method is enabled, it will only be used if there is no\nrunning external IM.\n\n- Example:\n\n ```toml\n [simple-im]\n enabled = false\n ```\n",
"$ref": "#/$defs/SimpleIm"
}
},
"required": []
@ -1835,9 +1839,25 @@
"create-mark",
"jump-to-mark",
"clear-modes",
"pop-mode"
"pop-mode",
"enable-simple-im",
"disable-simple-im",
"toggle-simple-im-enabled",
"reload-simple-im",
"enable-unicode-input"
]
},
"SimpleIm": {
"description": "Describes the settings of the simple, XCompose based input method.\n\n- Example:\n\n ```toml\n [simple-im]\n enabled = false\n ```\n",
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether the input method is enabled.\n\nEven if the input method is enabled, it will only be used if there is no\nrunning external IM.\n"
}
},
"required": []
},
"Status": {
"description": "The configuration of a status program whose output will be shown in the bar.\n\n- Example:\n\n ```toml\n [status]\n format = \"i3bar\"\n exec = \"i3status\"\n ```\n",
"type": "object",

View file

@ -538,7 +538,7 @@ This table is a tagged union. The variant is determined by the `type` field. It
- `set-log-level`:
Sets the log level of the compositor..
Sets the log level of the compositor.
- Example:
@ -2145,6 +2145,24 @@ The table has the following fields:
The value of this field should be a [WorkspaceDisplayOrder](#types-WorkspaceDisplayOrder).
- `simple-im` (optional):
Configures the simple, XCompose based input method.
By default, the input method is enabled.
Even if the input method is enabled, it will only be used if there is no
running external IM.
- Example:
```toml
[simple-im]
enabled = false
```
The value of this field should be a [SimpleIm](#types-SimpleIm).
<a name="types-Connector"></a>
### `Connector`
@ -4177,6 +4195,59 @@ The string should have one of the following values:
Pops the topmost mode from the input-mode stack.
- `enable-simple-im`:
Enables the simple, XCompose based input method.
Even if the input method is enabled, it will only be used if there is no
running external IM.
- `disable-simple-im`:
Disables the simple, XCompose based input method.
- `toggle-simple-im-enabled`:
Toggles whether the simple, XCompose based input method is enabled.
- `reload-simple-im`:
Reloads the simple, XCompose based input method.
This is useful if you change the XCompose files after starting the compositor.
- `enable-unicode-input`:
Enables Unicode input in the simple, XCompose based input method.
This has no effect if the simple IM is not currently active.
<a name="types-SimpleIm"></a>
### `SimpleIm`
Describes the settings of the simple, XCompose based input method.
- Example:
```toml
[simple-im]
enabled = false
```
Values of this type should be tables.
The table has the following fields:
- `enabled` (optional):
Whether the input method is enabled.
Even if the input method is enabled, it will only be used if there is no
running external IM.
The value of this field should be a boolean.
<a name="types-Status"></a>

View file

@ -497,7 +497,7 @@ Action:
ref: Theme
set-log-level:
description: |
Sets the log level of the compositor..
Sets the log level of the compositor.
- Example:
@ -1033,6 +1033,28 @@ SimpleActionName:
description: Disables all previously set input modes, clearing the input-mode stack.
- value: pop-mode
description: Pops the topmost mode from the input-mode stack.
- value: enable-simple-im
description: |
Enables the simple, XCompose based input method.
Even if the input method is enabled, it will only be used if there is no
running external IM.
- value: disable-simple-im
description: |
Disables the simple, XCompose based input method.
- value: toggle-simple-im-enabled
description: |
Toggles whether the simple, XCompose based input method is enabled.
- value: reload-simple-im
description: |
Reloads the simple, XCompose based input method.
This is useful if you change the XCompose files after starting the compositor.
- value: enable-unicode-input
description: |
Enables Unicode input in the simple, XCompose based input method.
This has no effect if the simple IM is not currently active.
Color:
@ -2881,6 +2903,23 @@ Config:
```toml
workspace-display-order = "sorted"
```
simple-im:
ref: SimpleIm
required: false
description: |
Configures the simple, XCompose based input method.
By default, the input method is enabled.
Even if the input method is enabled, it will only be used if there is no
running external IM.
- Example:
```toml
[simple-im]
enabled = false
```
Idle:
@ -4195,3 +4234,25 @@ ClientCapabilities:
description: An array of masks that are OR'd.
items:
ref: ClientCapabilities
SimpleIm:
kind: table
description: |
Describes the settings of the simple, XCompose based input method.
- Example:
```toml
[simple-im]
enabled = false
```
fields:
enabled:
kind: boolean
required: false
description: |
Whether the input method is enabled.
Even if the input method is enabled, it will only be used if there is no
running external IM.

View file

@ -134,6 +134,15 @@ request set_middle_button_emulation (since = 19) {
enabled: u32,
}
request set_simple_im_enabled (since = 22) {
seat: str,
enabled: u32,
}
request reload_simple_im (since = 22) {
seat: str,
}
# events
event seat {