diff --git a/CHANGELOG.md b/CHANGELOG.md index 56a82545c6..d72624b63d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On Wayland, support fractional scaling via the wp-fractional-scale protocol. - On web, fix removal of mouse event listeners from the global object upon window distruction. - Add WindowAttributes getter to WindowBuilder to allow introspection of default values. +- Added `Window::set_ime_purpose` for setting the IME purpose, currently implemented on Wayland only. # 0.27.5 diff --git a/examples/ime.rs b/examples/ime.rs index 2df780454c..59f43d4a33 100644 --- a/examples/ime.rs +++ b/examples/ime.rs @@ -6,7 +6,7 @@ use winit::{ dpi::PhysicalPosition, event::{ElementState, Event, Ime, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, + window::{ImePurpose, WindowBuilder}, }; fn main() { @@ -18,6 +18,7 @@ fn main() { 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"); + println!("Press F3 to cycle through IME purposes."); let event_loop = EventLoop::new(); @@ -26,6 +27,7 @@ fn main() { .build(&event_loop) .unwrap(); + let mut ime_purpose = ImePurpose::Normal; let mut ime_allowed = true; window.set_ime_allowed(ime_allowed); @@ -90,7 +92,18 @@ fn main() { { ime_allowed = !ime_allowed; window.set_ime_allowed(ime_allowed); - println!("\nIME: {ime_allowed}\n"); + println!("\nIME allowed: {ime_allowed}\n"); + } + if input.state == ElementState::Pressed + && input.virtual_keycode == Some(VirtualKeyCode::F3) + { + ime_purpose = match ime_purpose { + ImePurpose::Normal => ImePurpose::Password, + ImePurpose::Password => ImePurpose::Terminal, + _ => ImePurpose::Normal, + }; + window.set_ime_purpose(ime_purpose); + println!("\nIME purpose: {ime_purpose:?}\n"); } } _ => (), diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index f18eb58d82..2e1924e27b 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -25,7 +25,9 @@ use crate::{ error, event::{self, StartCause, VirtualKeyCode}, event_loop::{self, ControlFlow, EventLoopWindowTarget as RootELW}, - window::{self, CursorGrabMode, ResizeDirection, Theme, WindowButtons, WindowLevel}, + window::{ + self, CursorGrabMode, ImePurpose, ResizeDirection, Theme, WindowButtons, WindowLevel, + }, }; static HAS_FOCUS: Lazy> = Lazy::new(|| RwLock::new(true)); @@ -1006,6 +1008,8 @@ impl Window { pub fn set_ime_allowed(&self, _allowed: bool) {} + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + pub fn focus_window(&self) {} pub fn request_user_attention(&self, _request_type: Option) {} diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index edcd89711e..880bd843ff 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -27,8 +27,8 @@ use crate::{ monitor, EventLoopWindowTarget, Fullscreen, MonitorHandle, }, window::{ - CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowId as RootWindowId, WindowLevel, + CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowId as RootWindowId, WindowLevel, }, }; @@ -303,6 +303,10 @@ impl Inner { warn!("`Window::set_ime_allowed` is ignored on iOS") } + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + warn!("`Window::set_ime_allowed` is ignored on iOS") + } + pub fn focus_window(&self) { warn!("`Window::set_focus` is ignored on iOS") } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index ed0cc42172..0a90d085ee 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -34,8 +34,8 @@ use crate::{ }, icon::Icon, window::{ - CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowLevel, + CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, }, }; @@ -519,6 +519,11 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed)) } + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + x11_or_wayland!(match self; Window(w) => w.set_ime_purpose(purpose)) + } + #[inline] pub fn focus_window(&self) { match self { diff --git a/src/platform_impl/linux/wayland/seat/text_input/handlers.rs b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs index ed2540cb54..7f3b9fb60f 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs @@ -9,7 +9,7 @@ use crate::event::{Ime, WindowEvent}; use crate::platform_impl::wayland; use crate::platform_impl::wayland::event_loop::WinitState; -use super::{Preedit, TextInputHandler, TextInputInner}; +use super::{Preedit, TextInputHandler, TextInputInner, ZwpTextInputV3Ext}; #[inline] pub(super) fn handle_text_input( @@ -32,6 +32,7 @@ pub(super) fn handle_text_input( // Enable text input on that surface. if window_handle.ime_allowed.get() { text_input.enable(); + text_input.set_content_type_by_purpose(window_handle.ime_purpose.get()); text_input.commit(); event_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id); } diff --git a/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/src/platform_impl/linux/wayland/seat/text_input/mod.rs index 52ec94afaf..60219c7011 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/mod.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -1,10 +1,13 @@ use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::Attached; use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; -use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_v3::ZwpTextInputV3; +use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_v3::{ + ContentHint, ContentPurpose, ZwpTextInputV3, +}; use crate::platform_impl::wayland::event_loop::WinitState; use crate::platform_impl::wayland::WindowId; +use crate::window::ImePurpose; mod handlers; @@ -14,6 +17,21 @@ pub struct TextInputHandler { text_input: ZwpTextInputV3, } +trait ZwpTextInputV3Ext { + fn set_content_type_by_purpose(&self, purpose: ImePurpose); +} + +impl ZwpTextInputV3Ext for ZwpTextInputV3 { + fn set_content_type_by_purpose(&self, purpose: ImePurpose) { + let (hint, purpose) = match purpose { + ImePurpose::Normal => (ContentHint::None, ContentPurpose::Normal), + ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password), + ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal), + }; + self.set_content_type(hint, purpose); + } +} + impl TextInputHandler { #[inline] pub fn set_ime_position(&self, x: i32, y: i32) { @@ -22,8 +40,15 @@ impl TextInputHandler { } #[inline] - pub fn set_input_allowed(&self, allowed: bool) { - if allowed { + pub fn set_content_type_by_purpose(&self, purpose: ImePurpose) { + self.text_input.set_content_type_by_purpose(purpose); + self.text_input.commit(); + } + + #[inline] + pub fn set_input_allowed(&self, allowed: Option) { + if let Some(purpose) = allowed { + self.text_input.set_content_type_by_purpose(purpose); self.text_input.enable(); } else { self.text_input.disable(); diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index c66f32f1ce..a775b30795 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -21,8 +21,8 @@ use crate::platform_impl::{ OsError, PlatformSpecificWindowBuilderAttributes as PlatformAttributes, }; use crate::window::{ - CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, + CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, }; use super::env::WindowingFeatures; @@ -623,6 +623,11 @@ impl Window { self.send_request(WindowRequest::AllowIme(allowed)); } + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + self.send_request(WindowRequest::ImePurpose(purpose)); + } + #[inline] pub fn display(&self) -> &Display { &self.display diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index dd280bc944..1cc0a12459 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -23,7 +23,7 @@ use crate::platform_impl::wayland::protocols::wp_fractional_scale_v1::WpFraction use crate::platform_impl::wayland::seat::pointer::WinitPointer; use crate::platform_impl::wayland::seat::text_input::TextInputHandler; use crate::platform_impl::wayland::WindowId; -use crate::window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType}; +use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, Theme, UserAttentionType}; use super::WinitFrame; @@ -83,6 +83,9 @@ pub enum WindowRequest { /// Enable IME on the given window. AllowIme(bool), + /// Set the IME purpose. + ImePurpose(ImePurpose), + /// Mark the window as opaque. Transparent(bool), @@ -169,6 +172,9 @@ pub struct WindowHandle { /// Allow IME events for that window. pub ime_allowed: Cell, + /// IME purpose for that window. + pub ime_purpose: Cell, + /// Wether the window is transparent. pub transparent: Cell, @@ -226,6 +232,7 @@ impl WindowHandle { attention_requested: Cell::new(false), compositor, ime_allowed: Cell::new(false), + ime_purpose: Cell::new(ImePurpose::default()), has_focus, } } @@ -399,8 +406,9 @@ impl WindowHandle { self.ime_allowed.replace(allowed); let window_id = wayland::make_wid(self.window.surface()); + let purpose = allowed.then(|| self.ime_purpose.get()); for text_input in self.text_inputs.iter() { - text_input.set_input_allowed(allowed); + text_input.set_input_allowed(purpose); } let event = if allowed { @@ -412,6 +420,20 @@ impl WindowHandle { event_sink.push_window_event(event, window_id); } + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + if self.ime_purpose.get() == purpose { + return; + } + + self.ime_purpose.replace(purpose); + + if self.ime_allowed.get() { + for text_input in self.text_inputs.iter() { + text_input.set_content_type_by_purpose(purpose); + } + } + } + pub fn set_cursor_visible(&self, visible: bool) { self.cursor_visible.replace(visible); let cursor_icon = match visible { @@ -475,6 +497,9 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { let event_sink = &mut winit_state.event_sink; window_handle.set_ime_allowed(allow, event_sink); } + WindowRequest::ImePurpose(purpose) => { + window_handle.set_ime_purpose(purpose); + } WindowRequest::SetCursorGrabMode(mode) => { window_handle.set_cursor_grab(mode); } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index fd6d645e3d..a600eca069 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -21,7 +21,7 @@ use crate::{ PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, window::{ - CursorGrabMode, CursorIcon, Icon, ResizeDirection, Theme, UserAttentionType, + CursorGrabMode, CursorIcon, Icon, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, WindowButtons, WindowLevel, }, }; @@ -1532,6 +1532,9 @@ impl UnownedWindow { .send(ImeRequest::Allow(self.xwindow, allowed)); } + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + #[inline] pub fn focus_window(&self) { let state_atom = unsafe { self.xconn.get_atom_unchecked(b"WM_STATE\0") }; diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 36b24e21cb..68f1250816 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -33,8 +33,8 @@ use crate::{ Fullscreen, OsError, }, window::{ - CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowId as RootWindowId, WindowLevel, + CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowId as RootWindowId, WindowLevel, }, }; use core_graphics::display::{CGDisplay, CGPoint}; @@ -1154,6 +1154,9 @@ impl WinitWindow { unsafe { Id::from_shared(self.view()) }.set_ime_allowed(allowed); } + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + #[inline] pub fn focus_window(&self) { let is_minimized = self.isMiniaturized(); diff --git a/src/platform_impl/orbital/window.rs b/src/platform_impl/orbital/window.rs index da7d44be97..fa30bb10d4 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -12,6 +12,7 @@ use crate::{ error, platform_impl::Fullscreen, window, + window::ImePurpose, }; use super::{ @@ -323,6 +324,9 @@ impl Window { #[inline] pub fn set_ime_allowed(&self, _allowed: bool) {} + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + #[inline] pub fn focus_window(&self) {} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 15d82ac088..e9fc26b7c0 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -3,8 +3,8 @@ use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; use crate::event; use crate::icon::Icon; use crate::window::{ - CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowId as RootWI, WindowLevel, + CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowId as RootWI, WindowLevel, }; use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle}; @@ -351,6 +351,11 @@ impl Window { // Currently not implemented } + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + // Currently not implemented + } + #[inline] pub fn focus_window(&self) { // Currently a no-op as it does not seem there is good support for this on web diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 3c356b7c70..2284cce43f 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -71,8 +71,8 @@ use crate::{ Fullscreen, PlatformSpecificWindowBuilderAttributes, WindowId, }, window::{ - CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowLevel, + CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, }, }; @@ -733,6 +733,9 @@ impl Window { } } + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + #[inline] pub fn request_user_attention(&self, request_type: Option) { let window = self.window.clone(); diff --git a/src/window.rs b/src/window.rs index 0c3e90278f..1a6a9be3f5 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1054,6 +1054,16 @@ impl Window { self.window.set_ime_allowed(allowed); } + /// Sets the IME purpose for the window using [`ImePurpose`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + self.window.set_ime_purpose(purpose); + } + /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// @@ -1545,3 +1555,30 @@ impl Default for WindowLevel { Self::Normal } } + +/// Generic IME purposes for use in [`Window::set_ime_purpose`]. +/// +/// The purpose may improve UX by optimizing the IME for the specific use case, +/// if winit can express the purpose to the platform and the platform reacts accordingly. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum ImePurpose { + /// No special hints for the IME (default). + Normal, + /// The IME is used for password input. + Password, + /// The IME is used to input into a terminal. + /// + /// For example, that could alter OSK on Wayland to show extra buttons. + Terminal, +} + +impl Default for ImePurpose { + fn default() -> Self { + Self::Normal + } +}