diff --git a/examples/application.rs b/examples/application.rs index b319c4bc..d2abca07 100644 --- a/examples/application.rs +++ b/examples/application.rs @@ -13,6 +13,7 @@ use std::{fmt, mem}; use ::tracing::{error, info}; use cursor_icon::CursorIcon; +use dpi::LogicalPosition; #[cfg(not(android_platform))] use rwh_06::{DisplayHandle, HasDisplayHandle}; #[cfg(not(android_platform))] @@ -38,8 +39,12 @@ use winit::platform::wayland::{ActiveEventLoopExtWayland, WindowAttributesWaylan use winit::platform::web::{ActiveEventLoopExtWeb, WindowAttributesWeb}; #[cfg(x11_platform)] use winit::platform::x11::{ActiveEventLoopExtX11, WindowAttributesX11}; -use winit::window::{CursorGrabMode, ResizeDirection, Theme, Window, WindowAttributes, WindowId}; +use winit::window::{ + CursorGrabMode, ImeCapabilities, ImeEnableRequest, ImePurpose, ImeRequestData, ResizeDirection, + Theme, Window, WindowAttributes, WindowId, +}; use winit_core::application::macos::ApplicationHandlerExtMacOS; +use winit_core::window::ImeRequest; #[path = "util/tracing.rs"] mod tracing; @@ -49,6 +54,7 @@ mod fill; /// The amount of points to around the window for drag resize direction calculations. const BORDER_SIZE: f64 = 20.; +const IME_CURSOR_SIZE: PhysicalSize = PhysicalSize::new(20, 20); fn main() -> Result<(), Box> { #[cfg(web_platform)] @@ -604,8 +610,7 @@ impl ApplicationHandlerExtMacOS for Application { /// State of the window. struct WindowState { - /// IME input. - ime: bool, + ime_enabled: bool, /// Render surface. /// /// NOTE: This surface must be dropped before the `Window`. @@ -661,8 +666,14 @@ impl WindowState { window.set_cursor(CURSORS[named_idx].into()); // Allow IME out of the box. - let ime = true; - window.set_ime_allowed(ime); + let request_data = ImeRequestData::default() + .with_purpose(ImePurpose::Normal) + .with_cursor_area(LogicalPosition { x: 0, y: 0 }.into(), IME_CURSOR_SIZE.into()); + let enable_request = ImeEnableRequest::new(ImeCapabilities::all(), request_data).unwrap(); + let enable_ime = ImeRequest::Enable(enable_request); + + // Initial update + window.request_ime_update(enable_ime).unwrap(); let size = window.surface_size(); let mut state = Self { @@ -679,7 +690,7 @@ impl WindowState { continuous_redraw: false, #[cfg(not(android_platform))] start_time: Instant::now(), - ime, + ime_enabled: true, cursor_position: Default::default(), cursor_hidden: Default::default(), modifiers: Default::default(), @@ -694,11 +705,21 @@ impl WindowState { } pub fn toggle_ime(&mut self) { - self.ime = !self.ime; - self.window.set_ime_allowed(self.ime); - if let Some(position) = self.ime.then_some(self.cursor_position).flatten() { - self.window.set_ime_cursor_area(position.into(), PhysicalSize::new(20, 20).into()); - } + if self.ime_enabled { + self.window.request_ime_update(ImeRequest::Disable).expect("disable can not fail"); + } else { + let cursor_pos = self + .cursor_position + .map(Into::into) + .unwrap_or(LogicalPosition { x: 0, y: 0 }.into()); + let request_data = + ImeRequestData::default().with_cursor_area(cursor_pos, IME_CURSOR_SIZE.into()); + let enable_request = + ImeEnableRequest::new(ImeCapabilities::all(), request_data).unwrap(); + self.window.request_ime_update(ImeRequest::Enable(enable_request)).unwrap(); + }; + + self.ime_enabled = !self.ime_enabled; } pub fn minimize(&mut self) { @@ -706,9 +727,15 @@ impl WindowState { } pub fn cursor_moved(&mut self, position: PhysicalPosition) { + // the IME really cares about the caret, + // but there's nothing else to demonstrate a position self.cursor_position = Some(position); - if self.ime { - self.window.set_ime_cursor_area(position.into(), PhysicalSize::new(20, 20).into()); + if self.ime_enabled { + let request_data = + ImeRequestData::default().with_cursor_area(position.into(), IME_CURSOR_SIZE.into()); + self.window + .request_ime_update(ImeRequest::Update(request_data)) + .expect("A capability was not initially declared"); } } diff --git a/winit-android/src/event_loop.rs b/winit-android/src/event_loop.rs index 56d18283..1396574c 100644 --- a/winit-android/src/event_loop.rs +++ b/winit-android/src/event_loop.rs @@ -23,8 +23,9 @@ use winit_core::event_loop::{ }; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ - self, CursorGrabMode, ImePurpose, ResizeDirection, Theme, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + self, CursorGrabMode, ImeCapabilities, ImePurpose, ImeRequest, ImeRequestError, + ResizeDirection, Theme, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, }; use crate::keycodes; @@ -758,6 +759,7 @@ pub struct PlatformSpecificWindowAttributes; #[derive(Debug)] pub struct Window { app: AndroidApp, + ime_capabilities: Mutex>, redraw_requester: RedrawRequester, } @@ -768,7 +770,11 @@ impl Window { ) -> Result { // FIXME this ignores requested window attributes - Ok(Self { app: el.app.clone(), redraw_requester: el.redraw_requester.clone() }) + Ok(Self { + app: el.app.clone(), + ime_capabilities: Default::default(), + redraw_requester: el.redraw_requester.clone(), + }) } pub(crate) fn config(&self) -> ConfigurationRef { @@ -936,12 +942,33 @@ impl CoreWindow for Window { fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} - fn set_ime_allowed(&self, allowed: bool) { - if allowed { - self.app.show_soft_input(true); - } else { - self.app.hide_soft_input(true); + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + let mut current_caps = self.ime_capabilities.lock().unwrap(); + match request { + ImeRequest::Enable(enable) => { + let (capabilities, _) = enable.into_raw(); + if current_caps.is_some() { + return Err(ImeRequestError::AlreadyEnabled); + } + *current_caps = Some(capabilities); + self.app.show_soft_input(true); + }, + ImeRequest::Update(_) => { + if current_caps.is_none() { + return Err(ImeRequestError::NotEnabled); + } + }, + ImeRequest::Disable => { + *current_caps = None; + self.app.hide_soft_input(true); + }, } + + Ok(()) + } + + fn ime_capabilities(&self) -> Option { + *self.ime_capabilities.lock().unwrap() } fn set_ime_purpose(&self, _purpose: ImePurpose) {} diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 8435ebf7..8f8976e9 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -21,6 +21,7 @@ use winit_core::event::{ PointerKind, PointerSource, TouchPhase, WindowEvent, }; use winit_core::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey}; +use winit_core::window::ImeCapabilities; use super::app_state::AppState; use super::cursor::{default_cursor, invisible_cursor}; @@ -124,7 +125,7 @@ pub struct ViewState { /// True iff the application wants IME events. /// /// Can be set using `set_ime_allowed` - ime_allowed: Cell, + ime_capabilities: Cell>, /// True if the current key event should be forwarded /// to the application, even during IME @@ -456,7 +457,7 @@ define_class!( // 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 self.ivars().ime_allowed.get() { + if self.ivars().ime_capabilities.get().is_some() { let events_for_nsview = NSArray::from_slice(&[&*event]); unsafe { self.interpretKeyEvents(&events_for_nsview) }; @@ -797,7 +798,7 @@ impl WinitView { tracking_rect: Default::default(), ime_state: Default::default(), input_source: Default::default(), - ime_allowed: Default::default(), + ime_capabilities: Default::default(), forward_key_to_app: Default::default(), marked_text: Default::default(), accepts_first_mouse, @@ -859,12 +860,13 @@ impl WinitView { } } - pub(super) fn set_ime_allowed(&self, ime_allowed: bool) { - if self.ivars().ime_allowed.get() == ime_allowed { + pub(super) fn set_ime_allowed(&self, capabilities: Option) { + if self.ivars().ime_capabilities.get().is_some() { return; } - self.ivars().ime_allowed.set(ime_allowed); - if self.ivars().ime_allowed.get() { + self.ivars().ime_capabilities.set(capabilities); + + if capabilities.is_some() { return; } @@ -877,6 +879,10 @@ impl WinitView { } } + pub(super) fn ime_capabilities(&self) -> Option { + self.ivars().ime_capabilities.get() + } + pub(super) fn set_ime_cursor_area(&self, position: NSPoint, size: NSSize) { self.ivars().ime_position.set(position); self.ivars().ime_size.set(size); diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index 05d2199f..515ac55f 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -13,8 +13,8 @@ use winit_core::error::RequestError; use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ - ImePurpose, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, - WindowId, WindowLevel, + ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow, + WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use super::event_loop::ActiveEventLoop; @@ -233,16 +233,12 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.set_window_icon(window_icon)); } - fn set_ime_cursor_area(&self, position: Position, size: Size) { - self.maybe_wait_on_main(|delegate| delegate.set_ime_cursor_area(position, size)); + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + self.maybe_wait_on_main(|delegate| delegate.request_ime_update(request)) } - fn set_ime_allowed(&self, allowed: bool) { - self.maybe_wait_on_main(|delegate| delegate.set_ime_allowed(allowed)); - } - - fn set_ime_purpose(&self, purpose: ImePurpose) { - self.maybe_wait_on_main(|delegate| delegate.set_ime_purpose(purpose)); + fn ime_capabilities(&self) -> Option { + self.maybe_wait_on_main(|delegate| delegate.ime_capabilities()) } fn focus_window(&self) { diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index c8eb1d93..f8f06016 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -46,8 +46,8 @@ use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowId, WindowLevel, + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, + UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use super::app_state::AppState; @@ -1674,26 +1674,50 @@ impl WindowDelegate { // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Tasks/SettingWindowTitle.html } - #[inline] - pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { - let scale_factor = self.scale_factor(); - let logical_spot = spot.to_logical(scale_factor); - let logical_spot = NSPoint::new(logical_spot.x, logical_spot.y); + pub fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + let current_caps = self.view().ime_capabilities(); + let request_data = match request { + ImeRequest::Enable(enable) => { + let (capabilities, request_data) = enable.into_raw(); + if current_caps.is_some() { + return Err(ImeRequestError::AlreadyEnabled); + } + self.view().set_ime_allowed(Some(capabilities)); + request_data + }, + ImeRequest::Update(request_data) => { + if current_caps.is_none() { + return Err(ImeRequestError::NotEnabled); + } + request_data + }, + ImeRequest::Disable => { + self.view().set_ime_allowed(None); + return Ok(()); + }, + }; - let size = size.to_logical(scale_factor); - let size = NSSize::new(size.width, size.height); + if let Some((spot, size)) = request_data.cursor_area { + if self.view().ime_capabilities().unwrap().contains(ImeCapabilities::CURSOR_AREA) { + let scale_factor = self.scale_factor(); + let logical_spot = spot.to_logical(scale_factor); + let logical_spot = NSPoint::new(logical_spot.x, logical_spot.y); - self.view().set_ime_cursor_area(logical_spot, size); + let size = size.to_logical(scale_factor); + let size = NSSize::new(size.width, size.height); + self.view().set_ime_cursor_area(logical_spot, size); + } else { + warn!("discarding IME cursor area update without capability enabled."); + } + } + + Ok(()) } - #[inline] - pub fn set_ime_allowed(&self, allowed: bool) { - self.view().set_ime_allowed(allowed); + pub fn ime_capabilities(&self) -> Option { + self.view().ime_capabilities() } - #[inline] - pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} - #[inline] pub fn focus_window(&self) { let mtm = MainThreadMarker::from(self); diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 3b021cb2..8926e948 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -2,7 +2,9 @@ use std::fmt; use cursor_icon::CursorIcon; -use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; +use dpi::{ + LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -767,7 +769,7 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// /// - **Android / Orbital / Wayland / Windows / X11:** Unimplemented, returns `(0, 0, 0, 0)`. /// - /// ## Examples + /// ## Example /// /// Convert safe area insets to a size and a position. /// @@ -1096,13 +1098,24 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// /// [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 - fn set_ime_cursor_area(&self, position: Position, size: Size); + #[deprecated = "use Window::request_ime_update instead"] + fn set_ime_cursor_area(&self, position: Position, size: Size) { + if self + .ime_capabilities() + .map(|caps| caps.contains(ImeCapabilities::CURSOR_AREA)) + .unwrap_or(false) + { + let _ = self.request_ime_update(ImeRequest::Update( + ImeRequestData::default().with_cursor_area(position, size), + )); + } + } /// 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`] events. The window - /// should allow IME while it is expecting text input. + /// should allow IME when 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. Not allowing @@ -1120,14 +1133,101 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// /// [`Ime`]: crate::event::WindowEvent::Ime /// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput - fn set_ime_allowed(&self, allowed: bool); + #[deprecated = "use Window::request_ime_update instead"] + fn set_ime_allowed(&self, allowed: bool) { + let action = if allowed { + let position = LogicalPosition::new(0, 0); + let size = LogicalSize::new(0, 0); + let ime_caps = ImeCapabilities::CURSOR_AREA | ImeCapabilities::PURPOSE; + let request_data = ImeRequestData { + purpose: Some(ImePurpose::Normal), + // WARNING: there's nothing sensible to use here by default. + cursor_area: Some((position.into(), size.into())), + ..ImeRequestData::default() + }; + + // Enable all capabilities to reflect the old behavior. + ImeRequest::Enable(ImeEnableRequest::new(ime_caps, request_data).unwrap()) + } else { + ImeRequest::Disable + }; + + let _ = self.request_ime_update(action); + } /// Sets the IME purpose for the window using [`ImePurpose`]. /// /// ## Platform-specific /// /// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. - fn set_ime_purpose(&self, purpose: ImePurpose); + #[deprecated = "use Window::request_ime_update instead"] + fn set_ime_purpose(&self, purpose: ImePurpose) { + if self + .ime_capabilities() + .map(|caps| caps.contains(ImeCapabilities::PURPOSE)) + .unwrap_or(false) + { + let _ = self.request_ime_update(ImeRequest::Update(ImeRequestData { + purpose: Some(purpose), + ..ImeRequestData::default() + })); + } + } + + /// Atomically apply request to IME. + /// + /// For details consult [`ImeRequest`] and [`ImeCapabilities`]. + /// + /// Input methods allows the user to compose text without using a keyboard. Requesting one may + /// be beneficial for touch screen environments or ones where, for example, East Asian scripts + /// may be entered. + /// + /// If the focus within the application changes from one logical text input area to another, the + /// application should inform the IME of the switch by disabling the IME and enabling it again + /// in the other area. + /// + /// IME is **not** enabled by default. + /// + /// ## Example + /// + /// ```no_run + /// # use dpi::{Position, Size}; + /// # use winit_core::window::{Window, ImePurpose, ImeRequest, ImeCapabilities, ImeRequestData, ImeEnableRequest}; + /// # fn scope(window: &dyn Window, cursor_pos: Position, cursor_size: Size) { + /// // Clear previous state by switching off IME + /// window.request_ime_update(ImeRequest::Disable).expect("Disable cannot fail"); + /// + /// let ime_caps = ImeCapabilities::CURSOR_AREA | ImeCapabilities::PURPOSE; + /// let request_data = ImeRequestData::default() + /// .with_purpose(ImePurpose::Normal) + /// .with_cursor_area(cursor_pos, cursor_size); + /// let enable_ime = ImeEnableRequest::new(ime_caps, request_data.clone()).unwrap(); + /// window.request_ime_update(ImeRequest::Enable(enable_ime)).expect("Enabling may fail if IME is not supported"); + /// + /// // Update the current state + /// window + /// .request_ime_update(ImeRequest::Update(request_data.clone())) + /// .expect("will fail if it's not enabled or ime is not supported"); + /// + /// // Update the current state + /// window + /// .request_ime_update(ImeRequest::Update( + /// request_data.with_cursor_area(cursor_pos, cursor_size), + /// )) + /// .expect("Can fail - we didn't submit a cursor position initially"); + /// + /// // Switch off IME + /// window.request_ime_update(ImeRequest::Disable).expect("Disable cannot fail"); + /// # } + /// ``` + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError>; + + /// Return enabled by the client [`ImeCapabilities`] for this window. + /// + /// When the IME is not yet enabled it'll return `None`. + /// + /// By default IME is disabled, thus will return `None`. + fn ime_capabilities(&self) -> Option; /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. @@ -1235,7 +1335,7 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// Set grabbing [mode][CursorGrabMode] on the cursor preventing it from leaving the window. /// - /// # Example + /// ## Example /// /// First try confining the cursor, and if that fails, try locking it instead. /// @@ -1530,6 +1630,185 @@ impl Default for ImePurpose { } } +/// Request to send to IME. +#[derive(Debug, PartialEq, Clone)] +pub enum ImeRequest { + /// Enable the IME with the [`ImeCapabilities`] and [`ImeRequestData`] as initial state. When + /// the [`ImeRequestData`] is **not** matching capabilities fully, the default values will be + /// used instead. + /// + /// **Requesting to update data matching not enabled capabilities will result in update + /// being ignored.** The winit backend in such cases is recommended to log a warning. This + /// appiles to both [`ImeRequest::Enable`] and [`ImeRequest::Update`]. For details on + /// capabilities refer to [`ImeCapabilities`]. + /// + /// To update the [`ImeCapabilities`], the IME must be disabled and then re-enabled. + Enable(ImeEnableRequest), + /// Update the state of already enabled IME. Issuing this request before [`ImeRequest::Enable`] + /// will result in error. + Update(ImeRequestData), + /// Disable the IME. + /// + /// **The disable request can not fail**. + Disable, +} + +/// Initial IME request. +#[derive(Debug, Clone, PartialEq)] +pub struct ImeEnableRequest { + capabilities: ImeCapabilities, + request_data: ImeRequestData, +} + +impl ImeEnableRequest { + /// Create request for the [`ImeRequest::Enable`] + /// + /// This will return [`None`] if some capability was requested but its initial value was not + /// set by the user or value was set by the user, but capability not requested. + pub const fn new(capabilities: ImeCapabilities, request_data: ImeRequestData) -> Option { + if capabilities.contains(ImeCapabilities::CURSOR_AREA) ^ request_data.cursor_area.is_some() + { + return None; + } + + if capabilities.contains(ImeCapabilities::PURPOSE) ^ request_data.purpose.is_some() { + return None; + } + + Some(Self { capabilities, request_data }) + } + + /// [`ImeCapabilities`] to enable. + pub const fn capabilities(&self) -> &ImeCapabilities { + &self.capabilities + } + + /// Request data attached to request. + pub const fn request_data(&self) -> &ImeRequestData { + &self.request_data + } + + /// Destruct [`ImeEnableRequest`] into its raw parts. + pub const fn into_raw(self) -> (ImeCapabilities, ImeRequestData) { + (self.capabilities, self.request_data) + } +} + +bitflags::bitflags! { + /// IME capabilities supported by client. + /// + /// For example, if the client doesn't support [`ImeCapabilities::CURSOR_AREA`] not enabling + /// it will make IME hide the popup window instead of placing it arbitrary over the + /// client's window surface. + /// + /// When the capability is not enabled or not supported by the IME, trying to update its' + /// corresponding data with [`ImeRequest`] will be ignored. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct ImeCapabilities: u32 { + /// Client supports setting IME purpose. + const PURPOSE = 0b1; + /// Client supports reporting cursor area for IME popup to + /// appear. + const CURSOR_AREA = 0b10; + } +} + +/// The [`ImeRequest`] data to communicate to system's IME. +/// +/// This applies multiple IME state properties at once. +/// Fields set to `None` are not updated and the previously sent +/// value is reused. +#[non_exhaustive] +#[derive(Debug, PartialEq, Clone, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct ImeRequestData { + /// Text input purpose. + /// + /// To support updating it, enable [`ImeCapabilities::PURPOSE`]. + pub purpose: Option, + /// The IME cursor area which should not be covered by the input method popup. + /// + /// To support updating it, enable [`ImeCapabilities::CURSOR_AREA`]. + pub cursor_area: Option<(Position, Size)>, +} + +impl ImeRequestData { + /// Sets the purpose hint of the current text input. + pub fn with_purpose(self, purpose: ImePurpose) -> Self { + Self { purpose: Some(purpose), ..self } + } + + /// Sets the IME cursor editing area. + /// + /// The `position` is the top left corner of that area + /// in surface coordinates and `size` is the size of this area starting from the position. An + /// example of such area could be a input field in the UI or line in the editor. + /// + /// The windowing system could place a candidate box close to that area, but try to not obscure + /// the specified area, so the user input to it stays visible. + /// + /// The candidate box is the window / popup / overlay that allows you to select the desired + /// characters. The look of this box may differ between input devices, even on the same + /// platform. + /// + /// (Apple's official term is "candidate window", see their [chinese] and [japanese] guides). + /// + /// ## Example + /// + /// ```no_run + /// # use dpi::{LogicalPosition, PhysicalPosition, LogicalSize, PhysicalSize}; + /// # use winit_core::window::ImeRequestData; + /// # fn scope(ime_request_data: ImeRequestData) { + /// // Specify the position in logical dimensions like this: + /// let ime_request_data = ime_request_data.with_cursor_area( + /// LogicalPosition::new(400.0, 200.0).into(), + /// LogicalSize::new(100, 100).into(), + /// ); + /// + /// // Or specify the position in physical dimensions like this: + /// let ime_request_data = ime_request_data.with_cursor_area( + /// PhysicalPosition::new(400, 200).into(), + /// PhysicalSize::new(100, 100).into(), + /// ); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** 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 + pub fn with_cursor_area(self, position: Position, size: Size) -> Self { + Self { cursor_area: Some((position, size)), ..self } + } +} + +/// Error from sending request to IME with +/// [`Window::request_ime_update`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ImeRequestError { + /// IME is not yet enabled. + NotEnabled, + /// IME is already enabled. + AlreadyEnabled, + /// Not supported. + NotSupported, +} + +impl fmt::Display for ImeRequestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ImeRequestError::NotEnabled => write!(f, "ime is not enabled."), + ImeRequestError::AlreadyEnabled => write!(f, "ime is already enabled."), + ImeRequestError::NotSupported => write!(f, "ime is not supported."), + } + } +} + +impl std::error::Error for ImeRequestError {} + /// An opaque token used to activate the [`Window`]. /// /// [`Window`]: crate::window::Window @@ -1563,3 +1842,60 @@ impl ActivationToken { &self.token } } + +#[cfg(test)] +mod tests { + + use dpi::{LogicalPosition, LogicalSize, Position, Size}; + + use super::{ImeCapabilities, ImeEnableRequest, ImeRequestData}; + use crate::window::ImePurpose; + + #[test] + fn ime_initial_request_caps_match() { + let position: Position = LogicalPosition::new(0, 0).into(); + let size: Size = LogicalSize::new(0, 0).into(); + + assert!(ImeEnableRequest::new(ImeCapabilities::CURSOR_AREA, ImeRequestData::default()) + .is_none()); + assert!( + ImeEnableRequest::new(ImeCapabilities::PURPOSE, ImeRequestData::default()).is_none() + ); + + assert!(ImeEnableRequest::new( + ImeCapabilities::CURSOR_AREA, + ImeRequestData::default().with_purpose(ImePurpose::Normal) + ) + .is_none()); + + assert!(ImeEnableRequest::new( + ImeCapabilities::empty(), + ImeRequestData::default() + .with_purpose(ImePurpose::Normal) + .with_cursor_area(position, size) + ) + .is_none()); + + assert!(ImeEnableRequest::new( + ImeCapabilities::CURSOR_AREA, + ImeRequestData::default() + .with_purpose(ImePurpose::Normal) + .with_cursor_area(position, size) + ) + .is_none()); + + assert!(ImeEnableRequest::new( + ImeCapabilities::CURSOR_AREA, + ImeRequestData::default().with_cursor_area(position, size) + ) + .is_some()); + + assert!(ImeEnableRequest::new( + ImeCapabilities::all(), + ImeRequestData::default() + .with_purpose(ImePurpose::Normal) + .with_cursor_area(position, size) + ) + .is_some()); + } +} diff --git a/winit-orbital/src/window.rs b/winit-orbital/src/window.rs index 3452f00c..7f3918f7 100644 --- a/winit-orbital/src/window.rs +++ b/winit-orbital/src/window.rs @@ -6,7 +6,7 @@ use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use winit_core::cursor::Cursor; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; -use winit_core::window::{self, ImePurpose, Window as CoreWindow, WindowId}; +use winit_core::window::{self, Window as CoreWindow, WindowId}; use crate::event_loop::{ActiveEventLoop, EventLoopProxy}; use crate::{RedoxSocket, WindowProperties}; @@ -161,6 +161,10 @@ impl CoreWindow for Window { WindowId::from_raw(self.window_socket.fd) } + fn ime_capabilities(&self) -> Option { + None + } + #[inline] fn primary_monitor(&self) -> Option { None @@ -354,14 +358,9 @@ impl CoreWindow for Window { #[inline] fn set_window_icon(&self, _window_icon: Option) {} - #[inline] - fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} - - #[inline] - fn set_ime_allowed(&self, _allowed: bool) {} - - #[inline] - fn set_ime_purpose(&self, _purpose: ImePurpose) {} + fn request_ime_update(&self, _: window::ImeRequest) -> Result<(), window::ImeRequestError> { + Err(window::ImeRequestError::NotSupported) + } #[inline] fn focus_window(&self) {} diff --git a/winit-uikit/src/window.rs b/winit-uikit/src/window.rs index 5b9d7bcd..52acb038 100644 --- a/winit-uikit/src/window.rs +++ b/winit-uikit/src/window.rs @@ -1,7 +1,7 @@ #![allow(clippy::unnecessary_cast)] use std::collections::VecDeque; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use dispatch2::MainThreadBound; use dpi::{ @@ -23,8 +23,9 @@ use winit_core::event::WindowEvent; use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, }; use super::app_state::EventWrapper; @@ -113,6 +114,7 @@ pub struct Inner { view_controller: Retained, view: Retained, gl_or_metal_backed: bool, + ime_capabilities: Mutex>, } impl Inner { @@ -377,28 +379,42 @@ impl Inner { warn!("`Window::set_window_icon` is ignored on iOS") } - pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) { - warn!("`Window::set_ime_cursor_area` is ignored on iOS") - } - /// Show / hide the keyboard. To show the keyboard, we call `becomeFirstResponder`, /// requesting focus for the [WinitView]. Since [WinitView] implements /// [objc2_ui_kit::UIKeyInput], the keyboard will be shown. /// - pub fn set_ime_allowed(&self, allowed: bool) { - if allowed { - unsafe { - self.view.becomeFirstResponder(); - } - } else { - unsafe { - self.view.resignFirstResponder(); - } + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + let mut current_caps = self.ime_capabilities.lock().unwrap(); + match request { + ImeRequest::Enable(enable) => { + let (capabilities, _) = enable.into_raw(); + if current_caps.is_some() { + return Err(ImeRequestError::AlreadyEnabled); + } + *current_caps = Some(capabilities); + + unsafe { + self.view.becomeFirstResponder(); + } + }, + ImeRequest::Update(_) => { + if current_caps.is_none() { + return Err(ImeRequestError::NotEnabled); + } + }, + ImeRequest::Disable => { + *current_caps = None; + unsafe { + self.view.resignFirstResponder(); + } + }, } + + Ok(()) } - pub fn set_ime_purpose(&self, _purpose: ImePurpose) { - warn!("`Window::set_ime_purpose` is ignored on iOS") + fn ime_capabilities(&self) -> Option { + *self.ime_capabilities.lock().unwrap() } pub fn focus_window(&self) { @@ -529,7 +545,13 @@ impl Window { let window = WinitUIWindow::new(mtm, &window_attributes, frame, &view_controller); window.makeKeyAndVisible(); - let inner = Inner { window, view_controller, view, gl_or_metal_backed }; + let inner = Inner { + window, + view_controller, + view, + gl_or_metal_backed, + ime_capabilities: Default::default(), + }; Ok(Window { inner: MainThreadBound::new(inner, mtm) }) } @@ -711,16 +733,12 @@ impl CoreWindow for Window { self.maybe_wait_on_main(|delegate| delegate.set_window_icon(window_icon)); } - fn set_ime_cursor_area(&self, position: Position, size: Size) { - self.maybe_wait_on_main(|delegate| delegate.set_ime_cursor_area(position, size)); + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + self.maybe_wait_on_main(|delegate| delegate.request_ime_update(request)) } - fn set_ime_allowed(&self, allowed: bool) { - self.maybe_wait_on_main(|delegate| delegate.set_ime_allowed(allowed)); - } - - fn set_ime_purpose(&self, purpose: ImePurpose) { - self.maybe_wait_on_main(|delegate| delegate.set_ime_purpose(purpose)); + fn ime_capabilities(&self) -> Option { + self.maybe_wait_on_main(|delegate| delegate.ime_capabilities()) } fn focus_window(&self) { diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index e206e4c0..bf68d107 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -26,9 +26,11 @@ use keyboard::{KeyboardData, KeyboardState}; pub use pointer::relative_pointer::RelativePointerState; pub use pointer::{PointerConstraintsState, WinitPointerData, WinitPointerDataExt}; use text_input::TextInputData; -pub use text_input::{TextInputState, ZwpTextInputV3Ext}; +pub use text_input::{ClientState as TextInputClientState, TextInputState}; use touch::TouchPoint; +pub(crate) use crate::seat::text_input::ZwpTextInputV3Ext; + #[derive(Debug, Default)] pub struct WinitSeatState { /// The pointer bound on the seat. diff --git a/winit-wayland/src/seat/text_input/mod.rs b/winit-wayland/src/seat/text_input/mod.rs index be54eed5..02f8a9fb 100644 --- a/winit-wayland/src/seat/text_input/mod.rs +++ b/winit-wayland/src/seat/text_input/mod.rs @@ -1,5 +1,6 @@ use std::ops::Deref; +use dpi::{LogicalPosition, LogicalSize}; use sctk::globals::GlobalData; use sctk::reexports::client::globals::{BindError, GlobalList}; use sctk::reexports::client::protocol::wl_surface::WlSurface; @@ -8,8 +9,9 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_mana use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ ContentHint, ContentPurpose, Event as TextInputEvent, ZwpTextInputV3, }; +use tracing::warn; use winit_core::event::{Ime, WindowEvent}; -use winit_core::window::ImePurpose; +use winit_core::window::{ImeCapabilities, ImePurpose, ImeRequestData}; use crate::state::WinitState; @@ -69,10 +71,10 @@ impl Dispatch for TextInputState { None => return, }; - if window.ime_allowed() { - text_input.enable(); - text_input.set_content_type_by_purpose(window.ime_purpose()); - text_input.commit(); + if let Some(text_input_state) = window.text_input_state() { + text_input.set_state(Some(text_input_state), true); + // The input method doesn't have to reply anything, so a synthetic event + // carrying an empty state notifies the application about its presence. state.events_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id); } @@ -156,17 +158,40 @@ impl Dispatch for TextInputState { } pub trait ZwpTextInputV3Ext { - fn set_content_type_by_purpose(&self, purpose: ImePurpose); + /// Applies the entire state atomically to the input method. It will skip the "enable" request + /// if `already_enabled` is `true`. + fn set_state(&self, state: Option<&ClientState>, send_enable: bool); } impl ZwpTextInputV3Ext for ZwpTextInputV3 { - fn set_content_type_by_purpose(&self, purpose: ImePurpose) { - let (hint, purpose) = match purpose { - ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password), - ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal), - _ => (ContentHint::None, ContentPurpose::Normal), + fn set_state(&self, state: Option<&ClientState>, send_enable: bool) { + let state = match state { + Some(state) => state, + None => { + self.disable(); + self.commit(); + return; + }, }; - self.set_content_type(hint, purpose); + + if send_enable { + self.enable(); + } + + if let Some(content_type) = state.content_type() { + self.set_content_type(content_type.hint, content_type.purpose); + } + + if let Some((position, size)) = state.cursor_area() { + let (x, y) = (position.x as i32, position.y as i32); + let (width, height) = (size.width as i32, size.height as i32); + // The same cursor can be applied on different seats. + // It's the compositor's responsibility to make sure that any present popups don't + // overlap. + self.set_cursor_rectangle(x, y, width, height); + } + + self.commit(); } } @@ -189,11 +214,105 @@ pub struct TextInputDataInner { } /// The state of the preedit. +#[derive(Clone)] struct Preedit { text: String, cursor_begin: Option, cursor_end: Option, } +/// State change requested by the application. +/// +/// This is a version that uses text_input abstractions translated from the ones used in +/// winit::core::window::ImeStateChange. +/// +/// Fields that are initially set to None are unsupported capabilities +/// and trying to set them raises an error. +#[derive(Debug, PartialEq, Clone)] +pub struct ClientState { + capabilities: ImeCapabilities, + + content_type: ContentType, + + /// The IME cursor area which should not be covered by the input method popup. + cursor_area: (LogicalPosition, LogicalSize), +} + +impl ClientState { + pub fn new( + capabilities: ImeCapabilities, + request_data: ImeRequestData, + scale_factor: f64, + ) -> Self { + let mut this = Self { + capabilities, + content_type: Default::default(), + cursor_area: Default::default(), + }; + + this.update(request_data, scale_factor); + this + } + + pub fn capabilities(&self) -> ImeCapabilities { + self.capabilities + } + + /// Updates the fields of the state which are present in update_fields. + pub fn update(&mut self, request_data: ImeRequestData, scale_factor: f64) { + if let Some(purpose) = request_data.purpose { + if self.capabilities.contains(ImeCapabilities::PURPOSE) { + self.content_type = purpose.into(); + } else { + warn!("discarding ImePurpose update without capability enabled."); + } + } + + if let Some((position, size)) = request_data.cursor_area { + if self.capabilities.contains(ImeCapabilities::CURSOR_AREA) { + let position: LogicalPosition = position.to_logical(scale_factor); + let size: LogicalSize = size.to_logical(scale_factor); + self.cursor_area = (position, size); + } else { + warn!("discarding IME cursor area update without capability enabled."); + } + } + } + + pub fn content_type(&self) -> Option { + self.capabilities.contains(ImeCapabilities::PURPOSE).then_some(self.content_type) + } + + pub fn cursor_area(&self) -> Option<(LogicalPosition, LogicalSize)> { + self.capabilities.contains(ImeCapabilities::CURSOR_AREA).then_some(self.cursor_area) + } +} + +/// Arguments to content_type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ContentType { + /// Text input purpose + purpose: ContentPurpose, + hint: ContentHint, +} + +impl From for ContentType { + fn from(purpose: ImePurpose) -> Self { + let (hint, purpose) = match purpose { + ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password), + ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal), + _ => return Default::default(), + }; + + Self { hint, purpose } + } +} + +impl Default for ContentType { + fn default() -> Self { + ContentType { purpose: ContentPurpose::Normal, hint: ContentHint::None } + } +} + delegate_dispatch!(WinitState: [ZwpTextInputManagerV3: GlobalData] => TextInputState); delegate_dispatch!(WinitState: [ZwpTextInputV3: TextInputData] => TextInputState); diff --git a/winit-wayland/src/window/mod.rs b/winit-wayland/src/window/mod.rs index d5b1312e..fb5f22fe 100644 --- a/winit-wayland/src/window/mod.rs +++ b/winit-wayland/src/window/mod.rs @@ -20,8 +20,9 @@ use winit_core::event::{Ime, WindowEvent}; use winit_core::event_loop::AsyncRequestSerial; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, }; use super::event_loop::sink::EventSink; @@ -508,30 +509,21 @@ impl CoreWindow for Window { } #[inline] - fn set_ime_cursor_area(&self, position: Position, size: Size) { - let window_state = self.window_state.lock().unwrap(); - if window_state.ime_allowed() { - let scale_factor = window_state.scale_factor(); - let position = position.to_logical(scale_factor); - let size = size.to_logical(scale_factor); - window_state.set_ime_cursor_area(position, size); - } - } + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + let state_changed = self.window_state.lock().unwrap().request_ime_update(request)?; - #[inline] - fn set_ime_allowed(&self, allowed: bool) { - let mut window_state = self.window_state.lock().unwrap(); - - if window_state.ime_allowed() != allowed && window_state.set_ime_allowed(allowed) { + if let Some(allowed) = state_changed { let event = WindowEvent::Ime(if allowed { Ime::Enabled } else { Ime::Disabled }); self.window_events_sink.lock().unwrap().push_window_event(event, self.window_id); self.event_loop_awakener.ping(); } + + Ok(()) } #[inline] - fn set_ime_purpose(&self, purpose: ImePurpose) { - self.window_state.lock().unwrap().set_ime_purpose(purpose); + fn ime_capabilities(&self) -> Option { + self.window_state.lock().unwrap().ime_allowed() } fn focus_window(&self) {} diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 8d87d665..899b0e01 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -32,12 +32,15 @@ use wayland_protocols::xdg::toplevel_icon::v1::client::xdg_toplevel_icon_manager use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; use winit_core::cursor::{CursorIcon, CustomCursor as CoreCustomCursor}; use winit_core::error::{NotSupportedError, RequestError}; -use winit_core::window::{CursorGrabMode, ImePurpose, ResizeDirection, Theme, WindowId}; +use winit_core::window::{ + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId, +}; use crate::event_loop::OwnedDisplayHandle; use crate::logical_to_physical_rounded; use crate::seat::{ - PointerConstraintsState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, + PointerConstraintsState, TextInputClientState, WinitPointerData, WinitPointerDataExt, + ZwpTextInputV3Ext, }; use crate::state::{WindowCompositorUpdate, WinitState}; use crate::types::cursor::{CustomCursor, SelectedCursor, WaylandCustomCursor}; @@ -113,11 +116,11 @@ pub struct WindowState { /// The current cursor grabbing mode. cursor_grab_mode: GrabState, - /// Whether the IME input is allowed for that window. - ime_allowed: bool, - - /// The current IME purpose. - ime_purpose: ImePurpose, + /// The input method properties provided by the application to the IME. + /// + /// This state is cached here so that the window can automatically send the state to the IME as + /// soon as it becomes available without application involvement. + text_input_state: Option, /// The text inputs observed on the window. text_inputs: Vec, @@ -211,8 +214,7 @@ impl WindowState { frame_callback_state: FrameCallbackState::None, seat_focus: Default::default(), has_pending_move: None, - ime_allowed: false, - ime_purpose: ImePurpose::Normal, + text_input_state: None, last_configure: None, max_surface_size: None, min_surface_size: MIN_WINDOW_SIZE, @@ -544,8 +546,12 @@ impl WindowState { /// Whether the IME is allowed. #[inline] - pub fn ime_allowed(&self) -> bool { - self.ime_allowed + pub fn ime_allowed(&self) -> Option { + self.text_input_state.as_ref().map(|state| state.capabilities()) + } + + pub(crate) fn text_input_state(&self) -> Option<&TextInputClientState> { + self.text_input_state.as_ref() } /// Get the size of the window. @@ -983,53 +989,61 @@ impl WindowState { self.seat_focus.remove(seat); } - /// Returns `true` if the requested state was applied. - pub fn set_ime_allowed(&mut self, allowed: bool) -> bool { - self.ime_allowed = allowed; + /// Atomically update input method state. + /// + /// Returns `None` if an input method state haven't changed. Alternatively `Some(true)` and + /// `Some(false)` is returned respectfully. + pub fn request_ime_update( + &mut self, + request: ImeRequest, + ) -> Result, ImeRequestError> { + let state_change = match request { + ImeRequest::Enable(enable) => { + let (capabilities, request_data) = enable.into_raw(); - let mut applied = false; + if self.text_input_state.is_some() { + return Err(ImeRequestError::AlreadyEnabled); + } + + self.text_input_state = Some(TextInputClientState::new( + capabilities, + request_data, + self.scale_factor(), + )); + true + }, + ImeRequest::Update(request_data) => { + let scale_factor = self.scale_factor(); + if let Some(text_input_state) = self.text_input_state.as_mut() { + text_input_state.update(request_data, scale_factor); + } else { + return Err(ImeRequestError::NotEnabled); + } + false + }, + ImeRequest::Disable => { + self.text_input_state = None; + true + }, + }; + + // Only one input method may be active per (seat, surface), + // but there may be multiple seats focused on a surface, + // resulting in multiple text input objects. + // + // WARNING: this doesn't actually handle different seats with independent cursors. There's + // no API to set a per-seat input method state, so they all share a single state. for text_input in &self.text_inputs { - applied = true; - if allowed { - text_input.enable(); - text_input.set_content_type_by_purpose(self.ime_purpose); - } else { - text_input.disable(); - } - text_input.commit(); + text_input.set_state(self.text_input_state.as_ref(), state_change); } - applied - } - - /// Set the IME position. - pub fn set_ime_cursor_area(&self, position: LogicalPosition, size: LogicalSize) { - // FIXME: This won't fly unless user will have a way to request IME window per seat, since - // the ime windows will be overlapping, but winit doesn't expose API to specify for - // which seat we're setting IME position. - let (x, y) = (position.x as i32, position.y as i32); - let (width, height) = (size.width as i32, size.height as i32); - for text_input in self.text_inputs.iter() { - text_input.set_cursor_rectangle(x, y, width, height); - text_input.commit(); + if state_change { + Ok(Some(self.text_input_state.is_some())) + } else { + Ok(None) } } - /// Set the IME purpose. - pub fn set_ime_purpose(&mut self, purpose: ImePurpose) { - self.ime_purpose = purpose; - - for text_input in &self.text_inputs { - text_input.set_content_type_by_purpose(purpose); - text_input.commit(); - } - } - - /// Get the IME purpose. - pub fn ime_purpose(&self) -> ImePurpose { - self.ime_purpose - } - /// Set the scale factor for the given window. #[inline] pub fn set_scale_factor(&mut self, scale_factor: f64) { diff --git a/winit-web/src/window.rs b/winit-web/src/window.rs index b6e21215..a3579a33 100644 --- a/winit-web/src/window.rs +++ b/winit-web/src/window.rs @@ -12,8 +12,8 @@ use winit_core::error::{NotSupportedError, RequestError}; use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoremMonitorHandle}; use winit_core::window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as RootWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + CursorGrabMode, ImeRequestError, ResizeDirection, Theme, UserAttentionType, + Window as RootWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use crate::event_loop::ActiveEventLoop; @@ -308,16 +308,12 @@ impl RootWindow for Window { // Currently an intentional no-op } - fn set_ime_cursor_area(&self, _: Position, _: Size) { - // Currently not implemented + fn ime_capabilities(&self) -> Option { + None } - fn set_ime_allowed(&self, _: bool) { - // Currently not implemented - } - - fn set_ime_purpose(&self, _: ImePurpose) { - // Currently not implemented + fn request_ime_update(&self, _: winit_core::window::ImeRequest) -> Result<(), ImeRequestError> { + Err(ImeRequestError::NotSupported) } fn focus_window(&self) { diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index db0008f3..a14023f0 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -1416,7 +1416,7 @@ unsafe fn public_window_callback_inner( }, WM_IME_STARTCOMPOSITION => { - let ime_allowed = userdata.window_state_lock().ime_allowed; + let ime_allowed = userdata.window_state_lock().ime_capabilities.is_some(); if ime_allowed { userdata.window_state_lock().ime_state = ImeState::Enabled; @@ -1429,7 +1429,7 @@ unsafe fn public_window_callback_inner( WM_IME_COMPOSITION => { let ime_allowed_and_composing = { let w = userdata.window_state_lock(); - w.ime_allowed && w.ime_state != ImeState::Disabled + w.ime_capabilities.is_some() && w.ime_state != ImeState::Disabled }; // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so // check whether composing. @@ -1480,7 +1480,7 @@ unsafe fn public_window_callback_inner( WM_IME_ENDCOMPOSITION => { let ime_allowed_or_composing = { let w = userdata.window_state_lock(); - w.ime_allowed || w.ime_state != ImeState::Disabled + w.ime_capabilities.is_some() || w.ime_state != ImeState::Disabled }; if ime_allowed_or_composing { if userdata.window_state_lock().ime_state == ImeState::Preedit { diff --git a/winit-win32/src/ime.rs b/winit-win32/src/ime.rs index 534fe02c..6b379c4e 100644 --- a/winit-win32/src/ime.rs +++ b/winit-win32/src/ime.rs @@ -149,7 +149,7 @@ impl ImeContext { } } - unsafe fn system_has_ime() -> bool { + pub unsafe fn system_has_ime() -> bool { unsafe { GetSystemMetrics(SM_IMMENABLED) != 0 } } } diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index a02996b0..2286a319 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -51,8 +51,9 @@ use winit_core::error::RequestError; use winit_core::icon::{Icon, RgbaIcon}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, }; use crate::dark_mode::try_theme; @@ -1008,26 +1009,64 @@ impl CoreWindow for Window { } } - fn set_ime_cursor_area(&self, spot: Position, size: Size) { + fn ime_capabilities(&self) -> Option { + self.window_state.lock().unwrap().ime_capabilities + } + + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + // NOTE: this is racy way of doing this, but unless we remove the `Send` from the `Window` + // we can not do much about that. + let cap = self.window_state.lock().unwrap().ime_capabilities; + match &request { + ImeRequest::Enable(..) if cap.is_some() => return Err(ImeRequestError::AlreadyEnabled), + ImeRequest::Update(_) if cap.is_none() => return Err(ImeRequestError::NotEnabled), + _ => (), + } + let window = self.window; let state = self.window_state.clone(); self.thread_executor.execute_in_thread(move || unsafe { - let scale_factor = state.lock().unwrap().scale_factor; - ImeContext::current(window.hwnd()).set_ime_cursor_area(spot, size, scale_factor); + let hwnd = window.hwnd(); + let mut state = state.lock().unwrap(); + let (capabilities, request_data) = match &request { + ImeRequest::Enable(enable) => { + let capabilities = *enable.capabilities(); + state.ime_capabilities = Some(capabilities); + ImeContext::set_ime_allowed(hwnd, true); + (capabilities, enable.request_data()) + }, + ImeRequest::Update(request_data) => { + if let Some(capabilities) = state.ime_capabilities { + (capabilities, request_data) + } else { + warn!("ime update without IME enabled."); + return; + } + }, + ImeRequest::Disable => { + state.ime_capabilities = None; + ImeContext::set_ime_allowed(window.hwnd(), false); + return; + }, + }; + + if let Some((spot, size)) = request_data.cursor_area { + if capabilities.contains(ImeCapabilities::CURSOR_AREA) { + let scale_factor = state.scale_factor; + ImeContext::current(window.hwnd()).set_ime_cursor_area( + spot, + size, + scale_factor, + ); + } else { + warn!("discarding IME cursor area update without capability enabled."); + } + } }); - } - fn set_ime_allowed(&self, allowed: bool) { - let window = self.window; - let state = self.window_state.clone(); - self.thread_executor.execute_in_thread(move || unsafe { - state.lock().unwrap().ime_allowed = allowed; - ImeContext::set_ime_allowed(window.hwnd(), allowed); - }) + Ok(()) } - fn set_ime_purpose(&self, _purpose: ImePurpose) {} - fn request_user_attention(&self, request_type: Option) { let window = self.window; let active_window_handle = unsafe { GetActiveWindow() }; diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index 06135b05..08dfe84e 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -19,7 +19,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ use winit_core::icon::Icon; use winit_core::keyboard::ModifiersState; use winit_core::monitor::Fullscreen; -use winit_core::window::{Theme, WindowAttributes}; +use winit_core::window::{ImeCapabilities, Theme, WindowAttributes}; use crate::{event_loop, util, SelectedCursor}; @@ -48,7 +48,7 @@ pub(crate) struct WindowState { pub window_flags: WindowFlags, pub ime_state: ImeState, - pub ime_allowed: bool, + pub ime_capabilities: Option, // Used by WM_NCACTIVATE, WM_SETFOCUS and WM_KILLFOCUS pub is_active: bool, @@ -178,7 +178,7 @@ impl WindowState { window_flags: WindowFlags::empty(), ime_state: ImeState::Disabled, - ime_allowed: false, + ime_capabilities: None, is_active: false, is_focused: false, diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 5fd0ccd3..a9cc2191 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -20,8 +20,9 @@ use winit_core::monitor::{ Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider, VideoMode, }; use winit_core::window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + CursorGrabMode, ImeCapabilities, ImeRequest as CoreImeRequest, ImeRequestError, + ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, + WindowButtons, WindowId, WindowLevel, }; use x11rb::connection::{Connection, RequestConnection}; use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; @@ -210,16 +211,12 @@ impl CoreWindow for Window { self.0.set_window_icon(icon) } - fn set_ime_cursor_area(&self, position: Position, size: Size) { - self.0.set_ime_cursor_area(position, size); + fn request_ime_update(&self, action: CoreImeRequest) -> Result<(), ImeRequestError> { + self.0.request_ime_update(action) } - fn set_ime_allowed(&self, allowed: bool) { - self.0.set_ime_allowed(allowed); - } - - fn set_ime_purpose(&self, purpose: ImePurpose) { - self.0.set_ime_purpose(purpose); + fn ime_capabilities(&self) -> Option { + self.0.ime_capabilities() } fn focus_window(&self) { @@ -349,6 +346,7 @@ pub struct SharedState { pub inner_position_rel_parent: Option<(i32, i32)>, pub is_resizable: bool, pub is_decorated: bool, + pub ime_capabilities: Option, pub last_monitor: X11MonitorHandle, pub dpi_adjusted: Option<(u32, u32)>, pub(crate) fullscreen: Option, @@ -392,6 +390,7 @@ impl SharedState { size: None, position: None, inner_position: None, + ime_capabilities: None, inner_position_rel_parent: None, dpi_adjusted: None, fullscreen: None, @@ -2081,7 +2080,55 @@ impl UnownedWindow { } #[inline] - pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + pub fn request_ime_update(&self, request: CoreImeRequest) -> Result<(), ImeRequestError> { + let mut shared_state = self.shared_state_lock(); + let (capabilities, state) = match request { + CoreImeRequest::Enable(enable) => { + let (capabilities, request_data) = enable.into_raw(); + + if shared_state.ime_capabilities.is_some() { + return Err(ImeRequestError::AlreadyEnabled); + } + + shared_state.ime_capabilities = Some(capabilities); + drop(shared_state); + self.set_ime_allowed(true); + (capabilities, request_data) + }, + CoreImeRequest::Update(state) => { + if let Some(capabilities) = shared_state.ime_capabilities { + (capabilities, state) + } else { + // The IME was not yet enabled, so discard the update. + return Err(ImeRequestError::NotEnabled); + } + }, + CoreImeRequest::Disable => { + shared_state.ime_capabilities = None; + drop(shared_state); + self.set_ime_allowed(false); + return Ok(()); + }, + }; + + if let Some((position, size)) = state.cursor_area { + if capabilities.contains(ImeCapabilities::CURSOR_AREA) { + self.set_ime_cursor_area(position, size); + } else { + warn!("discarding IME cursor area update without capability enabled."); + } + } + + // Pretend that there is always some input method available. + // Better to make an application think it has an input method and send more events when it + // doesn't than think there is no input method and not send any IME events. + Ok(()) + } + + #[inline] + pub fn ime_capabilities(&self) -> Option { + self.shared_state_lock().ime_capabilities + } #[inline] pub fn focus_window(&self) { diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 84db1a3d..e255263d 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -82,6 +82,7 @@ changelog entry. - `ActivationToken::as_raw` to get a ref to raw token. - Each platform now has corresponding `WindowAttributes` struct instead of trait extension. - On Wayland, added implementation for `Window::set_window_icon` +- Add `Window::request_ime_update` to atomically apply set of IME changes. ### Changed @@ -203,6 +204,7 @@ changelog entry. - Move `EventLoopExtPumpEvents` and `PumpStatus` from platform module to `winit::event_loop::pump_events`. - Move `EventLoopExtRunOnDemand` from platform module to `winit::event_loop::run_on_demand`. - Use `NamedKey`, `Code` and `Location` from the `keyboard-types` v0.8 crate. +- Deprecate `Window::set_ime_allowed`, `Window::set_ime_cursor_area`, and `Window::set_ime_purpose`. ### Removed