Skip to content

Commit

Permalink
Add new IME event for desktop platforms
Browse files Browse the repository at this point in the history
This commit brings new IME event to account for preedit state of input
method, also adding `Window::set_ime_allowed` to toggle IME input on
the particular window.

This commit implements API as designed in #1497 for Wayland, X11, and
macOS.

Co-authored-by: Artur Kovacs <kovacs.artur.barnabas@gmail.com>
Co-authored-by: Murarth <murarth@gmail.com>
Co-authored-by: Yusuke Kominami <yukke.konan@gmail.com>
  • Loading branch information
4 people committed Apr 6, 2022
1 parent ab1f636 commit 5db61c1
Show file tree
Hide file tree
Showing 26 changed files with 963 additions and 256 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ And please only add new entries to the top of this list, right below the `# Unre
- On Wayland, fix polling during consecutive `EventLoop::run_return` invocations.
- On Windows, fix race issue creating fullscreen windows with `WindowBuilder::with_fullscreen`
- On Android, `virtual_keycode` for `KeyboardInput` events is now filled in where a suitable match is found.
- **Breaking:** Added new `WindowEvent::IME` supported on macOS, X11, and Wayland.
- Added `Window::set_ime_allowed` supported on macOS, X11, and Wayland.
- **Breaking:** IME input on macOS, X11, and Wayland won't be received unless it's explicitly allowed via `Window::set_ime_allowed` and new `WindowEvent::IME` events are handled.

# 0.26.1 (2022-01-05)

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ features = [
]

[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies]
wayland-client = { version = "0.29", default_features = false, features = ["use_system_lib"], optional = true }
wayland-protocols = { version = "0.29", features = [ "staging_protocols"], optional = true }
wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true }
wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true }
sctk = { package = "smithay-client-toolkit", version = "0.15.1", default_features = false, features = ["calloop"], optional = true }
mio = { version = "0.8", features = ["os-ext"], optional = true }
x11-dl = { version = "2.18.5", optional = true }
Expand Down
97 changes: 97 additions & 0 deletions examples/ime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use log::LevelFilter;
use simple_logger::SimpleLogger;
use winit::{
dpi::PhysicalPosition,
event::{ElementState, Event, VirtualKeyCode, WindowEvent, IME},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};

fn main() {
SimpleLogger::new()
.with_level(LevelFilter::Trace)
.init()
.unwrap();

println!("Ime position will system default");
println!("Click to set ime position to cursor's");
println!("Press F2 to toggle IME. See the documentation of `set_ime_allowed` for more info");

let event_loop = EventLoop::new();

let window = WindowBuilder::new()
.with_inner_size(winit::dpi::LogicalSize::new(256f64, 128f64))
.build(&event_loop)
.unwrap();

let mut ime_allowed = true;
window.set_ime_allowed(ime_allowed);

let mut may_show_ime = false;
let mut cursor_position = PhysicalPosition::new(0.0, 0.0);
let mut ime_pos = PhysicalPosition::new(0.0, 0.0);

event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::WindowEvent {
event: WindowEvent::CursorMoved { position, .. },
..
} => {
cursor_position = position;
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
state: ElementState::Released,
..
},
..
} => {
println!(
"Setting ime position to {}, {}",
cursor_position.x, cursor_position.y
);
ime_pos = cursor_position;
if may_show_ime {
window.set_ime_position(ime_pos);
}
}
Event::WindowEvent {
event: WindowEvent::IME(event),
..
} => {
println!("{:?}", event);
may_show_ime = event != IME::Disabled;
if may_show_ime {
window.set_ime_position(ime_pos);
}
}
Event::WindowEvent {
event: WindowEvent::ReceivedCharacter(ch),
..
} => {
println!("ch: {:?}", ch);
}
Event::WindowEvent {
event: WindowEvent::KeyboardInput { input, .. },
..
} => {
println!("key: {:?}", input);

if input.state == ElementState::Pressed
&& input.virtual_keycode == Some(VirtualKeyCode::F2)
{
ime_allowed = !ime_allowed;
window.set_ime_allowed(ime_allowed);
println!("\nIME: {}\n", ime_allowed);
}
}
_ => (),
}
});
}
53 changes: 0 additions & 53 deletions examples/set_ime_position.rs

This file was deleted.

28 changes: 27 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ pub enum WindowEvent<'a> {
/// issue, and it should get fixed - but it's the current state of the API.
ModifiersChanged(ModifiersState),

/// An event from IME
IME(IME),

/// The cursor has moved on the window.
CursorMoved {
device_id: DeviceId,
Expand Down Expand Up @@ -376,7 +379,7 @@ impl Clone for WindowEvent<'static> {
input: *input,
is_synthetic: *is_synthetic,
},

IME(preedit_state) => IME(preedit_state.clone()),
ModifiersChanged(modifiers) => ModifiersChanged(*modifiers),
#[allow(deprecated)]
CursorMoved {
Expand Down Expand Up @@ -468,6 +471,7 @@ impl<'a> WindowEvent<'a> {
is_synthetic,
}),
ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)),
IME(event) => Some(IME(event)),
#[allow(deprecated)]
CursorMoved {
device_id,
Expand Down Expand Up @@ -627,6 +631,28 @@ pub struct KeyboardInput {
pub modifiers: ModifiersState,
}

/// Describes an event from input method.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum IME {
/// Notifies when the IME was enabled.
Enabled,

/// Notifies when a new composing text should be set at the cursor position.
///
/// The value represents a pair of the preedit string and the cursor begin position and end
/// position. When both indices are `None`, the cursor should be hidden.
///
/// The cursor position is byte-wise indexed.
Preedit(String, Option<usize>, Option<usize>),

/// Notifies when text should be inserted into the editor widget.
Commit(String),

/// Notifies when the IME was disabled.
Disabled,
}

/// Describes touch-screen input state.
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
Expand Down
2 changes: 2 additions & 0 deletions src/platform_impl/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,8 @@ impl Window {

pub fn set_ime_position(&self, _position: Position) {}

pub fn set_ime_allowed(&self, _allowed: bool) {}

pub fn focus_window(&self) {}

pub fn request_user_attention(&self, _request_type: Option<window::UserAttentionType>) {}
Expand Down
4 changes: 4 additions & 0 deletions src/platform_impl/ios/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ impl Inner {
warn!("`Window::set_ime_position` is ignored on iOS")
}

pub fn set_ime_allowed(&self, _allowed: bool) {
warn!("`Window::set_ime_allowed` is ignored on iOS")
}

pub fn focus_window(&self) {
warn!("`Window::set_focus` is ignored on iOS")
}
Expand Down
5 changes: 5 additions & 0 deletions src/platform_impl/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,11 @@ impl Window {
x11_or_wayland!(match self; Window(w) => w.set_ime_position(position))
}

#[inline]
pub fn set_ime_allowed(&self, allowed: bool) {
x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed))
}

#[inline]
pub fn focus_window(&self) {
match self {
Expand Down
3 changes: 1 addition & 2 deletions src/platform_impl/linux/wayland/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ mod sink;
mod state;

pub use proxy::EventLoopProxy;
pub use sink::EventSink;
pub use state::WinitState;

use sink::EventSink;

type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>;

pub struct EventLoopWindowTarget<T> {
Expand Down
45 changes: 35 additions & 10 deletions src/platform_impl/linux/wayland/seat/text_input/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input
Event as TextInputEvent, ZwpTextInputV3,
};

use crate::event::WindowEvent;
use crate::event::{WindowEvent, IME};
use crate::platform_impl::wayland;
use crate::platform_impl::wayland::event_loop::WinitState;

use super::{TextInputHandler, TextInputInner};
use super::{Preedit, TextInputHandler, TextInputInner};

#[inline]
pub(super) fn handle_text_input(
Expand All @@ -30,8 +30,11 @@ pub(super) fn handle_text_input(
inner.target_window_id = Some(window_id);

// Enable text input on that surface.
text_input.enable();
text_input.commit();
if window_handle.ime_allowed.get() {
text_input.enable();
text_input.commit();
event_sink.push_window_event(WindowEvent::IME(IME::Enabled), window_id);
}

// Notify a window we're currently over about text input handler.
let text_input_handler = TextInputHandler {
Expand All @@ -58,19 +61,41 @@ pub(super) fn handle_text_input(
text_input: text_input.detach(),
};
window_handle.text_input_left(text_input_handler);
event_sink.push_window_event(WindowEvent::IME(IME::Disabled), window_id);
}
TextInputEvent::PreeditString {
text,
cursor_begin,
cursor_end,
} => {
let cursor_begin = usize::try_from(cursor_begin).ok();
let cursor_end = usize::try_from(cursor_end).ok();
let text = text.unwrap_or_default();
inner.pending_preedit = Some(Preedit {
text,
cursor_begin,
cursor_end,
});
}
TextInputEvent::CommitString { text } => {
// Update currenly commited string.
inner.commit_string = text;
// Update currenly commited string and reset previous preedit.
inner.pending_preedit = None;
inner.pending_commit = Some(text.unwrap_or_default());
}
TextInputEvent::Done { .. } => {
let (window_id, text) = match (inner.target_window_id, inner.commit_string.take()) {
(Some(window_id), Some(text)) => (window_id, text),
let window_id = match inner.target_window_id {
Some(window_id) => window_id,
_ => return,
};

for ch in text.chars() {
event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id);
if let Some(text) = inner.pending_commit.take() {
event_sink.push_window_event(WindowEvent::IME(IME::Commit(text)), window_id);
}

// Push preedit string we've got after latest commit.
if let Some(preedit) = inner.pending_preedit.take() {
let event = IME::Preedit(preedit.text, preedit.cursor_begin, preedit.cursor_end);
event_sink.push_window_event(WindowEvent::IME(event), window_id);
}
}
_ => (),
Expand Down
27 changes: 24 additions & 3 deletions src/platform_impl/linux/wayland/seat/text_input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ impl TextInputHandler {
self.text_input.set_cursor_rectangle(x, y, 0, 0);
self.text_input.commit();
}

#[inline]
pub fn set_input_allowed(&self, allowed: bool) {
if allowed {
self.text_input.enable();
} else {
self.text_input.disable();
}

self.text_input.commit();
}
}

/// A wrapper around text input to automatically destroy the object on `Drop`.
Expand Down Expand Up @@ -52,15 +63,25 @@ struct TextInputInner {
/// Currently focused surface.
target_window_id: Option<WindowId>,

/// Pending string to commit.
commit_string: Option<String>,
/// Pending commit event which will be dispatched on `text_input_v3::Done`.
pending_commit: Option<String>,

/// Pending preedit event which will be dispatched on `text_input_v3::Done`.
pending_preedit: Option<Preedit>,
}

struct Preedit {
text: String,
cursor_begin: Option<usize>,
cursor_end: Option<usize>,
}

impl TextInputInner {
fn new() -> Self {
Self {
target_window_id: None,
commit_string: None,
pending_commit: None,
pending_preedit: None,
}
}
}
Loading

0 comments on commit 5db61c1

Please sign in to comment.