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`.
|
- **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 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
|
- 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)
|
# 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]
|
[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-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true }
|
||||||
wayland-protocols = { version = "0.29", features = [ "staging_protocols"], 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 }
|
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 }
|
mio = { version = "0.8", features = ["os-ext"], optional = true }
|
||||||
x11-dl = { version = "2.18.5", 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 instant::Instant;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[cfg(doc)]
|
||||||
|
use crate::window::Window;
|
||||||
use crate::{
|
use crate::{
|
||||||
dpi::{PhysicalPosition, PhysicalSize},
|
dpi::{PhysicalPosition, PhysicalSize},
|
||||||
platform_impl,
|
platform_impl,
|
||||||
|
@ -93,8 +95,7 @@ pub enum Event<'a, T: 'static> {
|
||||||
/// This gets triggered in two scenarios:
|
/// This gets triggered in two scenarios:
|
||||||
/// - The OS has performed an operation that's invalidated the window's contents (such as
|
/// - The OS has performed an operation that's invalidated the window's contents (such as
|
||||||
/// resizing the window).
|
/// resizing the window).
|
||||||
/// - The application has explicitly requested a redraw via
|
/// - The application has explicitly requested a redraw via [`Window::request_redraw`].
|
||||||
/// [`Window::request_redraw`](crate::window::Window::request_redraw).
|
|
||||||
///
|
///
|
||||||
/// During each iteration of the event loop, Winit will aggregate duplicate redraw requests
|
/// During each iteration of the event loop, Winit will aggregate duplicate redraw requests
|
||||||
/// into a single event, to help avoid duplicating rendering work.
|
/// into a single event, to help avoid duplicating rendering work.
|
||||||
|
@ -206,7 +207,7 @@ pub enum StartCause {
|
||||||
Init,
|
Init,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describes an event from a `Window`.
|
/// Describes an event from a [`Window`].
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum WindowEvent<'a> {
|
pub enum WindowEvent<'a> {
|
||||||
/// The size of the window has changed. Contains the client area's new dimensions.
|
/// The size of the window has changed. Contains the client area's new dimensions.
|
||||||
|
@ -240,6 +241,8 @@ pub enum WindowEvent<'a> {
|
||||||
HoveredFileCancelled,
|
HoveredFileCancelled,
|
||||||
|
|
||||||
/// The window received a unicode character.
|
/// The window received a unicode character.
|
||||||
|
///
|
||||||
|
/// See also the [`Ime`](Self::Ime) event for more complex character sequences.
|
||||||
ReceivedCharacter(char),
|
ReceivedCharacter(char),
|
||||||
|
|
||||||
/// The window gained or lost focus.
|
/// 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.
|
/// issue, and it should get fixed - but it's the current state of the API.
|
||||||
ModifiersChanged(ModifiersState),
|
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.
|
/// The cursor has moved on the window.
|
||||||
CursorMoved {
|
CursorMoved {
|
||||||
device_id: DeviceId,
|
device_id: DeviceId,
|
||||||
|
@ -376,7 +387,7 @@ impl Clone for WindowEvent<'static> {
|
||||||
input: *input,
|
input: *input,
|
||||||
is_synthetic: *is_synthetic,
|
is_synthetic: *is_synthetic,
|
||||||
},
|
},
|
||||||
|
Ime(preedit_state) => Ime(preedit_state.clone()),
|
||||||
ModifiersChanged(modifiers) => ModifiersChanged(*modifiers),
|
ModifiersChanged(modifiers) => ModifiersChanged(*modifiers),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
CursorMoved {
|
CursorMoved {
|
||||||
|
@ -468,6 +479,7 @@ impl<'a> WindowEvent<'a> {
|
||||||
is_synthetic,
|
is_synthetic,
|
||||||
}),
|
}),
|
||||||
ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)),
|
ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)),
|
||||||
|
Ime(event) => Some(Ime(event)),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
CursorMoved {
|
CursorMoved {
|
||||||
device_id,
|
device_id,
|
||||||
|
@ -627,6 +639,73 @@ pub struct KeyboardInput {
|
||||||
pub modifiers: ModifiersState,
|
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.
|
/// Describes touch-screen input state.
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[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_position(&self, _position: Position) {}
|
||||||
|
|
||||||
|
pub fn set_ime_allowed(&self, _allowed: bool) {}
|
||||||
|
|
||||||
pub fn focus_window(&self) {}
|
pub fn focus_window(&self) {}
|
||||||
|
|
||||||
pub fn request_user_attention(&self, _request_type: Option<window::UserAttentionType>) {}
|
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")
|
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) {
|
pub fn focus_window(&self) {
|
||||||
warn!("`Window::set_focus` is ignored on iOS")
|
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))
|
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]
|
#[inline]
|
||||||
pub fn focus_window(&self) {
|
pub fn focus_window(&self) {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -32,10 +32,9 @@ mod sink;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
pub use proxy::EventLoopProxy;
|
pub use proxy::EventLoopProxy;
|
||||||
|
pub use sink::EventSink;
|
||||||
pub use state::WinitState;
|
pub use state::WinitState;
|
||||||
|
|
||||||
use sink::EventSink;
|
|
||||||
|
|
||||||
type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>;
|
type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>;
|
||||||
|
|
||||||
pub struct EventLoopWindowTarget<T> {
|
pub struct EventLoopWindowTarget<T> {
|
||||||
|
|
|
@ -5,11 +5,11 @@ use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input
|
||||||
Event as TextInputEvent, ZwpTextInputV3,
|
Event as TextInputEvent, ZwpTextInputV3,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::event::WindowEvent;
|
use crate::event::{Ime, WindowEvent};
|
||||||
use crate::platform_impl::wayland;
|
use crate::platform_impl::wayland;
|
||||||
use crate::platform_impl::wayland::event_loop::WinitState;
|
use crate::platform_impl::wayland::event_loop::WinitState;
|
||||||
|
|
||||||
use super::{TextInputHandler, TextInputInner};
|
use super::{Preedit, TextInputHandler, TextInputInner};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn handle_text_input(
|
pub(super) fn handle_text_input(
|
||||||
|
@ -30,8 +30,11 @@ pub(super) fn handle_text_input(
|
||||||
inner.target_window_id = Some(window_id);
|
inner.target_window_id = Some(window_id);
|
||||||
|
|
||||||
// Enable text input on that surface.
|
// Enable text input on that surface.
|
||||||
|
if window_handle.ime_allowed.get() {
|
||||||
text_input.enable();
|
text_input.enable();
|
||||||
text_input.commit();
|
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.
|
// Notify a window we're currently over about text input handler.
|
||||||
let text_input_handler = TextInputHandler {
|
let text_input_handler = TextInputHandler {
|
||||||
|
@ -58,19 +61,45 @@ pub(super) fn handle_text_input(
|
||||||
text_input: text_input.detach(),
|
text_input: text_input.detach(),
|
||||||
};
|
};
|
||||||
window_handle.text_input_left(text_input_handler);
|
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 } => {
|
TextInputEvent::CommitString { text } => {
|
||||||
// Update currenly commited string.
|
// Update currenly commited string and reset previous preedit.
|
||||||
inner.commit_string = text;
|
inner.pending_preedit = None;
|
||||||
|
inner.pending_commit = Some(text.unwrap_or_default());
|
||||||
}
|
}
|
||||||
TextInputEvent::Done { .. } => {
|
TextInputEvent::Done { .. } => {
|
||||||
let (window_id, text) = match (inner.target_window_id, inner.commit_string.take()) {
|
let window_id = match inner.target_window_id {
|
||||||
(Some(window_id), Some(text)) => (window_id, text),
|
Some(window_id) => window_id,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
for ch in text.chars() {
|
if let Some(text) = inner.pending_commit.take() {
|
||||||
event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id);
|
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.set_cursor_rectangle(x, y, 0, 0);
|
||||||
self.text_input.commit();
|
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`.
|
/// A wrapper around text input to automatically destroy the object on `Drop`.
|
||||||
|
@ -52,15 +63,25 @@ struct TextInputInner {
|
||||||
/// Currently focused surface.
|
/// Currently focused surface.
|
||||||
target_window_id: Option<WindowId>,
|
target_window_id: Option<WindowId>,
|
||||||
|
|
||||||
/// Pending string to commit.
|
/// Pending commit event which will be dispatched on `text_input_v3::Done`.
|
||||||
commit_string: Option<String>,
|
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 {
|
impl TextInputInner {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
target_window_id: None,
|
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) {
|
pub fn set_ime_position(&self, position: Position) {
|
||||||
let scale_factor = self.scale_factor() as f64;
|
let scale_factor = self.scale_factor() as f64;
|
||||||
let position = position.to_logical(scale_factor);
|
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]
|
#[inline]
|
||||||
|
|
|
@ -12,10 +12,10 @@ use sctk::window::{Decorations, FallbackFrame, Window};
|
||||||
|
|
||||||
use crate::dpi::{LogicalPosition, LogicalSize};
|
use crate::dpi::{LogicalPosition, LogicalSize};
|
||||||
|
|
||||||
use crate::event::WindowEvent;
|
use crate::event::{Ime, WindowEvent};
|
||||||
use crate::platform_impl::wayland;
|
use crate::platform_impl::wayland;
|
||||||
use crate::platform_impl::wayland::env::WinitEnv;
|
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::pointer::WinitPointer;
|
||||||
use crate::platform_impl::wayland::seat::text_input::TextInputHandler;
|
use crate::platform_impl::wayland::seat::text_input::TextInputHandler;
|
||||||
use crate::platform_impl::wayland::WindowId;
|
use crate::platform_impl::wayland::WindowId;
|
||||||
|
@ -69,7 +69,10 @@ pub enum WindowRequest {
|
||||||
FrameSize(LogicalSize<u32>),
|
FrameSize(LogicalSize<u32>),
|
||||||
|
|
||||||
/// Set IME window position.
|
/// Set IME window position.
|
||||||
IMEPosition(LogicalPosition<u32>),
|
ImePosition(LogicalPosition<u32>),
|
||||||
|
|
||||||
|
/// Enable IME on the given window.
|
||||||
|
AllowIme(bool),
|
||||||
|
|
||||||
/// Request Attention.
|
/// Request Attention.
|
||||||
///
|
///
|
||||||
|
@ -157,6 +160,9 @@ pub struct WindowHandle {
|
||||||
/// Whether the window is resizable.
|
/// Whether the window is resizable.
|
||||||
pub is_resizable: Cell<bool>,
|
pub is_resizable: Cell<bool>,
|
||||||
|
|
||||||
|
/// Allow IME events for that window.
|
||||||
|
pub ime_allowed: Cell<bool>,
|
||||||
|
|
||||||
/// Visible cursor or not.
|
/// Visible cursor or not.
|
||||||
cursor_visible: Cell<bool>,
|
cursor_visible: Cell<bool>,
|
||||||
|
|
||||||
|
@ -204,6 +210,7 @@ impl WindowHandle {
|
||||||
xdg_activation,
|
xdg_activation,
|
||||||
attention_requested: Cell::new(false),
|
attention_requested: Cell::new(false),
|
||||||
compositor,
|
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) {
|
pub fn set_cursor_visible(&self, visible: bool) {
|
||||||
self.cursor_visible.replace(visible);
|
self.cursor_visible.replace(visible);
|
||||||
let cursor_icon = match visible {
|
let cursor_icon = match visible {
|
||||||
|
@ -387,9 +415,13 @@ pub fn handle_window_requests(winit_state: &mut WinitState) {
|
||||||
WindowRequest::NewCursorIcon(cursor_icon) => {
|
WindowRequest::NewCursorIcon(cursor_icon) => {
|
||||||
window_handle.set_cursor_icon(cursor_icon);
|
window_handle.set_cursor_icon(cursor_icon);
|
||||||
}
|
}
|
||||||
WindowRequest::IMEPosition(position) => {
|
WindowRequest::ImePosition(position) => {
|
||||||
window_handle.set_ime_position(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) => {
|
WindowRequest::GrabCursor(grab) => {
|
||||||
window_handle.set_cursor_grab(grab);
|
window_handle.set_cursor_grab(grab);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,12 @@ use super::{
|
||||||
|
|
||||||
use util::modifiers::{ModifierKeyState, ModifierKeymap};
|
use util::modifiers::{ModifierKeyState, ModifierKeymap};
|
||||||
|
|
||||||
|
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest};
|
||||||
use crate::{
|
use crate::{
|
||||||
dpi::{PhysicalPosition, PhysicalSize},
|
dpi::{PhysicalPosition, PhysicalSize},
|
||||||
event::{
|
event::{
|
||||||
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, TouchPhase, WindowEvent,
|
DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, TouchPhase,
|
||||||
|
WindowEvent,
|
||||||
},
|
},
|
||||||
event_loop::EventLoopWindowTarget as RootELW,
|
event_loop::EventLoopWindowTarget as RootELW,
|
||||||
};
|
};
|
||||||
|
@ -26,6 +28,7 @@ const KEYCODE_OFFSET: u8 = 8;
|
||||||
pub(super) struct EventProcessor<T: 'static> {
|
pub(super) struct EventProcessor<T: 'static> {
|
||||||
pub(super) dnd: Dnd,
|
pub(super) dnd: Dnd,
|
||||||
pub(super) ime_receiver: ImeReceiver,
|
pub(super) ime_receiver: ImeReceiver,
|
||||||
|
pub(super) ime_event_receiver: ImeEventReceiver,
|
||||||
pub(super) randr_event_offset: c_int,
|
pub(super) randr_event_offset: c_int,
|
||||||
pub(super) devices: RefCell<HashMap<DeviceId, Device>>,
|
pub(super) devices: RefCell<HashMap<DeviceId, Device>>,
|
||||||
pub(super) xi2ext: XExtension,
|
pub(super) xi2ext: XExtension,
|
||||||
|
@ -37,6 +40,7 @@ pub(super) struct EventProcessor<T: 'static> {
|
||||||
pub(super) first_touch: Option<u64>,
|
pub(super) first_touch: Option<u64>,
|
||||||
// Currently focused window belonging to this process
|
// Currently focused window belonging to this process
|
||||||
pub(super) active_window: Option<ffi::Window>,
|
pub(super) active_window: Option<ffi::Window>,
|
||||||
|
pub(super) is_composing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static> EventProcessor<T> {
|
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
|
// When a compose sequence or IME pre-edit is finished, it ends in a KeyPress with
|
||||||
// a keycode of 0.
|
// a keycode of 0.
|
||||||
if keycode != 0 {
|
if keycode != 0 && !self.is_composing {
|
||||||
let scancode = keycode - KEYCODE_OFFSET as u32;
|
let scancode = keycode - KEYCODE_OFFSET as u32;
|
||||||
let keysym = wt.xconn.lookup_keysym(xkev);
|
let keysym = wt.xconn.lookup_keysym(xkev);
|
||||||
let virtual_keycode = events::keysym_to_element(keysym as c_uint);
|
let virtual_keycode = events::keysym_to_element(keysym as c_uint);
|
||||||
|
@ -602,15 +606,28 @@ impl<T: 'static> EventProcessor<T> {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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::Ime(Ime::Commit(written)),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.is_composing = false;
|
||||||
|
callback(event);
|
||||||
|
} else {
|
||||||
for chr in written.chars() {
|
for chr in written.chars() {
|
||||||
let event = Event::WindowEvent {
|
let event = Event::WindowEvent {
|
||||||
window_id,
|
window_id,
|
||||||
event: WindowEvent::ReceivedCharacter(chr),
|
event: WindowEvent::ReceivedCharacter(chr),
|
||||||
};
|
};
|
||||||
|
|
||||||
callback(event);
|
callback(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ffi::GenericEvent => {
|
ffi::GenericEvent => {
|
||||||
let guard = if let Some(e) = GenericEventCookie::from_event(&wt.xconn, *xev) {
|
let guard = if let Some(e) = GenericEventCookie::from_event(&wt.xconn, *xev) {
|
||||||
|
@ -1223,8 +1240,59 @@ impl<T: 'static> EventProcessor<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok((window_id, x, y)) = self.ime_receiver.try_recv() {
|
// Handle IME requests.
|
||||||
wt.ime.borrow_mut().send_xim_spot(window_id, x, y);
|
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();
|
let mut new_contexts = HashMap::new();
|
||||||
for (window, old_context) in (*inner).contexts.iter() {
|
for (window, old_context) in (*inner).contexts.iter() {
|
||||||
let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
|
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 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() {
|
if result.is_err() {
|
||||||
let _ = close_im(xconn, new_im.im);
|
let _ = close_im(xconn, new_im.im);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,196 @@
|
||||||
use std::{
|
use std::mem::transmute;
|
||||||
os::raw::{c_short, c_void},
|
use std::os::raw::c_short;
|
||||||
ptr,
|
use std::ptr;
|
||||||
sync::Arc,
|
use std::sync::Arc;
|
||||||
};
|
|
||||||
|
|
||||||
use super::{ffi, util, XConnection, XError};
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum ImeContextCreationError {
|
pub enum ImeContextCreationError {
|
||||||
|
/// Got the error from Xlib.
|
||||||
XError(XError),
|
XError(XError),
|
||||||
|
|
||||||
|
/// Got null pointer from Xlib but without exact reason.
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn create_pre_edit_attr<'a>(
|
/// The callback used by XIM preedit functions.
|
||||||
xconn: &'a Arc<XConnection>,
|
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
|
||||||
ic_spot: &'a ffi::XPoint,
|
|
||||||
) -> util::XSmartPointer<'a, c_void> {
|
/// Wrapper for creating XIM callbacks.
|
||||||
util::XSmartPointer::new(
|
#[inline]
|
||||||
xconn,
|
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
|
||||||
(xconn.xlib.XVaCreateNestedList)(
|
XIMCallback {
|
||||||
0,
|
client_data,
|
||||||
ffi::XNSpotLocation_0.as_ptr() as *const _,
|
callback: Some(callback),
|
||||||
ic_spot,
|
}
|
||||||
ptr::null_mut::<()>(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.expect("XVaCreateNestedList returned NULL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// 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
|
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
|
||||||
// through `ImeInner`.
|
// through `ImeInner`.
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ImeContext {
|
pub struct ImeContext {
|
||||||
pub ic: ffi::XIC,
|
pub(super) ic: ffi::XIC,
|
||||||
pub ic_spot: ffi::XPoint,
|
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 {
|
impl ImeContext {
|
||||||
|
@ -44,66 +199,111 @@ impl ImeContext {
|
||||||
im: ffi::XIM,
|
im: ffi::XIM,
|
||||||
window: ffi::Window,
|
window: ffi::Window,
|
||||||
ic_spot: Option<ffi::XPoint>,
|
ic_spot: Option<ffi::XPoint>,
|
||||||
|
is_allowed: bool,
|
||||||
|
event_sender: ImeEventSender,
|
||||||
) -> Result<Self, ImeContextCreationError> {
|
) -> Result<Self, ImeContextCreationError> {
|
||||||
let ic = if let Some(ic_spot) = ic_spot {
|
let client_data = Box::into_raw(Box::new(ImeContextClientData {
|
||||||
ImeContext::create_ic_with_spot(xconn, im, window, ic_spot)
|
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 {
|
} 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
|
xconn
|
||||||
.check_errors()
|
.check_errors()
|
||||||
.map_err(ImeContextCreationError::XError)?;
|
.map_err(ImeContextCreationError::XError)?;
|
||||||
|
|
||||||
Ok(ImeContext {
|
let mut context = ImeContext {
|
||||||
ic,
|
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(
|
unsafe fn create_ic(
|
||||||
xconn: &Arc<XConnection>,
|
xconn: &Arc<XConnection>,
|
||||||
im: ffi::XIM,
|
im: ffi::XIM,
|
||||||
window: ffi::Window,
|
window: ffi::Window,
|
||||||
|
client_data: ffi::XPointer,
|
||||||
) -> Option<ffi::XIC> {
|
) -> Option<ffi::XIC> {
|
||||||
let ic = (xconn.xlib.XCreateIC)(
|
let preedit_callbacks = PreeditCallbacks::new(client_data);
|
||||||
im,
|
let preedit_attr = util::XSmartPointer::new(
|
||||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
xconn,
|
||||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
(xconn.xlib.XVaCreateNestedList)(
|
||||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
0,
|
||||||
window,
|
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::<()>(),
|
ptr::null_mut::<()>(),
|
||||||
);
|
),
|
||||||
if ic.is_null() {
|
)
|
||||||
None
|
.expect("XVaCreateNestedList returned NULL");
|
||||||
} else {
|
|
||||||
Some(ic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn create_ic_with_spot(
|
let ic = {
|
||||||
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)(
|
let ic = (xconn.xlib.XCreateIC)(
|
||||||
im,
|
im,
|
||||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing,
|
||||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||||
window,
|
window,
|
||||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||||
pre_edit_attr.ptr,
|
preedit_attr.ptr,
|
||||||
ptr::null_mut::<()>(),
|
ptr::null_mut::<()>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If we've failed to create IC with preedit callbacks fallback to normal one.
|
||||||
if ic.is_null() {
|
if ic.is_null() {
|
||||||
None
|
(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 {
|
} else {
|
||||||
Some(ic)
|
ic
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(!ic.is_null()).then(|| ic)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
|
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
|
||||||
|
@ -120,18 +320,34 @@ impl ImeContext {
|
||||||
xconn.check_errors()
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ic_spot = ffi::XPoint { x, y };
|
self.ic_spot = ffi::XPoint { x, y };
|
||||||
|
|
||||||
unsafe {
|
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)(
|
(xconn.xlib.XSetICValues)(
|
||||||
self.ic,
|
self.ic,
|
||||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||||
pre_edit_attr.ptr,
|
preedit_attr.ptr,
|
||||||
ptr::null_mut::<()>(),
|
ptr::null_mut::<()>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::{collections::HashMap, mem, ptr, sync::Arc};
|
||||||
use super::{ffi, XConnection, XError};
|
use super::{ffi, XConnection, XError};
|
||||||
|
|
||||||
use super::{context::ImeContext, input_method::PotentialInputMethods};
|
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> {
|
pub unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
|
||||||
(xconn.xlib.XCloseIM)(im);
|
(xconn.xlib.XCloseIM)(im);
|
||||||
|
@ -22,6 +23,7 @@ pub struct ImeInner {
|
||||||
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
|
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
|
||||||
// WARNING: this is initially zeroed!
|
// WARNING: this is initially zeroed!
|
||||||
pub destroy_callback: ffi::XIMCallback,
|
pub destroy_callback: ffi::XIMCallback,
|
||||||
|
pub event_sender: ImeEventSender,
|
||||||
// Indicates whether or not the the input method was destroyed on the server end
|
// Indicates whether or not the the input method was destroyed on the server end
|
||||||
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
|
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
|
||||||
pub is_destroyed: bool,
|
pub is_destroyed: bool,
|
||||||
|
@ -29,13 +31,18 @@ pub struct ImeInner {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
ImeInner {
|
||||||
xconn,
|
xconn,
|
||||||
im: ptr::null_mut(),
|
im: ptr::null_mut(),
|
||||||
potential_input_methods,
|
potential_input_methods,
|
||||||
contexts: HashMap::new(),
|
contexts: HashMap::new(),
|
||||||
destroy_callback: unsafe { mem::zeroed() },
|
destroy_callback: unsafe { mem::zeroed() },
|
||||||
|
event_sender,
|
||||||
is_destroyed: false,
|
is_destroyed: false,
|
||||||
is_fallback: false,
|
is_fallback: false,
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,29 @@ use self::{
|
||||||
inner::{close_im, ImeInner},
|
inner::{close_im, ImeInner},
|
||||||
input_method::PotentialInputMethods,
|
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 ImeReceiver = Receiver<ImeRequest>;
|
||||||
pub type ImeSender = Sender<(ffi::Window, i16, i16)>;
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum ImeCreationError {
|
pub enum ImeCreationError {
|
||||||
|
@ -37,11 +57,14 @@ pub struct Ime {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 potential_input_methods = PotentialInputMethods::new(&xconn);
|
||||||
|
|
||||||
let (mut inner, client_data) = {
|
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 inner_ptr = Box::into_raw(inner);
|
||||||
let client_data = inner_ptr as _;
|
let client_data = inner_ptr as _;
|
||||||
let destroy_callback = ffi::XIMCallback {
|
let destroy_callback = ffi::XIMCallback {
|
||||||
|
@ -88,12 +111,37 @@ impl Ime {
|
||||||
// Ok(_) indicates that nothing went wrong internally
|
// Ok(_) indicates that nothing went wrong internally
|
||||||
// Ok(true) indicates that the action was actually performed
|
// Ok(true) indicates that the action was actually performed
|
||||||
// Ok(false) indicates that the action is not presently applicable
|
// 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() {
|
let context = if self.is_destroyed() {
|
||||||
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
|
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
|
||||||
None
|
None
|
||||||
} else {
|
} 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);
|
self.inner.contexts.insert(window, context);
|
||||||
Ok(!self.is_destroyed())
|
Ok(!self.is_destroyed())
|
||||||
|
@ -151,6 +199,24 @@ impl Ime {
|
||||||
context.set_spot(&self.xconn, x as _, y as _);
|
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 {
|
impl Drop for Ime {
|
||||||
|
|
|
@ -44,7 +44,7 @@ use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker};
|
||||||
use self::{
|
use self::{
|
||||||
dnd::{Dnd, DndState},
|
dnd::{Dnd, DndState},
|
||||||
event_processor::EventProcessor,
|
event_processor::EventProcessor,
|
||||||
ime::{Ime, ImeCreationError, ImeReceiver, ImeSender},
|
ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender},
|
||||||
util::modifiers::ModifierKeymap,
|
util::modifiers::ModifierKeymap,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -144,6 +144,7 @@ impl<T: 'static> EventLoop<T> {
|
||||||
.expect("Failed to call XInternAtoms when initializing drag and drop");
|
.expect("Failed to call XInternAtoms when initializing drag and drop");
|
||||||
|
|
||||||
let (ime_sender, ime_receiver) = mpsc::channel();
|
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
|
// Input methods will open successfully without setting the locale, but it won't be
|
||||||
// possible to actually commit pre-edit sequences.
|
// possible to actually commit pre-edit sequences.
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -168,7 +169,7 @@ impl<T: 'static> EventLoop<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let ime = RefCell::new({
|
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 {
|
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
|
||||||
panic!("Failed to open input method: {:#?}", state);
|
panic!("Failed to open input method: {:#?}", state);
|
||||||
}
|
}
|
||||||
|
@ -252,12 +253,14 @@ impl<T: 'static> EventLoop<T> {
|
||||||
devices: Default::default(),
|
devices: Default::default(),
|
||||||
randr_event_offset,
|
randr_event_offset,
|
||||||
ime_receiver,
|
ime_receiver,
|
||||||
|
ime_event_receiver,
|
||||||
xi2ext,
|
xi2ext,
|
||||||
mod_keymap,
|
mod_keymap,
|
||||||
device_mod_state: Default::default(),
|
device_mod_state: Default::default(),
|
||||||
num_touch: 0,
|
num_touch: 0,
|
||||||
first_touch: None,
|
first_touch: None,
|
||||||
active_window: None,
|
active_window: None,
|
||||||
|
is_composing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register for device hotplug events
|
// Register for device hotplug events
|
||||||
|
|
|
@ -26,7 +26,8 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ffi, util, EventLoopWindowTarget, ImeSender, WakeSender, WindowId, XConnection, XError,
|
ffi, util, EventLoopWindowTarget, ImeRequest, ImeSender, WakeSender, WindowId, XConnection,
|
||||||
|
XError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -453,7 +454,10 @@ impl UnownedWindow {
|
||||||
.queue();
|
.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 {
|
if let Err(err) = result {
|
||||||
let e = match err {
|
let e = match err {
|
||||||
ImeContextCreationError::XError(err) => OsError::XError(err),
|
ImeContextCreationError::XError(err) => OsError::XError(err),
|
||||||
|
@ -1410,17 +1414,21 @@ impl UnownedWindow {
|
||||||
.map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))
|
.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]
|
#[inline]
|
||||||
pub fn set_ime_position(&self, spot: Position) {
|
pub fn set_ime_position(&self, spot: Position) {
|
||||||
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
|
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]
|
#[inline]
|
||||||
|
|
|
@ -4,6 +4,7 @@ mod cursor;
|
||||||
pub use self::{cursor::*, r#async::*};
|
pub use self::{cursor::*, r#async::*};
|
||||||
|
|
||||||
use std::ops::{BitAnd, Deref};
|
use std::ops::{BitAnd, Deref};
|
||||||
|
use std::os::raw::c_uchar;
|
||||||
|
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{NSApp, NSWindowStyleMask},
|
appkit::{NSApp, NSWindowStyleMask},
|
||||||
|
@ -11,7 +12,7 @@ use cocoa::{
|
||||||
foundation::{NSPoint, NSRect, NSString, NSUInteger},
|
foundation::{NSPoint, NSRect, NSString, NSUInteger},
|
||||||
};
|
};
|
||||||
use core_graphics::display::CGDisplay;
|
use core_graphics::display::CGDisplay;
|
||||||
use objc::runtime::{Class, Object};
|
use objc::runtime::{Class, Object, BOOL, NO};
|
||||||
|
|
||||||
use crate::dpi::LogicalPosition;
|
use crate::dpi::LogicalPosition;
|
||||||
use crate::platform_impl::platform::ffi;
|
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!
|
// If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly!
|
||||||
window.makeFirstResponder_(view);
|
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,
|
collections::VecDeque,
|
||||||
os::raw::*,
|
os::raw::*,
|
||||||
ptr, slice, str,
|
ptr, slice, str,
|
||||||
sync::{Arc, Mutex, Weak},
|
sync::{
|
||||||
|
atomic::{compiler_fence, Ordering},
|
||||||
|
Arc, Mutex, Weak,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
|
@ -19,7 +22,7 @@ use objc::{
|
||||||
use crate::{
|
use crate::{
|
||||||
dpi::LogicalPosition,
|
dpi::LogicalPosition,
|
||||||
event::{
|
event::{
|
||||||
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseButton,
|
DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, MouseButton,
|
||||||
MouseScrollDelta, TouchPhase, VirtualKeyCode, WindowEvent,
|
MouseScrollDelta, TouchPhase, VirtualKeyCode, WindowEvent,
|
||||||
},
|
},
|
||||||
platform_impl::platform::{
|
platform_impl::platform::{
|
||||||
|
@ -29,7 +32,7 @@ use crate::{
|
||||||
scancode_to_keycode, EventWrapper,
|
scancode_to_keycode, EventWrapper,
|
||||||
},
|
},
|
||||||
ffi::*,
|
ffi::*,
|
||||||
util::{self, IdRef},
|
util::{self, id_to_string_lossy, IdRef},
|
||||||
window::get_window_id,
|
window::get_window_id,
|
||||||
DEVICE_ID,
|
DEVICE_ID,
|
||||||
},
|
},
|
||||||
|
@ -50,20 +53,42 @@ impl Default for CursorState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq)]
|
||||||
|
enum ImeState {
|
||||||
|
Disabled,
|
||||||
|
Enabled,
|
||||||
|
Preedit,
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) struct ViewState {
|
pub(super) struct ViewState {
|
||||||
ns_window: id,
|
ns_window: id,
|
||||||
pub cursor_state: Arc<Mutex<CursorState>>,
|
pub cursor_state: Arc<Mutex<CursorState>>,
|
||||||
/// The position of the candidate window.
|
|
||||||
ime_position: LogicalPosition<f64>,
|
ime_position: LogicalPosition<f64>,
|
||||||
raw_characters: Option<String>,
|
|
||||||
pub(super) modifiers: ModifiersState,
|
pub(super) modifiers: ModifiersState,
|
||||||
tracking_rect: Option<NSInteger>,
|
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 {
|
impl ViewState {
|
||||||
fn get_scale_factor(&self) -> f64 {
|
fn get_scale_factor(&self) -> f64 {
|
||||||
(unsafe { NSWindow::backingScaleFactor(self.ns_window) }) as 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>>) {
|
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 {
|
let state = ViewState {
|
||||||
ns_window,
|
ns_window,
|
||||||
cursor_state,
|
cursor_state,
|
||||||
// By default, open the candidate window in the top left corner
|
|
||||||
ime_position: LogicalPosition::new(0.0, 0.0),
|
ime_position: LogicalPosition::new(0.0, 0.0),
|
||||||
raw_characters: None,
|
|
||||||
modifiers: Default::default(),
|
modifiers: Default::default(),
|
||||||
tracking_rect: None,
|
tracking_rect: None,
|
||||||
|
ime_state: ImeState::Disabled,
|
||||||
|
input_source: String::new(),
|
||||||
|
ime_allowed: false,
|
||||||
|
forward_key_to_app: false,
|
||||||
};
|
};
|
||||||
unsafe {
|
unsafe {
|
||||||
// This is free'd in `dealloc`
|
// 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];
|
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);
|
struct ViewClass(*const Class);
|
||||||
unsafe impl Send for ViewClass {}
|
unsafe impl Send for ViewClass {}
|
||||||
unsafe impl Sync for ViewClass {}
|
unsafe impl Sync for ViewClass {}
|
||||||
|
@ -130,6 +184,9 @@ lazy_static! {
|
||||||
sel!(resetCursorRects),
|
sel!(resetCursorRects),
|
||||||
reset_cursor_rects as extern "C" fn(&Object, Sel),
|
reset_cursor_rects as extern "C" fn(&Object, Sel),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// NSTextInputClient
|
||||||
decl.add_method(
|
decl.add_method(
|
||||||
sel!(hasMarkedText),
|
sel!(hasMarkedText),
|
||||||
has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
|
has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
|
||||||
|
@ -173,6 +230,8 @@ lazy_static! {
|
||||||
sel!(doCommandBySelector:),
|
sel!(doCommandBySelector:),
|
||||||
do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
|
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!(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(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, id));
|
||||||
decl.add_method(
|
decl.add_method(
|
||||||
|
@ -266,9 +325,9 @@ lazy_static! {
|
||||||
|
|
||||||
extern "C" fn dealloc(this: &Object, _sel: Sel) {
|
extern "C" fn dealloc(this: &Object, _sel: Sel) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let state: *mut c_void = *this.get_ivar("winitState");
|
|
||||||
let marked_text: id = *this.get_ivar("markedText");
|
let marked_text: id = *this.get_ivar("markedText");
|
||||||
let _: () = msg_send![marked_text, release];
|
let _: () = msg_send![marked_text, release];
|
||||||
|
let state: *mut c_void = *this.get_ivar("winitState");
|
||||||
Box::from_raw(state as *mut ViewState);
|
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 =
|
let notification_center: &Object =
|
||||||
msg_send![class!(NSNotificationCenter), defaultCenter];
|
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"));
|
IdRef::new(NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification"));
|
||||||
let _: () = msg_send![
|
let _: () = msg_send![
|
||||||
notification_center,
|
notification_center,
|
||||||
addObserver: this
|
addObserver: this
|
||||||
selector: sel!(frameDidChange:)
|
selector: sel!(frameDidChange:)
|
||||||
name: notification_name
|
name: frame_did_change_notification_name
|
||||||
object: this
|
object: this
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let winit_state = &mut *(state as *mut ViewState);
|
||||||
|
winit_state.input_source = current_input_source(this);
|
||||||
}
|
}
|
||||||
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 marked_text: id = *this.get_ivar("markedText");
|
||||||
let length = marked_text.length();
|
let length = marked_text.length();
|
||||||
if length > 0 {
|
if length > 0 {
|
||||||
NSRange::new(0, length - 1)
|
NSRange::new(0, length)
|
||||||
} else {
|
} else {
|
||||||
util::EMPTY_RANGE
|
util::EMPTY_RANGE
|
||||||
}
|
}
|
||||||
|
@ -414,6 +477,13 @@ extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange {
|
||||||
util::EMPTY_RANGE
|
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(
|
extern "C" fn set_marked_text(
|
||||||
this: &mut Object,
|
this: &mut Object,
|
||||||
_sel: Sel,
|
_sel: Sel,
|
||||||
|
@ -423,7 +493,10 @@ extern "C" fn set_marked_text(
|
||||||
) {
|
) {
|
||||||
trace_scope!("setMarkedText:selectedRange:replacementRange:");
|
trace_scope!("setMarkedText:selectedRange:replacementRange:");
|
||||||
unsafe {
|
unsafe {
|
||||||
|
// Get pre-edit text
|
||||||
let marked_text_ref: &mut id = this.get_mut_ivar("markedText");
|
let marked_text_ref: &mut id = this.get_mut_ivar("markedText");
|
||||||
|
|
||||||
|
// Update markedText
|
||||||
let _: () = msg_send![(*marked_text_ref), release];
|
let _: () = msg_send![(*marked_text_ref), release];
|
||||||
let marked_text = NSMutableAttributedString::alloc(nil);
|
let marked_text = NSMutableAttributedString::alloc(nil);
|
||||||
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
|
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.initWithString(string);
|
||||||
};
|
};
|
||||||
*marked_text_ref = marked_text;
|
*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 _: () = msg_send![s, release];
|
||||||
let input_context: id = msg_send![this, inputContext];
|
let input_context: id = msg_send![this, inputContext];
|
||||||
let _: () = msg_send![input_context, discardMarkedText];
|
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_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||||
let state = &mut *(state_ptr as *mut ViewState);
|
let state = &mut *(state_ptr as *mut ViewState);
|
||||||
|
|
||||||
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
|
let string = id_to_string_lossy(string);
|
||||||
let characters = if has_attr != NO {
|
|
||||||
// This is a *mut NSAttributedString
|
|
||||||
msg_send![string, string]
|
|
||||||
} else {
|
|
||||||
// This is already a *mut NSString
|
|
||||||
string
|
|
||||||
};
|
|
||||||
|
|
||||||
let slice =
|
let is_control = string.chars().next().map_or(false, |c| c.is_control());
|
||||||
slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len());
|
|
||||||
let string = str::from_utf8_unchecked(slice);
|
|
||||||
|
|
||||||
// We don't need this now, but it's here if that changes.
|
// We don't need this now, but it's here if that changes.
|
||||||
//let event: id = msg_send![NSApp(), currentEvent];
|
//let event: id = msg_send![NSApp(), currentEvent];
|
||||||
|
|
||||||
let mut events = VecDeque::with_capacity(characters.len());
|
if state.is_ime_enabled() && !is_control {
|
||||||
for character in string.chars().filter(|c| !is_corporate_character(*c)) {
|
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||||
events.push_back(EventWrapper::StaticEvent(Event::WindowEvent {
|
|
||||||
window_id: WindowId(get_window_id(state.ns_window)),
|
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:");
|
trace_scope!("doCommandBySelector:");
|
||||||
// Basically, we're sent this message whenever a keyboard event that doesn't generate a "human readable" character
|
// 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.
|
// 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_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||||
let state = &mut *(state_ptr as *mut ViewState);
|
let state = &mut *(state_ptr as *mut ViewState);
|
||||||
|
|
||||||
let mut events = VecDeque::with_capacity(1);
|
state.forward_key_to_app = true;
|
||||||
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),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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,17 +723,48 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) {
|
||||||
let state_ptr: *mut c_void = *this.get_ivar("winitState");
|
let state_ptr: *mut c_void = *this.get_ivar("winitState");
|
||||||
let state = &mut *(state_ptr as *mut ViewState);
|
let state = &mut *(state_ptr as *mut ViewState);
|
||||||
let window_id = WindowId(get_window_id(state.ns_window));
|
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 scancode = get_scancode(event) as u32;
|
||||||
let virtual_keycode = retrieve_keycode(event);
|
let virtual_keycode = retrieve_keycode(event);
|
||||||
|
|
||||||
let is_repeat: BOOL = msg_send![event, isARepeat];
|
|
||||||
|
|
||||||
update_potentially_stale_modifiers(state, event);
|
update_potentially_stale_modifiers(state, event);
|
||||||
|
|
||||||
|
let preedit_related = was_in_preedit || now_in_preedit;
|
||||||
|
if !preedit_related || state.forward_key_to_app || !state.ime_allowed {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let window_event = Event::WindowEvent {
|
let window_event = Event::WindowEvent {
|
||||||
window_id,
|
window_id,
|
||||||
|
@ -663,28 +780,14 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let pass_along = {
|
|
||||||
AppState::queue_event(EventWrapper::StaticEvent(window_event));
|
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)) {
|
for character in characters.chars().filter(|c| !is_corporate_character(*c)) {
|
||||||
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
|
||||||
window_id,
|
window_id,
|
||||||
event: WindowEvent::ReceivedCharacter(character),
|
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];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -700,6 +803,8 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) {
|
||||||
|
|
||||||
update_potentially_stale_modifiers(state, event);
|
update_potentially_stale_modifiers(state, event);
|
||||||
|
|
||||||
|
// We want to send keyboard input when we are not currently in preedit
|
||||||
|
if state.ime_state != ImeState::Preedit {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let window_event = Event::WindowEvent {
|
let window_event = Event::WindowEvent {
|
||||||
window_id: WindowId(get_window_id(state.ns_window)),
|
window_id: WindowId(get_window_id(state.ns_window)),
|
||||||
|
@ -718,6 +823,7 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) {
|
||||||
AppState::queue_event(EventWrapper::StaticEvent(window_event));
|
AppState::queue_event(EventWrapper::StaticEvent(window_event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" fn flags_changed(this: &Object, _sel: Sel, event: id) {
|
extern "C" fn flags_changed(this: &Object, _sel: Sel, event: id) {
|
||||||
trace_scope!("flagsChanged:");
|
trace_scope!("flagsChanged:");
|
||||||
|
|
|
@ -461,7 +461,7 @@ impl UnownedWindow {
|
||||||
if maximized {
|
if maximized {
|
||||||
window.set_maximized(maximized);
|
window.set_maximized(maximized);
|
||||||
}
|
}
|
||||||
|
trace!("Done unowned window::new");
|
||||||
Ok((window, delegate))
|
Ok((window, delegate))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1054,6 +1054,13 @@ impl UnownedWindow {
|
||||||
unsafe { view::set_ime_position(*self.ns_view, logical_spot) };
|
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]
|
#[inline]
|
||||||
pub fn focus_window(&self) {
|
pub fn focus_window(&self) {
|
||||||
let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] };
|
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
|
// 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]
|
#[inline]
|
||||||
pub fn focus_window(&self) {
|
pub fn focus_window(&self) {
|
||||||
// Currently a no-op as it does not seem there is good support for this on web
|
// 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::{
|
UI::{
|
||||||
Controls::{HOVER_DEFAULT, WM_MOUSELEAVE},
|
Controls::{HOVER_DEFAULT, WM_MOUSELEAVE},
|
||||||
Input::{
|
Input::{
|
||||||
|
Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW},
|
||||||
KeyboardAndMouse::{
|
KeyboardAndMouse::{
|
||||||
MapVirtualKeyA, ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE,
|
MapVirtualKeyA, ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE,
|
||||||
TRACKMOUSEEVENT, VK_F4,
|
TRACKMOUSEEVENT, VK_F4,
|
||||||
|
@ -59,21 +60,23 @@ use windows_sys::Win32::{
|
||||||
SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
|
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,
|
SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, WM_CREATE,
|
||||||
WM_DESTROY, WM_DPICHANGED, WM_DROPFILES, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE,
|
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_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT,
|
||||||
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL,
|
WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP,
|
||||||
WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT,
|
WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP,
|
||||||
WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP,
|
WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY,
|
||||||
WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SYSCHAR, WM_SYSCOMMAND,
|
WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE,
|
||||||
WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING,
|
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE,
|
||||||
WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE,
|
WM_SYSCHAR, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED,
|
||||||
WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
|
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::{
|
use crate::{
|
||||||
dpi::{PhysicalPosition, PhysicalSize},
|
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},
|
event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW},
|
||||||
monitor::MonitorHandle as RootMonitorHandle,
|
monitor::MonitorHandle as RootMonitorHandle,
|
||||||
platform_impl::platform::{
|
platform_impl::platform::{
|
||||||
|
@ -81,10 +84,11 @@ use crate::{
|
||||||
dpi::{become_dpi_aware, dpi_to_scale_factor},
|
dpi::{become_dpi_aware, dpi_to_scale_factor},
|
||||||
drop_handler::FileDropHandler,
|
drop_handler::FileDropHandler,
|
||||||
event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey},
|
event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey},
|
||||||
|
ime::ImeContext,
|
||||||
monitor::{self, MonitorHandle},
|
monitor::{self, MonitorHandle},
|
||||||
raw_input, util,
|
raw_input, util,
|
||||||
window::InitData,
|
window::InitData,
|
||||||
window_state::{CursorFlags, WindowFlags, WindowState},
|
window_state::{CursorFlags, ImeState, WindowFlags, WindowState},
|
||||||
wrap_device_id, WindowId, DEVICE_ID,
|
wrap_device_id, WindowId, DEVICE_ID,
|
||||||
},
|
},
|
||||||
window::{Fullscreen, WindowId as RootWindowId},
|
window::{Fullscreen, WindowId as RootWindowId},
|
||||||
|
@ -1128,6 +1132,104 @@ unsafe fn public_window_callback_inner<T: 'static>(
|
||||||
0
|
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
|
// this is necessary for us to maintain minimize/restore state
|
||||||
WM_SYSCOMMAND => {
|
WM_SYSCOMMAND => {
|
||||||
if wparam == SC_RESTORE as usize {
|
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;
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
mod ime;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
mod raw_input;
|
mod raw_input;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
|
@ -31,10 +31,6 @@ use windows_sys::Win32::{
|
||||||
},
|
},
|
||||||
UI::{
|
UI::{
|
||||||
Input::{
|
Input::{
|
||||||
Ime::{
|
|
||||||
ImmGetContext, ImmReleaseContext, ImmSetCompositionWindow, CFS_POINT,
|
|
||||||
COMPOSITIONFORM,
|
|
||||||
},
|
|
||||||
KeyboardAndMouse::{
|
KeyboardAndMouse::{
|
||||||
EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, INPUT,
|
EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, INPUT,
|
||||||
INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
|
INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
|
||||||
|
@ -49,8 +45,8 @@ use windows_sys::Win32::{
|
||||||
SetWindowPlacement, SetWindowPos, SetWindowTextW, CS_HREDRAW, CS_VREDRAW,
|
SetWindowPlacement, SetWindowPos, SetWindowTextW, CS_HREDRAW, CS_VREDRAW,
|
||||||
CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY,
|
CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY,
|
||||||
GWLP_HINSTANCE, HTCAPTION, MAPVK_VK_TO_VSC, NID_READY, PM_NOREMOVE, SM_DIGITIZER,
|
GWLP_HINSTANCE, HTCAPTION, MAPVK_VK_TO_VSC, NID_READY, PM_NOREMOVE, SM_DIGITIZER,
|
||||||
SM_IMMENABLED, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER,
|
SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, WM_NCLBUTTONDOWN,
|
||||||
WM_NCLBUTTONDOWN, WNDCLASSEXW,
|
WNDCLASSEXW,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -69,6 +65,7 @@ use crate::{
|
||||||
drop_handler::FileDropHandler,
|
drop_handler::FileDropHandler,
|
||||||
event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID},
|
event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID},
|
||||||
icon::{self, IconType},
|
icon::{self, IconType},
|
||||||
|
ime::ImeContext,
|
||||||
monitor, util,
|
monitor, util,
|
||||||
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
|
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
|
||||||
Parent, PlatformSpecificWindowBuilderAttributes, WindowId,
|
Parent, PlatformSpecificWindowBuilderAttributes, WindowId,
|
||||||
|
@ -626,25 +623,19 @@ impl Window {
|
||||||
self.window_state.lock().taskbar_icon = taskbar_icon;
|
self.window_state.lock().taskbar_icon = taskbar_icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) {
|
#[inline]
|
||||||
if unsafe { GetSystemMetrics(SM_IMMENABLED) } != 0 {
|
pub fn set_ime_position(&self, spot: Position) {
|
||||||
let composition_form = COMPOSITIONFORM {
|
|
||||||
dwStyle: CFS_POINT,
|
|
||||||
ptCurrentPos: POINT { x, y },
|
|
||||||
rcArea: unsafe { mem::zeroed() },
|
|
||||||
};
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let himc = ImmGetContext(self.hwnd());
|
ImeContext::current(self.hwnd()).set_ime_position(spot, self.scale_factor());
|
||||||
ImmSetCompositionWindow(himc, &composition_form);
|
|
||||||
ImmReleaseContext(self.hwnd(), himc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_ime_position(&self, spot: Position) {
|
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||||
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
|
self.window_state.lock().ime_allowed = allowed;
|
||||||
self.set_ime_position_physical(x, y);
|
unsafe {
|
||||||
|
ImeContext::set_ime_allowed(self.hwnd(), allowed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -798,6 +789,8 @@ impl<'a, T: 'static> InitData<'a, T> {
|
||||||
|
|
||||||
enable_non_client_dpi_scaling(window);
|
enable_non_client_dpi_scaling(window);
|
||||||
|
|
||||||
|
ImeContext::set_ime_allowed(window, false);
|
||||||
|
|
||||||
Window {
|
Window {
|
||||||
window: WindowWrapper(window),
|
window: WindowWrapper(window),
|
||||||
window_state,
|
window_state,
|
||||||
|
|
|
@ -42,6 +42,9 @@ pub struct WindowState {
|
||||||
pub preferred_theme: Option<Theme>,
|
pub preferred_theme: Option<Theme>,
|
||||||
pub high_surrogate: Option<u16>,
|
pub high_surrogate: Option<u16>,
|
||||||
pub window_flags: WindowFlags,
|
pub window_flags: WindowFlags,
|
||||||
|
|
||||||
|
pub ime_state: ImeState,
|
||||||
|
pub ime_allowed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -101,6 +104,13 @@ bitflags! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq)]
|
||||||
|
pub enum ImeState {
|
||||||
|
Disabled,
|
||||||
|
Enabled,
|
||||||
|
Preedit,
|
||||||
|
}
|
||||||
|
|
||||||
impl WindowState {
|
impl WindowState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
attributes: &WindowAttributes,
|
attributes: &WindowAttributes,
|
||||||
|
@ -132,6 +142,9 @@ impl WindowState {
|
||||||
preferred_theme,
|
preferred_theme,
|
||||||
high_surrogate: None,
|
high_surrogate: None,
|
||||||
window_flags: WindowFlags::empty(),
|
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.
|
/// 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
|
/// ```no_run
|
||||||
/// # use winit::dpi::{LogicalPosition, PhysicalPosition};
|
/// # use winit::dpi::{LogicalPosition, PhysicalPosition};
|
||||||
/// # use winit::event_loop::EventLoop;
|
/// # use winit::event_loop::EventLoop;
|
||||||
|
@ -830,11 +837,41 @@ impl Window {
|
||||||
/// ## Platform-specific
|
/// ## Platform-specific
|
||||||
///
|
///
|
||||||
/// - **iOS / Android / Web:** Unsupported.
|
/// - **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]
|
#[inline]
|
||||||
pub fn set_ime_position<P: Into<Position>>(&self, position: P) {
|
pub fn set_ime_position<P: Into<Position>>(&self, position: P) {
|
||||||
self.window.set_ime_position(position.into())
|
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
|
/// Brings the window to the front and sets input focus. Has no effect if the window is
|
||||||
/// already in focus, minimized, or not visible.
|
/// already in focus, minimized, or not visible.
|
||||||
///
|
///
|
||||||
|
|
Loading…
Reference in a new issue