mirror of
https://github.com/italicsjenga/winit-sonoma-fix.git
synced 2025-01-11 05:21:31 +11:00
Add new Ime
event for desktop platforms
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 desktop platforms. Co-authored-by: Artur Kovacs <kovacs.artur.barnabas@gmail.com> Co-authored-by: Markus Siglreithmaier <m.siglreith@gmail.com> Co-authored-by: Murarth <murarth@gmail.com> Co-authored-by: Yusuke Kominami <yukke.konan@gmail.com> Co-authored-by: moko256 <koutaro.mo@gmail.com>
This commit is contained in:
parent
b4175c1454
commit
f04fa5d54f
|
@ -41,6 +41,9 @@ And please only add new entries to the top of this list, right below the `# Unre
|
|||
- **Breaking:** Replaced `Window::with_app_id` and `Window::with_class` with `Window::with_name` on `WindowBuilderExtUnix`.
|
||||
- On Wayland and X11, fix window not resizing with `Window::set_inner_size` after calling `Window:set_resizable(false)`.
|
||||
- On Windows, fix wrong fullscreen monitors being recognized when handling WM_WINDOWPOSCHANGING messages
|
||||
- **Breaking:** Added new `WindowEvent::Ime` supported on desktop platforms.
|
||||
- Added `Window::set_ime_allowed` supported on desktop platforms.
|
||||
- **Breaking:** IME input on desktop platforms 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)
|
||||
|
||||
|
|
|
@ -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.4", default_features = false, features = ["calloop"], optional = true }
|
||||
mio = { version = "0.8", features = ["os-ext"], optional = true }
|
||||
x11-dl = { version = "2.18.5", optional = true }
|
||||
|
|
97
examples/ime.rs
Normal file
97
examples/ime.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use log::LevelFilter;
|
||||
use simple_logger::SimpleLogger;
|
||||
use winit::{
|
||||
dpi::PhysicalPosition,
|
||||
event::{ElementState, Event, Ime, VirtualKeyCode, WindowEvent},
|
||||
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);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
use simple_logger::SimpleLogger;
|
||||
use winit::{
|
||||
dpi::PhysicalPosition,
|
||||
event::{ElementState, Event, WindowEvent},
|
||||
event_loop::EventLoop,
|
||||
window::WindowBuilder,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
SimpleLogger::new().init().unwrap();
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
let window = WindowBuilder::new().build(&event_loop).unwrap();
|
||||
window.set_title("A fantastic window!");
|
||||
|
||||
println!("Ime position will system default");
|
||||
println!("Click to set ime position to cursor's");
|
||||
|
||||
let mut cursor_position = PhysicalPosition::new(0.0, 0.0);
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
control_flow.set_wait();
|
||||
|
||||
match event {
|
||||
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
|
||||
);
|
||||
window.set_ime_position(cursor_position);
|
||||
}
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => {
|
||||
control_flow.set_exit();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
87
src/event.rs
87
src/event.rs
|
@ -36,6 +36,8 @@
|
|||
use instant::Instant;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::window::Window;
|
||||
use crate::{
|
||||
dpi::{PhysicalPosition, PhysicalSize},
|
||||
platform_impl,
|
||||
|
@ -93,8 +95,7 @@ pub enum Event<'a, T: 'static> {
|
|||
/// This gets triggered in two scenarios:
|
||||
/// - The OS has performed an operation that's invalidated the window's contents (such as
|
||||
/// resizing the window).
|
||||
/// - The application has explicitly requested a redraw via
|
||||
/// [`Window::request_redraw`](crate::window::Window::request_redraw).
|
||||
/// - The application has explicitly requested a redraw via [`Window::request_redraw`].
|
||||
///
|
||||
/// During each iteration of the event loop, Winit will aggregate duplicate redraw requests
|
||||
/// into a single event, to help avoid duplicating rendering work.
|
||||
|
@ -206,7 +207,7 @@ pub enum StartCause {
|
|||
Init,
|
||||
}
|
||||
|
||||
/// Describes an event from a `Window`.
|
||||
/// Describes an event from a [`Window`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum WindowEvent<'a> {
|
||||
/// The size of the window has changed. Contains the client area's new dimensions.
|
||||
|
@ -240,6 +241,8 @@ pub enum WindowEvent<'a> {
|
|||
HoveredFileCancelled,
|
||||
|
||||
/// The window received a unicode character.
|
||||
///
|
||||
/// See also the [`Ime`](Self::Ime) event for more complex character sequences.
|
||||
ReceivedCharacter(char),
|
||||
|
||||
/// The window gained or lost focus.
|
||||
|
@ -270,6 +273,14 @@ pub enum WindowEvent<'a> {
|
|||
/// issue, and it should get fixed - but it's the current state of the API.
|
||||
ModifiersChanged(ModifiersState),
|
||||
|
||||
/// An event from input method.
|
||||
///
|
||||
/// **Note :** You have to explicitly enable this event using [`Window::set_ime_allowed`].
|
||||
///
|
||||
/// Platform-specific behavior:
|
||||
/// - **iOS / Android / Web :** Unsupported.
|
||||
Ime(Ime),
|
||||
|
||||
/// The cursor has moved on the window.
|
||||
CursorMoved {
|
||||
device_id: DeviceId,
|
||||
|
@ -376,7 +387,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 {
|
||||
|
@ -468,6 +479,7 @@ impl<'a> WindowEvent<'a> {
|
|||
is_synthetic,
|
||||
}),
|
||||
ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)),
|
||||
Ime(event) => Some(Ime(event)),
|
||||
#[allow(deprecated)]
|
||||
CursorMoved {
|
||||
device_id,
|
||||
|
@ -627,6 +639,73 @@ pub struct KeyboardInput {
|
|||
pub modifiers: ModifiersState,
|
||||
}
|
||||
|
||||
/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events.
|
||||
///
|
||||
/// This is also called a "composition event".
|
||||
///
|
||||
/// Most keypresses using a latin-like keyboard layout simply generate a [`WindowEvent::ReceivedCharacter`].
|
||||
/// However, one couldn't possibly have a key for every single unicode character that the user might want to type
|
||||
/// - so the solution operating systems employ is to allow the user to type these using _a sequence of keypresses_ instead.
|
||||
///
|
||||
/// A prominent example of this is accents - many keyboard layouts allow you to first click the "accent key", and then
|
||||
/// the character you want to apply the accent to. This will generate the following event sequence:
|
||||
/// ```ignore
|
||||
/// // Press "`" key
|
||||
/// Ime::Preedit("`", Some(0), Some(0))
|
||||
/// // Press "E" key
|
||||
/// Ime::Commit("é")
|
||||
/// ```
|
||||
///
|
||||
/// Additionally, certain input devices are configured to display a candidate box that allow the user to select the
|
||||
/// desired character interactively. (To properly position this box, you must use [`Window::set_ime_position`].)
|
||||
///
|
||||
/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keybaord the following event
|
||||
/// sequence could be obtained:
|
||||
/// ```ignore
|
||||
/// // Press "A" key
|
||||
/// Ime::Preedit("a", Some(1), Some(1))
|
||||
/// // Press "B" key
|
||||
/// Ime::Preedit("a b", Some(3), Some(3))
|
||||
/// // Press left arrow key
|
||||
/// Ime::Preedit("a b", Some(1), Some(1))
|
||||
/// // Press space key
|
||||
/// Ime::Preedit("啊b", Some(3), Some(3))
|
||||
/// // Press space key
|
||||
/// Ime::Commit("啊不")
|
||||
/// ```
|
||||
///
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Ime {
|
||||
/// Notifies when the IME was enabled.
|
||||
///
|
||||
/// After getting this event you could receive [`Preedit`](Self::Preedit) and
|
||||
/// [`Commit`](Self::Commit) events. You should also start performing IME related requests
|
||||
/// like [`Window::set_ime_position`].
|
||||
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 it's `None`, the cursor should be hidden.
|
||||
///
|
||||
/// The cursor position is byte-wise indexed.
|
||||
Preedit(String, Option<(usize, usize)>),
|
||||
|
||||
/// Notifies when text should be inserted into the editor widget.
|
||||
///
|
||||
/// Any pending [`Preedit`](Self::Preedit) must be cleared.
|
||||
Commit(String),
|
||||
|
||||
/// Notifies when the IME was disabled.
|
||||
///
|
||||
/// After receiving this event you won't get any more [`Preedit`](Self::Preedit) or
|
||||
/// [`Commit`](Self::Commit) events until the next [`Enabled`](Self::Enabled) event. You can
|
||||
/// also stop issuing IME related requests like [`Window::set_ime_position`] and clear pending
|
||||
/// preedit text.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// Describes touch-screen input state.
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
|
|
|
@ -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>) {}
|
||||
|
|
|
@ -291,6 +291,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")
|
||||
}
|
||||
|
|
|
@ -477,6 +477,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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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::{Ime, WindowEvent};
|
||||
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(
|
||||
|
@ -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 {
|
||||
|
@ -58,19 +61,45 @@ 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 cursor_range = preedit
|
||||
.cursor_begin
|
||||
.map(|b| (b, preedit.cursor_end.unwrap_or(b)));
|
||||
|
||||
let event = Ime::Preedit(preedit.text, cursor_range);
|
||||
event_sink.push_window_event(WindowEvent::Ime(event), window_id);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
@ -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`.
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -496,7 +496,12 @@ impl Window {
|
|||
pub fn set_ime_position(&self, position: Position) {
|
||||
let scale_factor = self.scale_factor() as f64;
|
||||
let position = position.to_logical(scale_factor);
|
||||
self.send_request(WindowRequest::IMEPosition(position));
|
||||
self.send_request(WindowRequest::ImePosition(position));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
self.send_request(WindowRequest::AllowIme(allowed));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
@ -12,10 +12,10 @@ use sctk::window::{Decorations, FallbackFrame, Window};
|
|||
|
||||
use crate::dpi::{LogicalPosition, LogicalSize};
|
||||
|
||||
use crate::event::WindowEvent;
|
||||
use crate::event::{Ime, WindowEvent};
|
||||
use crate::platform_impl::wayland;
|
||||
use crate::platform_impl::wayland::env::WinitEnv;
|
||||
use crate::platform_impl::wayland::event_loop::WinitState;
|
||||
use crate::platform_impl::wayland::event_loop::{EventSink, WinitState};
|
||||
use crate::platform_impl::wayland::seat::pointer::WinitPointer;
|
||||
use crate::platform_impl::wayland::seat::text_input::TextInputHandler;
|
||||
use crate::platform_impl::wayland::WindowId;
|
||||
|
@ -69,7 +69,10 @@ pub enum WindowRequest {
|
|||
FrameSize(LogicalSize<u32>),
|
||||
|
||||
/// Set IME window position.
|
||||
IMEPosition(LogicalPosition<u32>),
|
||||
ImePosition(LogicalPosition<u32>),
|
||||
|
||||
/// Enable IME on the given window.
|
||||
AllowIme(bool),
|
||||
|
||||
/// Request Attention.
|
||||
///
|
||||
|
@ -157,6 +160,9 @@ pub struct WindowHandle {
|
|||
/// Whether the window is resizable.
|
||||
pub is_resizable: Cell<bool>,
|
||||
|
||||
/// Allow IME events for that window.
|
||||
pub ime_allowed: Cell<bool>,
|
||||
|
||||
/// Visible cursor or not.
|
||||
cursor_visible: Cell<bool>,
|
||||
|
||||
|
@ -204,6 +210,7 @@ impl WindowHandle {
|
|||
xdg_activation,
|
||||
attention_requested: Cell::new(false),
|
||||
compositor,
|
||||
ime_allowed: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,6 +340,27 @@ impl WindowHandle {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_ime_allowed(&self, allowed: bool, event_sink: &mut EventSink) {
|
||||
if self.ime_allowed.get() == allowed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ime_allowed.replace(allowed);
|
||||
let window_id = wayland::make_wid(self.window.surface());
|
||||
|
||||
for text_input in self.text_inputs.iter() {
|
||||
text_input.set_input_allowed(allowed);
|
||||
}
|
||||
|
||||
let event = if allowed {
|
||||
WindowEvent::Ime(Ime::Enabled)
|
||||
} else {
|
||||
WindowEvent::Ime(Ime::Disabled)
|
||||
};
|
||||
|
||||
event_sink.push_window_event(event, window_id);
|
||||
}
|
||||
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
self.cursor_visible.replace(visible);
|
||||
let cursor_icon = match visible {
|
||||
|
@ -387,9 +415,13 @@ pub fn handle_window_requests(winit_state: &mut WinitState) {
|
|||
WindowRequest::NewCursorIcon(cursor_icon) => {
|
||||
window_handle.set_cursor_icon(cursor_icon);
|
||||
}
|
||||
WindowRequest::IMEPosition(position) => {
|
||||
WindowRequest::ImePosition(position) => {
|
||||
window_handle.set_ime_position(position);
|
||||
}
|
||||
WindowRequest::AllowIme(allow) => {
|
||||
let event_sink = &mut winit_state.event_sink;
|
||||
window_handle.set_ime_allowed(allow, event_sink);
|
||||
}
|
||||
WindowRequest::GrabCursor(grab) => {
|
||||
window_handle.set_cursor_grab(grab);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,12 @@ use super::{
|
|||
|
||||
use util::modifiers::{ModifierKeyState, ModifierKeymap};
|
||||
|
||||
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest};
|
||||
use crate::{
|
||||
dpi::{PhysicalPosition, PhysicalSize},
|
||||
event::{
|
||||
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, TouchPhase, WindowEvent,
|
||||
DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, TouchPhase,
|
||||
WindowEvent,
|
||||
},
|
||||
event_loop::EventLoopWindowTarget as RootELW,
|
||||
};
|
||||
|
@ -26,6 +28,7 @@ const KEYCODE_OFFSET: u8 = 8;
|
|||
pub(super) struct EventProcessor<T: 'static> {
|
||||
pub(super) dnd: Dnd,
|
||||
pub(super) ime_receiver: ImeReceiver,
|
||||
pub(super) ime_event_receiver: ImeEventReceiver,
|
||||
pub(super) randr_event_offset: c_int,
|
||||
pub(super) devices: RefCell<HashMap<DeviceId, Device>>,
|
||||
pub(super) xi2ext: XExtension,
|
||||
|
@ -37,6 +40,7 @@ pub(super) struct EventProcessor<T: 'static> {
|
|||
pub(super) first_touch: Option<u64>,
|
||||
// Currently focused window belonging to this process
|
||||
pub(super) active_window: Option<ffi::Window>,
|
||||
pub(super) is_composing: bool,
|
||||
}
|
||||
|
||||
impl<T: 'static> EventProcessor<T> {
|
||||
|
@ -567,7 +571,7 @@ impl<T: 'static> EventProcessor<T> {
|
|||
|
||||
// When a compose sequence or IME pre-edit is finished, it ends in a KeyPress with
|
||||
// a keycode of 0.
|
||||
if keycode != 0 {
|
||||
if keycode != 0 && !self.is_composing {
|
||||
let scancode = keycode - KEYCODE_OFFSET as u32;
|
||||
let keysym = wt.xconn.lookup_keysym(xkev);
|
||||
let virtual_keycode = events::keysym_to_element(keysym as c_uint);
|
||||
|
@ -602,12 +606,25 @@ impl<T: 'static> EventProcessor<T> {
|
|||
return;
|
||||
};
|
||||
|
||||
for chr in written.chars() {
|
||||
// If we're composing right now, send the string we've got from X11 via
|
||||
// Ime::Commit.
|
||||
if self.is_composing && keycode == 0 && !written.is_empty() {
|
||||
let event = Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::ReceivedCharacter(chr),
|
||||
event: WindowEvent::Ime(Ime::Commit(written)),
|
||||
};
|
||||
|
||||
self.is_composing = false;
|
||||
callback(event);
|
||||
} else {
|
||||
for chr in written.chars() {
|
||||
let event = Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::ReceivedCharacter(chr),
|
||||
};
|
||||
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1223,8 +1240,59 @@ impl<T: 'static> EventProcessor<T> {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok((window_id, x, y)) = self.ime_receiver.try_recv() {
|
||||
wt.ime.borrow_mut().send_xim_spot(window_id, x, y);
|
||||
// Handle IME requests.
|
||||
if let Ok(request) = self.ime_receiver.try_recv() {
|
||||
let mut ime = wt.ime.borrow_mut();
|
||||
match request {
|
||||
ImeRequest::Position(window_id, x, y) => {
|
||||
ime.send_xim_spot(window_id, x, y);
|
||||
}
|
||||
ImeRequest::Allow(window_id, allowed) => {
|
||||
ime.set_ime_allowed(window_id, allowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.ime_event_receiver.try_recv() {
|
||||
Ok((window, event)) => match event {
|
||||
ImeEvent::Enabled => {
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Enabled),
|
||||
});
|
||||
}
|
||||
ImeEvent::Start => {
|
||||
self.is_composing = true;
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Preedit("".to_owned(), None)),
|
||||
});
|
||||
}
|
||||
ImeEvent::Update(text, position) => {
|
||||
if self.is_composing {
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))),
|
||||
});
|
||||
}
|
||||
}
|
||||
ImeEvent::End => {
|
||||
self.is_composing = false;
|
||||
// Issue empty preedit on `Done`.
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Preedit(String::new(), None)),
|
||||
});
|
||||
}
|
||||
ImeEvent::Disabled => {
|
||||
self.is_composing = false;
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Disabled),
|
||||
});
|
||||
}
|
||||
},
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -108,8 +108,19 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
|
|||
let mut new_contexts = HashMap::new();
|
||||
for (window, old_context) in (*inner).contexts.iter() {
|
||||
let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
|
||||
let is_allowed = old_context
|
||||
.as_ref()
|
||||
.map(|old_context| old_context.is_allowed)
|
||||
.unwrap_or_default();
|
||||
let new_context = {
|
||||
let result = ImeContext::new(xconn, new_im.im, *window, spot);
|
||||
let result = ImeContext::new(
|
||||
xconn,
|
||||
new_im.im,
|
||||
*window,
|
||||
spot,
|
||||
is_allowed,
|
||||
(*inner).event_sender.clone(),
|
||||
);
|
||||
if result.is_err() {
|
||||
let _ = close_im(xconn, new_im.im);
|
||||
}
|
||||
|
|
|
@ -1,41 +1,196 @@
|
|||
use std::{
|
||||
os::raw::{c_short, c_void},
|
||||
ptr,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::mem::transmute;
|
||||
use std::os::raw::c_short;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{ffi, util, XConnection, XError};
|
||||
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
|
||||
use std::ffi::CStr;
|
||||
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
|
||||
|
||||
/// IME creation error.
|
||||
#[derive(Debug)]
|
||||
pub enum ImeContextCreationError {
|
||||
/// Got the error from Xlib.
|
||||
XError(XError),
|
||||
|
||||
/// Got null pointer from Xlib but without exact reason.
|
||||
Null,
|
||||
}
|
||||
|
||||
unsafe fn create_pre_edit_attr<'a>(
|
||||
xconn: &'a Arc<XConnection>,
|
||||
ic_spot: &'a ffi::XPoint,
|
||||
) -> util::XSmartPointer<'a, c_void> {
|
||||
util::XSmartPointer::new(
|
||||
xconn,
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNSpotLocation_0.as_ptr() as *const _,
|
||||
ic_spot,
|
||||
ptr::null_mut::<()>(),
|
||||
),
|
||||
)
|
||||
.expect("XVaCreateNestedList returned NULL")
|
||||
/// The callback used by XIM preedit functions.
|
||||
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
|
||||
|
||||
/// Wrapper for creating XIM callbacks.
|
||||
#[inline]
|
||||
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
|
||||
XIMCallback {
|
||||
client_data,
|
||||
callback: Some(callback),
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING: this struct doesn't destroy its XIC resource when dropped.
|
||||
/// The server started preedit.
|
||||
extern "C" fn preedit_start_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
_call_data: ffi::XPointer,
|
||||
) -> i32 {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
|
||||
client_data.text.clear();
|
||||
client_data.cursor_pos = 0;
|
||||
client_data
|
||||
.event_sender
|
||||
.send((client_data.window, ImeEvent::Start))
|
||||
.expect("failed to send preedit start event");
|
||||
-1
|
||||
}
|
||||
|
||||
/// Done callback is used when the preedit should be hidden.
|
||||
extern "C" fn preedit_done_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
_call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
|
||||
// Drop text buffer and reset cursor position on done.
|
||||
client_data.text = Vec::new();
|
||||
client_data.cursor_pos = 0;
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((client_data.window, ImeEvent::End))
|
||||
.expect("failed to send preedit end event");
|
||||
}
|
||||
|
||||
fn calc_byte_position(text: &Vec<char>, pos: usize) -> usize {
|
||||
let mut byte_pos = 0;
|
||||
for i in 0..pos {
|
||||
byte_pos += text[i].len_utf8();
|
||||
}
|
||||
byte_pos
|
||||
}
|
||||
|
||||
/// Preedit text information to be drawn inline by the client.
|
||||
extern "C" fn preedit_draw_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
|
||||
client_data.cursor_pos = call_data.caret as usize;
|
||||
|
||||
let chg_range =
|
||||
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
|
||||
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
|
||||
warn!(
|
||||
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
|
||||
client_data.text.len(),
|
||||
call_data.chg_first,
|
||||
call_data.chg_length
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// NULL indicate text deletion
|
||||
let mut new_chars = if call_data.text.is_null() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let xim_text = unsafe { &mut *(call_data.text) };
|
||||
if xim_text.encoding_is_wchar > 0 {
|
||||
return;
|
||||
}
|
||||
let new_text = unsafe { CStr::from_ptr(xim_text.string.multi_byte) };
|
||||
|
||||
String::from(new_text.to_str().expect("Invalid UTF-8 String from IME"))
|
||||
.chars()
|
||||
.collect()
|
||||
};
|
||||
let mut old_text_tail = client_data.text.split_off(chg_range.end);
|
||||
client_data.text.truncate(chg_range.start);
|
||||
client_data.text.append(&mut new_chars);
|
||||
client_data.text.append(&mut old_text_tail);
|
||||
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((
|
||||
client_data.window,
|
||||
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
|
||||
))
|
||||
.expect("failed to send preedit update event");
|
||||
}
|
||||
|
||||
/// Handling of cursor movements in preedit text.
|
||||
extern "C" fn preedit_caret_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
|
||||
|
||||
if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition {
|
||||
client_data.cursor_pos = call_data.position as usize;
|
||||
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((
|
||||
client_data.window,
|
||||
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
|
||||
))
|
||||
.expect("failed to send preedit update event");
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct to simplify callback creation and latter passing into Xlib XIM.
|
||||
struct PreeditCallbacks {
|
||||
start_callback: ffi::XIMCallback,
|
||||
done_callback: ffi::XIMCallback,
|
||||
draw_callback: ffi::XIMCallback,
|
||||
caret_callback: ffi::XIMCallback,
|
||||
}
|
||||
|
||||
impl PreeditCallbacks {
|
||||
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
|
||||
let start_callback = create_xim_callback(client_data, unsafe {
|
||||
transmute(preedit_start_callback as usize)
|
||||
});
|
||||
let done_callback = create_xim_callback(client_data, preedit_done_callback);
|
||||
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
|
||||
let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
|
||||
|
||||
PreeditCallbacks {
|
||||
start_callback,
|
||||
done_callback,
|
||||
caret_callback,
|
||||
draw_callback,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImeContextClientData {
|
||||
window: ffi::Window,
|
||||
event_sender: ImeEventSender,
|
||||
text: Vec<char>,
|
||||
cursor_pos: usize,
|
||||
}
|
||||
|
||||
// XXX: this struct doesn't destroy its XIC resource when dropped.
|
||||
// This is intentional, as it doesn't have enough information to know whether or not the context
|
||||
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
|
||||
// through `ImeInner`.
|
||||
#[derive(Debug)]
|
||||
pub struct ImeContext {
|
||||
pub ic: ffi::XIC,
|
||||
pub ic_spot: ffi::XPoint,
|
||||
pub(super) ic: ffi::XIC,
|
||||
pub(super) ic_spot: ffi::XPoint,
|
||||
pub(super) is_allowed: bool,
|
||||
// Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from
|
||||
// there we keep the pointer to automatically deallocate it.
|
||||
_client_data: Box<ImeContextClientData>,
|
||||
}
|
||||
|
||||
impl ImeContext {
|
||||
|
@ -44,66 +199,111 @@ impl ImeContext {
|
|||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
ic_spot: Option<ffi::XPoint>,
|
||||
is_allowed: bool,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Result<Self, ImeContextCreationError> {
|
||||
let ic = if let Some(ic_spot) = ic_spot {
|
||||
ImeContext::create_ic_with_spot(xconn, im, window, ic_spot)
|
||||
let client_data = Box::into_raw(Box::new(ImeContextClientData {
|
||||
window,
|
||||
event_sender,
|
||||
text: Vec::new(),
|
||||
cursor_pos: 0,
|
||||
}));
|
||||
|
||||
let ic = if is_allowed {
|
||||
ImeContext::create_ic(xconn, im, window, client_data as ffi::XPointer)
|
||||
.ok_or(ImeContextCreationError::Null)?
|
||||
} else {
|
||||
ImeContext::create_ic(xconn, im, window)
|
||||
ImeContext::create_none_ic(xconn, im, window).ok_or(ImeContextCreationError::Null)?
|
||||
};
|
||||
|
||||
let ic = ic.ok_or(ImeContextCreationError::Null)?;
|
||||
xconn
|
||||
.check_errors()
|
||||
.map_err(ImeContextCreationError::XError)?;
|
||||
|
||||
Ok(ImeContext {
|
||||
let mut context = ImeContext {
|
||||
ic,
|
||||
ic_spot: ic_spot.unwrap_or(ffi::XPoint { x: 0, y: 0 }),
|
||||
})
|
||||
ic_spot: ffi::XPoint { x: 0, y: 0 },
|
||||
is_allowed,
|
||||
_client_data: Box::from_raw(client_data),
|
||||
};
|
||||
|
||||
// Set the spot location, if it's present.
|
||||
if let Some(ic_spot) = ic_spot {
|
||||
context.set_spot(xconn, ic_spot.x, ic_spot.y)
|
||||
}
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
unsafe fn create_none_ic(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
) -> Option<ffi::XIC> {
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNone | ffi::XIMStatusNone,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
|
||||
(!ic.is_null()).then(|| ic)
|
||||
}
|
||||
|
||||
unsafe fn create_ic(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
client_data: ffi::XPointer,
|
||||
) -> Option<ffi::XIC> {
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
if ic.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(ic)
|
||||
}
|
||||
}
|
||||
let preedit_callbacks = PreeditCallbacks::new(client_data);
|
||||
let preedit_attr = util::XSmartPointer::new(
|
||||
xconn,
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNPreeditStartCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.start_callback) as *const _,
|
||||
ffi::XNPreeditDoneCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.done_callback) as *const _,
|
||||
ffi::XNPreeditCaretCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.caret_callback) as *const _,
|
||||
ffi::XNPreeditDrawCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.draw_callback) as *const _,
|
||||
ptr::null_mut::<()>(),
|
||||
),
|
||||
)
|
||||
.expect("XVaCreateNestedList returned NULL");
|
||||
|
||||
unsafe fn create_ic_with_spot(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
ic_spot: ffi::XPoint,
|
||||
) -> Option<ffi::XIC> {
|
||||
let pre_edit_attr = create_pre_edit_attr(xconn, &ic_spot);
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
pre_edit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
if ic.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(ic)
|
||||
}
|
||||
let ic = {
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
preedit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
|
||||
// If we've failed to create IC with preedit callbacks fallback to normal one.
|
||||
if ic.is_null() {
|
||||
(xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
)
|
||||
} else {
|
||||
ic
|
||||
}
|
||||
};
|
||||
|
||||
(!ic.is_null()).then(|| ic)
|
||||
}
|
||||
|
||||
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
|
||||
|
@ -120,18 +320,34 @@ impl ImeContext {
|
|||
xconn.check_errors()
|
||||
}
|
||||
|
||||
// Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks
|
||||
// are being used. Certain IMEs do show selection window, but it's placed in bottom left of the
|
||||
// window and couldn't be changed.
|
||||
//
|
||||
// For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580.
|
||||
pub fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
|
||||
if self.ic_spot.x == x && self.ic_spot.y == y {
|
||||
if !self.is_allowed || self.ic_spot.x == x && self.ic_spot.y == y {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ic_spot = ffi::XPoint { x, y };
|
||||
|
||||
unsafe {
|
||||
let pre_edit_attr = create_pre_edit_attr(xconn, &self.ic_spot);
|
||||
let preedit_attr = util::XSmartPointer::new(
|
||||
xconn,
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNSpotLocation_0.as_ptr(),
|
||||
&self.ic_spot,
|
||||
ptr::null_mut::<()>(),
|
||||
),
|
||||
)
|
||||
.expect("XVaCreateNestedList returned NULL");
|
||||
|
||||
(xconn.xlib.XSetICValues)(
|
||||
self.ic,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
pre_edit_attr.ptr,
|
||||
preedit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::{collections::HashMap, mem, ptr, sync::Arc};
|
|||
use super::{ffi, XConnection, XError};
|
||||
|
||||
use super::{context::ImeContext, input_method::PotentialInputMethods};
|
||||
use crate::platform_impl::platform::x11::ime::ImeEventSender;
|
||||
|
||||
pub unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
|
||||
(xconn.xlib.XCloseIM)(im);
|
||||
|
@ -22,6 +23,7 @@ pub struct ImeInner {
|
|||
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
|
||||
// WARNING: this is initially zeroed!
|
||||
pub destroy_callback: ffi::XIMCallback,
|
||||
pub event_sender: ImeEventSender,
|
||||
// Indicates whether or not the the input method was destroyed on the server end
|
||||
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
|
||||
pub is_destroyed: bool,
|
||||
|
@ -29,13 +31,18 @@ pub struct ImeInner {
|
|||
}
|
||||
|
||||
impl ImeInner {
|
||||
pub fn new(xconn: Arc<XConnection>, potential_input_methods: PotentialInputMethods) -> Self {
|
||||
pub fn new(
|
||||
xconn: Arc<XConnection>,
|
||||
potential_input_methods: PotentialInputMethods,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Self {
|
||||
ImeInner {
|
||||
xconn,
|
||||
im: ptr::null_mut(),
|
||||
potential_input_methods,
|
||||
contexts: HashMap::new(),
|
||||
destroy_callback: unsafe { mem::zeroed() },
|
||||
event_sender,
|
||||
is_destroyed: false,
|
||||
is_fallback: false,
|
||||
}
|
||||
|
|
|
@ -19,9 +19,29 @@ use self::{
|
|||
inner::{close_im, ImeInner},
|
||||
input_method::PotentialInputMethods,
|
||||
};
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum ImeEvent {
|
||||
Enabled,
|
||||
Start,
|
||||
Update(String, usize),
|
||||
End,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub type ImeReceiver = Receiver<(ffi::Window, i16, i16)>;
|
||||
pub type ImeSender = Sender<(ffi::Window, i16, i16)>;
|
||||
pub type ImeReceiver = Receiver<ImeRequest>;
|
||||
pub type ImeSender = Sender<ImeRequest>;
|
||||
pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>;
|
||||
pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>;
|
||||
|
||||
/// Request to control XIM handler from the window.
|
||||
pub enum ImeRequest {
|
||||
/// Set IME spot position for given `window_id`.
|
||||
Position(ffi::Window, i16, i16),
|
||||
|
||||
/// Allow IME input for the given `window_id`.
|
||||
Allow(ffi::Window, bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ImeCreationError {
|
||||
|
@ -37,11 +57,14 @@ pub struct Ime {
|
|||
}
|
||||
|
||||
impl Ime {
|
||||
pub fn new(xconn: Arc<XConnection>) -> Result<Self, ImeCreationError> {
|
||||
pub fn new(
|
||||
xconn: Arc<XConnection>,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Result<Self, ImeCreationError> {
|
||||
let potential_input_methods = PotentialInputMethods::new(&xconn);
|
||||
|
||||
let (mut inner, client_data) = {
|
||||
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods));
|
||||
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender));
|
||||
let inner_ptr = Box::into_raw(inner);
|
||||
let client_data = inner_ptr as _;
|
||||
let destroy_callback = ffi::XIMCallback {
|
||||
|
@ -88,12 +111,37 @@ impl Ime {
|
|||
// Ok(_) indicates that nothing went wrong internally
|
||||
// Ok(true) indicates that the action was actually performed
|
||||
// Ok(false) indicates that the action is not presently applicable
|
||||
pub fn create_context(&mut self, window: ffi::Window) -> Result<bool, ImeContextCreationError> {
|
||||
pub fn create_context(
|
||||
&mut self,
|
||||
window: ffi::Window,
|
||||
with_preedit: bool,
|
||||
) -> Result<bool, ImeContextCreationError> {
|
||||
let context = if self.is_destroyed() {
|
||||
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { ImeContext::new(&self.inner.xconn, self.inner.im, window, None) }?)
|
||||
let event = if with_preedit {
|
||||
ImeEvent::Enabled
|
||||
} else {
|
||||
// There's no IME without preedit.
|
||||
ImeEvent::Disabled
|
||||
};
|
||||
|
||||
self.inner
|
||||
.event_sender
|
||||
.send((window, event))
|
||||
.expect("Failed to send enabled event");
|
||||
|
||||
Some(unsafe {
|
||||
ImeContext::new(
|
||||
&self.inner.xconn,
|
||||
self.inner.im,
|
||||
window,
|
||||
None,
|
||||
with_preedit,
|
||||
self.inner.event_sender.clone(),
|
||||
)
|
||||
}?)
|
||||
};
|
||||
self.inner.contexts.insert(window, context);
|
||||
Ok(!self.is_destroyed())
|
||||
|
@ -151,6 +199,24 @@ impl Ime {
|
|||
context.set_spot(&self.xconn, x as _, y as _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) {
|
||||
if self.is_destroyed() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
|
||||
if allowed == context.is_allowed {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove context for that window.
|
||||
let _ = self.remove_context(window);
|
||||
|
||||
// Create new context supporting IME input.
|
||||
let _ = self.create_context(window, allowed);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ime {
|
||||
|
|
|
@ -44,7 +44,7 @@ use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker};
|
|||
use self::{
|
||||
dnd::{Dnd, DndState},
|
||||
event_processor::EventProcessor,
|
||||
ime::{Ime, ImeCreationError, ImeReceiver, ImeSender},
|
||||
ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender},
|
||||
util::modifiers::ModifierKeymap,
|
||||
};
|
||||
use crate::{
|
||||
|
@ -144,6 +144,7 @@ impl<T: 'static> EventLoop<T> {
|
|||
.expect("Failed to call XInternAtoms when initializing drag and drop");
|
||||
|
||||
let (ime_sender, ime_receiver) = mpsc::channel();
|
||||
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
|
||||
// Input methods will open successfully without setting the locale, but it won't be
|
||||
// possible to actually commit pre-edit sequences.
|
||||
unsafe {
|
||||
|
@ -168,7 +169,7 @@ impl<T: 'static> EventLoop<T> {
|
|||
}
|
||||
}
|
||||
let ime = RefCell::new({
|
||||
let result = Ime::new(Arc::clone(&xconn));
|
||||
let result = Ime::new(Arc::clone(&xconn), ime_event_sender);
|
||||
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
|
||||
panic!("Failed to open input method: {:#?}", state);
|
||||
}
|
||||
|
@ -252,12 +253,14 @@ impl<T: 'static> EventLoop<T> {
|
|||
devices: Default::default(),
|
||||
randr_event_offset,
|
||||
ime_receiver,
|
||||
ime_event_receiver,
|
||||
xi2ext,
|
||||
mod_keymap,
|
||||
device_mod_state: Default::default(),
|
||||
num_touch: 0,
|
||||
first_touch: None,
|
||||
active_window: None,
|
||||
is_composing: false,
|
||||
};
|
||||
|
||||
// Register for device hotplug events
|
||||
|
|
|
@ -26,7 +26,8 @@ use crate::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
ffi, util, EventLoopWindowTarget, ImeSender, WakeSender, WindowId, XConnection, XError,
|
||||
ffi, util, EventLoopWindowTarget, ImeRequest, ImeSender, WakeSender, WindowId, XConnection,
|
||||
XError,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -453,7 +454,10 @@ impl UnownedWindow {
|
|||
.queue();
|
||||
|
||||
{
|
||||
let result = event_loop.ime.borrow_mut().create_context(window.xwindow);
|
||||
let result = event_loop
|
||||
.ime
|
||||
.borrow_mut()
|
||||
.create_context(window.xwindow, false);
|
||||
if let Err(err) = result {
|
||||
let e = match err {
|
||||
ImeContextCreationError::XError(err) => OsError::XError(err),
|
||||
|
@ -1410,17 +1414,21 @@ impl UnownedWindow {
|
|||
.map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))
|
||||
}
|
||||
|
||||
pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) {
|
||||
let _ = self
|
||||
.ime_sender
|
||||
.lock()
|
||||
.send((self.xwindow, x as i16, y as i16));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_position(&self, spot: Position) {
|
||||
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
|
||||
self.set_ime_position_physical(x, y);
|
||||
let _ = self
|
||||
.ime_sender
|
||||
.lock()
|
||||
.send(ImeRequest::Position(self.xwindow, x, y));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
let _ = self
|
||||
.ime_sender
|
||||
.lock()
|
||||
.send(ImeRequest::Allow(self.xwindow, allowed));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
@ -4,6 +4,7 @@ mod cursor;
|
|||
pub use self::{cursor::*, r#async::*};
|
||||
|
||||
use std::ops::{BitAnd, Deref};
|
||||
use std::os::raw::c_uchar;
|
||||
|
||||
use cocoa::{
|
||||
appkit::{NSApp, NSWindowStyleMask},
|
||||
|
@ -11,7 +12,7 @@ use cocoa::{
|
|||
foundation::{NSPoint, NSRect, NSString, NSUInteger},
|
||||
};
|
||||
use core_graphics::display::CGDisplay;
|
||||
use objc::runtime::{Class, Object};
|
||||
use objc::runtime::{Class, Object, BOOL, NO};
|
||||
|
||||
use crate::dpi::LogicalPosition;
|
||||
use crate::platform_impl::platform::ffi;
|
||||
|
@ -165,3 +166,21 @@ pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, o
|
|||
// If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly!
|
||||
window.makeFirstResponder_(view);
|
||||
}
|
||||
|
||||
/// For invalid utf8 sequences potentially returned by `UTF8String`,
|
||||
/// it behaves identically to `String::from_utf8_lossy`
|
||||
///
|
||||
/// Safety: Assumes that `string` is an instance of `NSAttributedString` or `NSString`
|
||||
pub unsafe fn id_to_string_lossy(string: id) -> String {
|
||||
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
|
||||
let characters = if has_attr != NO {
|
||||
// This is a *mut NSAttributedString
|
||||
msg_send![string, string]
|
||||
} else {
|
||||
// This is already a *mut NSString
|
||||
string
|
||||
};
|
||||
let utf8_sequence =
|
||||
std::slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len());
|
||||
String::from_utf8_lossy(utf8_sequence).into_owned()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@ use std::{
|
|||
collections::VecDeque,
|
||||
os::raw::*,
|
||||
ptr, slice, str,
|
||||
sync::{Arc, Mutex, Weak},
|
||||
sync::{
|
||||
atomic::{compiler_fence, Ordering},
|
||||
Arc, Mutex, Weak,
|
||||
},
|
||||
};
|
||||
|
||||
use cocoa::{
|
||||
|
@ -19,7 +22,7 @@ use objc::{
|
|||
use crate::{
|
||||
dpi::LogicalPosition,
|
||||
event::{
|
||||
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseButton,
|
||||
DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, MouseButton,
|
||||
MouseScrollDelta, TouchPhase, VirtualKeyCode, WindowEvent,
|
||||
},
|
||||
platform_impl::platform::{
|
||||
|
@ -29,7 +32,7 @@ use crate::{
|
|||
scancode_to_keycode, EventWrapper,
|
||||
},
|
||||
ffi::*,
|
||||
util::{self, IdRef},
|
||||
util::{self, id_to_string_lossy, IdRef},
|
||||
window::get_window_id,
|
||||
DEVICE_ID,
|
||||
},
|
||||
|
@ -50,20 +53,42 @@ impl Default for CursorState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum ImeState {
|
||||
Disabled,
|
||||
Enabled,
|
||||
Preedit,
|
||||
}
|
||||
|
||||
pub(super) struct ViewState {
|
||||
ns_window: id,
|
||||
pub cursor_state: Arc<Mutex<CursorState>>,
|
||||
/// The position of the candidate window.
|
||||
ime_position: LogicalPosition<f64>,
|
||||
raw_characters: Option<String>,
|
||||
pub(super) modifiers: ModifiersState,
|
||||
tracking_rect: Option<NSInteger>,
|
||||
ime_state: ImeState,
|
||||
input_source: String,
|
||||
|
||||
/// True iff the application wants IME events.
|
||||
///
|
||||
/// Can be set using `set_ime_allowed`
|
||||
ime_allowed: bool,
|
||||
|
||||
/// True if the current key event should be forwarded
|
||||
/// to the application, even during IME
|
||||
forward_key_to_app: bool,
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
fn get_scale_factor(&self) -> f64 {
|
||||
(unsafe { NSWindow::backingScaleFactor(self.ns_window) }) as f64
|
||||
}
|
||||
fn is_ime_enabled(&self) -> bool {
|
||||
match self.ime_state {
|
||||
ImeState::Disabled => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_view(ns_window: id) -> (IdRef, Weak<Mutex<CursorState>>) {
|
||||
|
@ -72,11 +97,13 @@ pub fn new_view(ns_window: id) -> (IdRef, Weak<Mutex<CursorState>>) {
|
|||
let state = ViewState {
|
||||
ns_window,
|
||||
cursor_state,
|
||||
// By default, open the candidate window in the top left corner
|
||||
ime_position: LogicalPosition::new(0.0, 0.0),
|
||||
raw_characters: None,
|
||||
modifiers: Default::default(),
|
||||
tracking_rect: None,
|
||||
ime_state: ImeState::Disabled,
|
||||
input_source: String::new(),
|
||||
ime_allowed: false,
|
||||
forward_key_to_app: false,
|
||||
};
|
||||
unsafe {
|
||||
// This is free'd in `dealloc`
|
||||
|
@ -97,6 +124,33 @@ pub unsafe fn set_ime_position(ns_view: id, position: LogicalPosition<f64>) {
|
|||
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
|
||||
}
|
||||
|
||||
pub unsafe fn set_ime_allowed(ns_view: id, ime_allowed: bool) {
|
||||
let state_ptr: *mut c_void = *(*ns_view).get_mut_ivar("winitState");
|
||||
let state = &mut *(state_ptr as *mut ViewState);
|
||||
if state.ime_allowed == ime_allowed {
|
||||
return;
|
||||
}
|
||||
state.ime_allowed = ime_allowed;
|
||||
if state.ime_allowed {
|
||||
return;
|
||||
}
|
||||
let marked_text_ref: &mut id = (*ns_view).get_mut_ivar("markedText");
|
||||
|
||||
// Clear markedText
|
||||
let _: () = msg_send![*marked_text_ref, release];
|
||||
let marked_text =
|
||||
<id as NSMutableAttributedString>::init(NSMutableAttributedString::alloc(nil));
|
||||
*marked_text_ref = marked_text;
|
||||
|
||||
if state.ime_state != ImeState::Disabled {
|
||||
state.ime_state = ImeState::Disabled;
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::Ime(Ime::Disabled),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewClass(*const Class);
|
||||
unsafe impl Send for ViewClass {}
|
||||
unsafe impl Sync for ViewClass {}
|
||||
|
@ -130,6 +184,9 @@ lazy_static! {
|
|||
sel!(resetCursorRects),
|
||||
reset_cursor_rects as extern "C" fn(&Object, Sel),
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// NSTextInputClient
|
||||
decl.add_method(
|
||||
sel!(hasMarkedText),
|
||||
has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
|
@ -173,6 +230,8 @@ lazy_static! {
|
|||
sel!(doCommandBySelector:),
|
||||
do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
|
||||
);
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
decl.add_method(sel!(keyDown:), key_down as extern "C" fn(&Object, Sel, id));
|
||||
decl.add_method(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, id));
|
||||
decl.add_method(
|
||||
|
@ -266,9 +325,9 @@ lazy_static! {
|
|||
|
||||
extern "C" fn dealloc(this: &Object, _sel: Sel) {
|
||||
unsafe {
|
||||
let state: *mut c_void = *this.get_ivar("winitState");
|
||||
let marked_text: id = *this.get_ivar("markedText");
|
||||
let _: () = msg_send![marked_text, release];
|
||||
let state: *mut c_void = *this.get_ivar("winitState");
|
||||
Box::from_raw(state as *mut ViewState);
|
||||
}
|
||||
}
|
||||
|
@ -285,15 +344,19 @@ extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> i
|
|||
|
||||
let notification_center: &Object =
|
||||
msg_send![class!(NSNotificationCenter), defaultCenter];
|
||||
let notification_name =
|
||||
// About frame change
|
||||
let frame_did_change_notification_name =
|
||||
IdRef::new(NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification"));
|
||||
let _: () = msg_send![
|
||||
notification_center,
|
||||
addObserver: this
|
||||
selector: sel!(frameDidChange:)
|
||||
name: notification_name
|
||||
name: frame_did_change_notification_name
|
||||
object: this
|
||||
];
|
||||
|
||||
let winit_state = &mut *(state as *mut ViewState);
|
||||
winit_state.input_source = current_input_source(this);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
@ -402,7 +465,7 @@ extern "C" fn marked_range(this: &Object, _sel: Sel) -> NSRange {
|
|||
let marked_text: id = *this.get_ivar("markedText");
|
||||
let length = marked_text.length();
|
||||
if length > 0 {
|
||||
NSRange::new(0, length - 1)
|
||||
NSRange::new(0, length)
|
||||
} else {
|
||||
util::EMPTY_RANGE
|
||||
}
|
||||
|
@ -414,6 +477,13 @@ extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange {
|
|||
util::EMPTY_RANGE
|
||||
}
|
||||
|
||||
/// Safety: Assumes that `view` is an instance of `VIEW_CLASS` from winit.
|
||||
unsafe fn current_input_source(view: *const Object) -> String {
|
||||
let input_context: id = msg_send![view, inputContext];
|
||||
let input_source: id = msg_send![input_context, selectedKeyboardInputSource];
|
||||
id_to_string_lossy(input_source)
|
||||
}
|
||||
|
||||
extern "C" fn set_marked_text(
|
||||
this: &mut Object,
|
||||
_sel: Sel,
|
||||
|
@ -423,7 +493,10 @@ extern "C" fn set_marked_text(
|
|||
) {
|
||||
trace_scope!("setMarkedText:selectedRange:replacementRange:");
|
||||
unsafe {
|
||||
// Get pre-edit text
|
||||
let marked_text_ref: &mut id = this.get_mut_ivar("markedText");
|
||||
|
||||
// Update markedText
|
||||
let _: () = msg_send![(*marked_text_ref), release];
|
||||
let marked_text = NSMutableAttributedString::alloc(nil);
|
||||
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
|
||||
|
@ -433,6 +506,33 @@ extern "C" fn set_marked_text(
|
|||
marked_text.initWithString(string);
|
||||
};
|
||||
*marked_text_ref = marked_text;
|
||||
|
||||
// Update ViewState with new marked text
|
||||
let state_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||
let state = &mut *(state_ptr as *mut ViewState);
|
||||
let preedit_string = id_to_string_lossy(string);
|
||||
|
||||
// Notify IME is active if application still doesn't know it.
|
||||
if state.ime_state == ImeState::Disabled {
|
||||
state.input_source = current_input_source(this);
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::Ime(Ime::Enabled),
|
||||
}));
|
||||
}
|
||||
|
||||
let cursor_start = preedit_string.len();
|
||||
let cursor_end = preedit_string.len();
|
||||
state.ime_state = ImeState::Preedit;
|
||||
|
||||
// Send WindowEvent for updating marked text
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::Ime(Ime::Preedit(
|
||||
preedit_string,
|
||||
Some((cursor_start, cursor_end)),
|
||||
)),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,6 +546,19 @@ extern "C" fn unmark_text(this: &Object, _sel: Sel) {
|
|||
let _: () = msg_send![s, release];
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
let _: () = msg_send![input_context, discardMarkedText];
|
||||
|
||||
let state_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||
let state = &mut *(state_ptr as *mut ViewState);
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::Ime(Ime::Preedit(String::new(), Some((0, 0)))),
|
||||
}));
|
||||
if state.is_ime_enabled() {
|
||||
// Leave the Preedit state
|
||||
state.ime_state = ImeState::Enabled;
|
||||
} else {
|
||||
warn!("Expected to have IME enabled when receiving unmarkText");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -499,35 +612,24 @@ extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_ran
|
|||
let state_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||
let state = &mut *(state_ptr as *mut ViewState);
|
||||
|
||||
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
|
||||
let characters = if has_attr != NO {
|
||||
// This is a *mut NSAttributedString
|
||||
msg_send![string, string]
|
||||
} else {
|
||||
// This is already a *mut NSString
|
||||
string
|
||||
};
|
||||
let string = id_to_string_lossy(string);
|
||||
|
||||
let slice =
|
||||
slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len());
|
||||
let string = str::from_utf8_unchecked(slice);
|
||||
let is_control = string.chars().next().map_or(false, |c| c.is_control());
|
||||
|
||||
// We don't need this now, but it's here if that changes.
|
||||
//let event: id = msg_send![NSApp(), currentEvent];
|
||||
|
||||
let mut events = VecDeque::with_capacity(characters.len());
|
||||
for character in string.chars().filter(|c| !is_corporate_character(*c)) {
|
||||
events.push_back(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
if state.is_ime_enabled() && !is_control {
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::ReceivedCharacter(character),
|
||||
event: WindowEvent::Ime(Ime::Commit(string)),
|
||||
}));
|
||||
state.ime_state = ImeState::Enabled;
|
||||
}
|
||||
|
||||
AppState::queue_events(events);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) {
|
||||
extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, _command: Sel) {
|
||||
trace_scope!("doCommandBySelector:");
|
||||
// Basically, we're sent this message whenever a keyboard event that doesn't generate a "human readable" character
|
||||
// happens, i.e. newlines, tabs, and Ctrl+C.
|
||||
|
@ -535,31 +637,15 @@ extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) {
|
|||
let state_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||
let state = &mut *(state_ptr as *mut ViewState);
|
||||
|
||||
let mut events = VecDeque::with_capacity(1);
|
||||
if command == sel!(insertNewline:) {
|
||||
// The `else` condition would emit the same character, but I'm keeping this here both...
|
||||
// 1) as a reminder for how `doCommandBySelector` works
|
||||
// 2) to make our use of carriage return explicit
|
||||
events.push_back(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::ReceivedCharacter('\r'),
|
||||
}));
|
||||
} else {
|
||||
let raw_characters = state.raw_characters.take();
|
||||
if let Some(raw_characters) = raw_characters {
|
||||
for character in raw_characters
|
||||
.chars()
|
||||
.filter(|c| !is_corporate_character(*c))
|
||||
{
|
||||
events.push_back(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::ReceivedCharacter(character),
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
state.forward_key_to_app = true;
|
||||
|
||||
AppState::queue_events(events);
|
||||
let has_marked_text: BOOL = msg_send![this, hasMarkedText];
|
||||
if has_marked_text == NO {
|
||||
if state.ime_state == ImeState::Preedit {
|
||||
// Leave preedit so that we also report the keyup for this key
|
||||
state.ime_state = ImeState::Enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -637,54 +723,71 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) {
|
|||
let state_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||
let state = &mut *(state_ptr as *mut ViewState);
|
||||
let window_id = WindowId(get_window_id(state.ns_window));
|
||||
let characters = get_characters(event, false);
|
||||
|
||||
state.raw_characters = Some(characters.clone());
|
||||
let input_source = current_input_source(this);
|
||||
if state.input_source != input_source && state.is_ime_enabled() {
|
||||
state.ime_state = ImeState::Disabled;
|
||||
state.input_source = input_source;
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::Ime(Ime::Disabled),
|
||||
}));
|
||||
}
|
||||
let was_in_preedit = state.ime_state == ImeState::Preedit;
|
||||
|
||||
let characters = get_characters(event, false);
|
||||
state.forward_key_to_app = false;
|
||||
|
||||
// The `interpretKeyEvents` function might call
|
||||
// `setMarkedText`, `insertText`, and `doCommandBySelector`.
|
||||
// It's important that we call this before queuing the KeyboardInput, because
|
||||
// we must send the `KeyboardInput` event during IME if it triggered
|
||||
// `doCommandBySelector`. (doCommandBySelector means that the keyboard input
|
||||
// is not handled by IME and should be handled by the application)
|
||||
if state.ime_allowed {
|
||||
let events_for_nsview: id = msg_send![class!(NSArray), arrayWithObject: event];
|
||||
let _: () = msg_send![this, interpretKeyEvents: events_for_nsview];
|
||||
|
||||
// Using a compiler fence because `interpretKeyEvents` might call
|
||||
// into functions that modify the `ViewState`, but the compiler
|
||||
// doesn't know this. Without the fence, the compiler may think that
|
||||
// some of the reads (eg `state.ime_state`) that happen after this
|
||||
// point are not needed.
|
||||
compiler_fence(Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let now_in_preedit = state.ime_state == ImeState::Preedit;
|
||||
|
||||
let scancode = get_scancode(event) as u32;
|
||||
let virtual_keycode = retrieve_keycode(event);
|
||||
|
||||
let is_repeat: BOOL = msg_send![event, isARepeat];
|
||||
|
||||
update_potentially_stale_modifiers(state, event);
|
||||
|
||||
#[allow(deprecated)]
|
||||
let window_event = Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::KeyboardInput {
|
||||
device_id: DEVICE_ID,
|
||||
input: KeyboardInput {
|
||||
state: ElementState::Pressed,
|
||||
scancode,
|
||||
virtual_keycode,
|
||||
modifiers: event_mods(event),
|
||||
let preedit_related = was_in_preedit || now_in_preedit;
|
||||
if !preedit_related || state.forward_key_to_app || !state.ime_allowed {
|
||||
#[allow(deprecated)]
|
||||
let window_event = Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::KeyboardInput {
|
||||
device_id: DEVICE_ID,
|
||||
input: KeyboardInput {
|
||||
state: ElementState::Pressed,
|
||||
scancode,
|
||||
virtual_keycode,
|
||||
modifiers: event_mods(event),
|
||||
},
|
||||
is_synthetic: false,
|
||||
},
|
||||
is_synthetic: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
let pass_along = {
|
||||
AppState::queue_event(EventWrapper::StaticEvent(window_event));
|
||||
// Emit `ReceivedCharacter` for key repeats
|
||||
if is_repeat != NO {
|
||||
for character in characters.chars().filter(|c| !is_corporate_character(*c)) {
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::ReceivedCharacter(character),
|
||||
}));
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if pass_along {
|
||||
// Some keys (and only *some*, with no known reason) don't trigger `insertText`, while others do...
|
||||
// So, we don't give repeats the opportunity to trigger that, since otherwise our hack will cause some
|
||||
// keys to generate twice as many characters.
|
||||
let array: id = msg_send![class!(NSArray), arrayWithObject: event];
|
||||
let _: () = msg_send![this, interpretKeyEvents: array];
|
||||
for character in characters.chars().filter(|c| !is_corporate_character(*c)) {
|
||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::ReceivedCharacter(character),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -700,22 +803,25 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) {
|
|||
|
||||
update_potentially_stale_modifiers(state, event);
|
||||
|
||||
#[allow(deprecated)]
|
||||
let window_event = Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::KeyboardInput {
|
||||
device_id: DEVICE_ID,
|
||||
input: KeyboardInput {
|
||||
state: ElementState::Released,
|
||||
scancode,
|
||||
virtual_keycode,
|
||||
modifiers: event_mods(event),
|
||||
// We want to send keyboard input when we are not currently in preedit
|
||||
if state.ime_state != ImeState::Preedit {
|
||||
#[allow(deprecated)]
|
||||
let window_event = Event::WindowEvent {
|
||||
window_id: WindowId(get_window_id(state.ns_window)),
|
||||
event: WindowEvent::KeyboardInput {
|
||||
device_id: DEVICE_ID,
|
||||
input: KeyboardInput {
|
||||
state: ElementState::Released,
|
||||
scancode,
|
||||
virtual_keycode,
|
||||
modifiers: event_mods(event),
|
||||
},
|
||||
is_synthetic: false,
|
||||
},
|
||||
is_synthetic: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
AppState::queue_event(EventWrapper::StaticEvent(window_event));
|
||||
AppState::queue_event(EventWrapper::StaticEvent(window_event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -461,7 +461,7 @@ impl UnownedWindow {
|
|||
if maximized {
|
||||
window.set_maximized(maximized);
|
||||
}
|
||||
|
||||
trace!("Done unowned window::new");
|
||||
Ok((window, delegate))
|
||||
}
|
||||
|
||||
|
@ -1054,6 +1054,13 @@ impl UnownedWindow {
|
|||
unsafe { view::set_ime_position(*self.ns_view, logical_spot) };
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
unsafe {
|
||||
view::set_ime_allowed(*self.ns_view, allowed);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn focus_window(&self) {
|
||||
let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] };
|
||||
|
|
|
@ -302,6 +302,11 @@ impl Window {
|
|||
// Currently a no-op as it does not seem there is good support for this on web
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, _allowed: bool) {
|
||||
// Currently not implemented
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn focus_window(&self) {
|
||||
// Currently a no-op as it does not seem there is good support for this on web
|
||||
|
|
|
@ -34,6 +34,7 @@ use windows_sys::Win32::{
|
|||
UI::{
|
||||
Controls::{HOVER_DEFAULT, WM_MOUSELEAVE},
|
||||
Input::{
|
||||
Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW},
|
||||
KeyboardAndMouse::{
|
||||
MapVirtualKeyA, ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE,
|
||||
TRACKMOUSEEVENT, VK_F4,
|
||||
|
@ -59,21 +60,23 @@ use windows_sys::Win32::{
|
|||
SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
|
||||
SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, WM_CREATE,
|
||||
WM_DESTROY, WM_DPICHANGED, WM_DROPFILES, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE,
|
||||
WM_GETMINMAXINFO, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS,
|
||||
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL,
|
||||
WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT,
|
||||
WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP,
|
||||
WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SYSCHAR, WM_SYSCOMMAND,
|
||||
WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING,
|
||||
WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE,
|
||||
WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
|
||||
WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT,
|
||||
WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP,
|
||||
WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP,
|
||||
WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY,
|
||||
WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE,
|
||||
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE,
|
||||
WM_SYSCHAR, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED,
|
||||
WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED,
|
||||
WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP,
|
||||
WS_VISIBLE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dpi::{PhysicalPosition, PhysicalSize},
|
||||
event::{DeviceEvent, Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent},
|
||||
event::{DeviceEvent, Event, Force, Ime, KeyboardInput, Touch, TouchPhase, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW},
|
||||
monitor::MonitorHandle as RootMonitorHandle,
|
||||
platform_impl::platform::{
|
||||
|
@ -81,10 +84,11 @@ use crate::{
|
|||
dpi::{become_dpi_aware, dpi_to_scale_factor},
|
||||
drop_handler::FileDropHandler,
|
||||
event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey},
|
||||
ime::ImeContext,
|
||||
monitor::{self, MonitorHandle},
|
||||
raw_input, util,
|
||||
window::InitData,
|
||||
window_state::{CursorFlags, WindowFlags, WindowState},
|
||||
window_state::{CursorFlags, ImeState, WindowFlags, WindowState},
|
||||
wrap_device_id, WindowId, DEVICE_ID,
|
||||
},
|
||||
window::{Fullscreen, WindowId as RootWindowId},
|
||||
|
@ -1128,6 +1132,104 @@ unsafe fn public_window_callback_inner<T: 'static>(
|
|||
0
|
||||
}
|
||||
|
||||
WM_IME_STARTCOMPOSITION => {
|
||||
let ime_allowed = userdata.window_state.lock().ime_allowed;
|
||||
if ime_allowed {
|
||||
userdata.window_state.lock().ime_state = ImeState::Enabled;
|
||||
|
||||
userdata.send_event(Event::WindowEvent {
|
||||
window_id: RootWindowId(WindowId(window)),
|
||||
event: WindowEvent::Ime(Ime::Enabled),
|
||||
});
|
||||
}
|
||||
|
||||
DefWindowProcW(window, msg, wparam, lparam)
|
||||
}
|
||||
|
||||
WM_IME_COMPOSITION => {
|
||||
let ime_allowed_and_composing = {
|
||||
let w = userdata.window_state.lock();
|
||||
w.ime_allowed && w.ime_state != ImeState::Disabled
|
||||
};
|
||||
// Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so
|
||||
// check whether composing.
|
||||
if ime_allowed_and_composing {
|
||||
let ime_context = ImeContext::current(window);
|
||||
|
||||
if lparam == 0 {
|
||||
userdata.send_event(Event::WindowEvent {
|
||||
window_id: RootWindowId(WindowId(window)),
|
||||
event: WindowEvent::Ime(Ime::Preedit(String::new(), None)),
|
||||
});
|
||||
}
|
||||
|
||||
// Google Japanese Input and ATOK have both flags, so
|
||||
// first, receive composing result if exist.
|
||||
if (lparam as u32 & GCS_RESULTSTR) != 0 {
|
||||
if let Some(text) = ime_context.get_composed_text() {
|
||||
userdata.window_state.lock().ime_state = ImeState::Enabled;
|
||||
|
||||
userdata.send_event(Event::WindowEvent {
|
||||
window_id: RootWindowId(WindowId(window)),
|
||||
event: WindowEvent::Ime(Ime::Commit(text)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Next, receive preedit range for next composing if exist.
|
||||
if (lparam as u32 & GCS_COMPSTR) != 0 {
|
||||
if let Some((text, first, last)) = ime_context.get_composing_text_and_cursor() {
|
||||
userdata.window_state.lock().ime_state = ImeState::Preedit;
|
||||
let cursor_range = first.map(|f| (f, last.unwrap_or(f)));
|
||||
|
||||
userdata.send_event(Event::WindowEvent {
|
||||
window_id: RootWindowId(WindowId(window)),
|
||||
event: WindowEvent::Ime(Ime::Preedit(text, cursor_range)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not calling DefWindowProc to hide composing text drawn by IME.
|
||||
0
|
||||
}
|
||||
|
||||
WM_IME_ENDCOMPOSITION => {
|
||||
let ime_allowed_or_composing = {
|
||||
let w = userdata.window_state.lock();
|
||||
w.ime_allowed || w.ime_state != ImeState::Disabled
|
||||
};
|
||||
if ime_allowed_or_composing {
|
||||
if userdata.window_state.lock().ime_state == ImeState::Preedit {
|
||||
// Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so
|
||||
// trying receiving composing result and commit if exists.
|
||||
let ime_context = ImeContext::current(window);
|
||||
if let Some(text) = ime_context.get_composed_text() {
|
||||
userdata.send_event(Event::WindowEvent {
|
||||
window_id: RootWindowId(WindowId(window)),
|
||||
event: WindowEvent::Ime(Ime::Commit(text)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
userdata.window_state.lock().ime_state = ImeState::Disabled;
|
||||
|
||||
userdata.send_event(Event::WindowEvent {
|
||||
window_id: RootWindowId(WindowId(window)),
|
||||
event: WindowEvent::Ime(Ime::Disabled),
|
||||
});
|
||||
}
|
||||
|
||||
DefWindowProcW(window, msg, wparam, lparam)
|
||||
}
|
||||
|
||||
WM_IME_SETCONTEXT => {
|
||||
// Hide composing text drawn by IME.
|
||||
let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize);
|
||||
|
||||
DefWindowProcW(window, msg, wparam, lparam)
|
||||
}
|
||||
|
||||
// this is necessary for us to maintain minimize/restore state
|
||||
WM_SYSCOMMAND => {
|
||||
if wparam == SC_RESTORE as usize {
|
||||
|
|
150
src/platform_impl/windows/ime.rs
Normal file
150
src/platform_impl/windows/ime.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
use std::{
|
||||
ffi::{c_void, OsString},
|
||||
mem::zeroed,
|
||||
os::windows::prelude::OsStringExt,
|
||||
ptr::null_mut,
|
||||
};
|
||||
|
||||
use windows_sys::Win32::{
|
||||
Foundation::POINT,
|
||||
Globalization::HIMC,
|
||||
UI::{
|
||||
Input::Ime::{
|
||||
ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext,
|
||||
ImmSetCandidateWindow, ATTR_TARGET_CONVERTED, ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM,
|
||||
CFS_EXCLUDE, GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN,
|
||||
IACE_DEFAULT,
|
||||
},
|
||||
WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{dpi::Position, platform::windows::HWND};
|
||||
|
||||
pub struct ImeContext {
|
||||
hwnd: HWND,
|
||||
himc: HIMC,
|
||||
}
|
||||
|
||||
impl ImeContext {
|
||||
pub unsafe fn current(hwnd: HWND) -> Self {
|
||||
let himc = ImmGetContext(hwnd);
|
||||
ImeContext { hwnd, himc }
|
||||
}
|
||||
|
||||
pub unsafe fn get_composing_text_and_cursor(
|
||||
&self,
|
||||
) -> Option<(String, Option<usize>, Option<usize>)> {
|
||||
let text = self.get_composition_string(GCS_COMPSTR)?;
|
||||
let attrs = self.get_composition_data(GCS_COMPATTR).unwrap_or_default();
|
||||
|
||||
let mut first = None;
|
||||
let mut last = None;
|
||||
let mut boundary_before_char = 0;
|
||||
|
||||
for (attr, chr) in attrs.into_iter().zip(text.chars()) {
|
||||
let char_is_targetted =
|
||||
attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED;
|
||||
|
||||
if first.is_none() && char_is_targetted {
|
||||
first = Some(boundary_before_char);
|
||||
} else if first.is_some() && last.is_none() && !char_is_targetted {
|
||||
last = Some(boundary_before_char);
|
||||
}
|
||||
|
||||
boundary_before_char += chr.len_utf8();
|
||||
}
|
||||
|
||||
if first.is_some() && last.is_none() {
|
||||
last = Some(text.len());
|
||||
} else if first.is_none() {
|
||||
// IME haven't split words and select any clause yet, so trying to retrieve normal cursor.
|
||||
let cursor = self.get_composition_cursor(&text);
|
||||
first = cursor;
|
||||
last = cursor;
|
||||
}
|
||||
|
||||
Some((text, first, last))
|
||||
}
|
||||
|
||||
pub unsafe fn get_composed_text(&self) -> Option<String> {
|
||||
self.get_composition_string(GCS_RESULTSTR)
|
||||
}
|
||||
|
||||
unsafe fn get_composition_cursor(&self, text: &str) -> Option<usize> {
|
||||
let cursor = ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0);
|
||||
(cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum())
|
||||
}
|
||||
|
||||
unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option<String> {
|
||||
let data = self.get_composition_data(gcs_mode)?;
|
||||
let (prefix, shorts, suffix) = data.align_to::<u16>();
|
||||
if prefix.is_empty() && suffix.is_empty() {
|
||||
OsString::from_wide(&shorts).into_string().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option<Vec<u8>> {
|
||||
let size = ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0);
|
||||
if size < 0 {
|
||||
return None;
|
||||
} else if size == 0 {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
|
||||
let mut buf = Vec::<u8>::with_capacity(size as _);
|
||||
let size = ImmGetCompositionStringW(
|
||||
self.himc,
|
||||
gcs_mode,
|
||||
buf.as_mut_ptr() as *mut c_void,
|
||||
size as _,
|
||||
);
|
||||
|
||||
if size < 0 {
|
||||
None
|
||||
} else {
|
||||
buf.set_len(size as _);
|
||||
Some(buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn set_ime_position(&self, spot: Position, scale_factor: f64) {
|
||||
if !ImeContext::system_has_ime() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (x, y) = spot.to_physical::<i32>(scale_factor).into();
|
||||
let candidate_form = CANDIDATEFORM {
|
||||
dwIndex: 0,
|
||||
dwStyle: CFS_EXCLUDE,
|
||||
ptCurrentPos: POINT { x, y },
|
||||
rcArea: zeroed(),
|
||||
};
|
||||
|
||||
ImmSetCandidateWindow(self.himc, &candidate_form);
|
||||
}
|
||||
|
||||
pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) {
|
||||
if !ImeContext::system_has_ime() {
|
||||
return;
|
||||
}
|
||||
|
||||
if allowed {
|
||||
ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT);
|
||||
} else {
|
||||
ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn system_has_ime() -> bool {
|
||||
return GetSystemMetrics(SM_IMMENABLED) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ImeContext {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ImmReleaseContext(self.hwnd, self.himc) };
|
||||
}
|
||||
}
|
|
@ -154,6 +154,7 @@ mod drop_handler;
|
|||
mod event;
|
||||
mod event_loop;
|
||||
mod icon;
|
||||
mod ime;
|
||||
mod monitor;
|
||||
mod raw_input;
|
||||
mod window;
|
||||
|
|
|
@ -31,10 +31,6 @@ use windows_sys::Win32::{
|
|||
},
|
||||
UI::{
|
||||
Input::{
|
||||
Ime::{
|
||||
ImmGetContext, ImmReleaseContext, ImmSetCompositionWindow, CFS_POINT,
|
||||
COMPOSITIONFORM,
|
||||
},
|
||||
KeyboardAndMouse::{
|
||||
EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, INPUT,
|
||||
INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
|
||||
|
@ -49,8 +45,8 @@ use windows_sys::Win32::{
|
|||
SetWindowPlacement, SetWindowPos, SetWindowTextW, CS_HREDRAW, CS_VREDRAW,
|
||||
CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY,
|
||||
GWLP_HINSTANCE, HTCAPTION, MAPVK_VK_TO_VSC, NID_READY, PM_NOREMOVE, SM_DIGITIZER,
|
||||
SM_IMMENABLED, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER,
|
||||
WM_NCLBUTTONDOWN, WNDCLASSEXW,
|
||||
SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, WM_NCLBUTTONDOWN,
|
||||
WNDCLASSEXW,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -69,6 +65,7 @@ use crate::{
|
|||
drop_handler::FileDropHandler,
|
||||
event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID},
|
||||
icon::{self, IconType},
|
||||
ime::ImeContext,
|
||||
monitor, util,
|
||||
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
|
||||
Parent, PlatformSpecificWindowBuilderAttributes, WindowId,
|
||||
|
@ -626,25 +623,19 @@ impl Window {
|
|||
self.window_state.lock().taskbar_icon = taskbar_icon;
|
||||
}
|
||||
|
||||
pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) {
|
||||
if unsafe { GetSystemMetrics(SM_IMMENABLED) } != 0 {
|
||||
let composition_form = COMPOSITIONFORM {
|
||||
dwStyle: CFS_POINT,
|
||||
ptCurrentPos: POINT { x, y },
|
||||
rcArea: unsafe { mem::zeroed() },
|
||||
};
|
||||
unsafe {
|
||||
let himc = ImmGetContext(self.hwnd());
|
||||
ImmSetCompositionWindow(himc, &composition_form);
|
||||
ImmReleaseContext(self.hwnd(), himc);
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_ime_position(&self, spot: Position) {
|
||||
unsafe {
|
||||
ImeContext::current(self.hwnd()).set_ime_position(spot, self.scale_factor());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_position(&self, spot: Position) {
|
||||
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
|
||||
self.set_ime_position_physical(x, y);
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
self.window_state.lock().ime_allowed = allowed;
|
||||
unsafe {
|
||||
ImeContext::set_ime_allowed(self.hwnd(), allowed);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -798,6 +789,8 @@ impl<'a, T: 'static> InitData<'a, T> {
|
|||
|
||||
enable_non_client_dpi_scaling(window);
|
||||
|
||||
ImeContext::set_ime_allowed(window, false);
|
||||
|
||||
Window {
|
||||
window: WindowWrapper(window),
|
||||
window_state,
|
||||
|
|
|
@ -42,6 +42,9 @@ pub struct WindowState {
|
|||
pub preferred_theme: Option<Theme>,
|
||||
pub high_surrogate: Option<u16>,
|
||||
pub window_flags: WindowFlags,
|
||||
|
||||
pub ime_state: ImeState,
|
||||
pub ime_allowed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -101,6 +104,13 @@ bitflags! {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub enum ImeState {
|
||||
Disabled,
|
||||
Enabled,
|
||||
Preedit,
|
||||
}
|
||||
|
||||
impl WindowState {
|
||||
pub fn new(
|
||||
attributes: &WindowAttributes,
|
||||
|
@ -132,6 +142,9 @@ impl WindowState {
|
|||
preferred_theme,
|
||||
high_surrogate: None,
|
||||
window_flags: WindowFlags::empty(),
|
||||
|
||||
ime_state: ImeState::Disabled,
|
||||
ime_allowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -814,6 +814,13 @@ impl Window {
|
|||
|
||||
/// Sets location of IME candidate box in client area coordinates relative to the top left.
|
||||
///
|
||||
/// This is the window / popup / overlay that allows you to select the desired characters.
|
||||
/// The look of this box may differ between input devices, even on the same platform.
|
||||
///
|
||||
/// (Apple's official term is "candidate window", see their [chinese] and [japanese] guides).
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use winit::dpi::{LogicalPosition, PhysicalPosition};
|
||||
/// # use winit::event_loop::EventLoop;
|
||||
|
@ -830,11 +837,41 @@ impl Window {
|
|||
/// ## Platform-specific
|
||||
///
|
||||
/// - **iOS / Android / Web:** Unsupported.
|
||||
///
|
||||
/// [chinese]: https://support.apple.com/guide/chinese-input-method/use-the-candidate-window-cim12992/104/mac/12.0
|
||||
/// [japanese]: https://support.apple.com/guide/japanese-input-method/use-the-candidate-window-jpim10262/6.3/mac/12.0
|
||||
#[inline]
|
||||
pub fn set_ime_position<P: Into<Position>>(&self, position: P) {
|
||||
self.window.set_ime_position(position.into())
|
||||
}
|
||||
|
||||
/// Sets whether the window should get IME events
|
||||
///
|
||||
/// When IME is allowed, the window will receive [`Ime`] events, and during the
|
||||
/// preedit phase the window will NOT get [`KeyboardInput`] or
|
||||
/// [`ReceivedCharacter`] events. The window should allow IME while it is
|
||||
/// expecting text input.
|
||||
///
|
||||
/// When IME is not allowed, the window won't receive [`Ime`] events, and will
|
||||
/// receive [`KeyboardInput`] events for every keypress instead. Without
|
||||
/// allowing IME, the window will also get [`ReceivedCharacter`] events for
|
||||
/// certain keyboard input. Not allowing IME is useful for games for example.
|
||||
///
|
||||
/// IME is **not** allowed by default.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are combined.
|
||||
/// - ** iOS / Android / Web :** Unsupported.
|
||||
///
|
||||
/// [`Ime`]: crate::event::WindowEvent::Ime
|
||||
/// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput
|
||||
/// [`ReceivedCharacter`]: crate::event::WindowEvent::ReceivedCharacter
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
self.window.set_ime_allowed(allowed);
|
||||
}
|
||||
|
||||
/// Brings the window to the front and sets input focus. Has no effect if the window is
|
||||
/// already in focus, minimized, or not visible.
|
||||
///
|
||||
|
|
Loading…
Reference in a new issue