winit-core/ime: add more purposes and content hints
Also move IME example into `ime.rs` thus making IME integrations more easily comprehendible. Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
1
examples
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
winit/examples
|
||||||
|
|
@ -1136,9 +1136,9 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
|
||||||
let action = if allowed {
|
let action = if allowed {
|
||||||
let position = LogicalPosition::new(0, 0);
|
let position = LogicalPosition::new(0, 0);
|
||||||
let size = LogicalSize::new(0, 0);
|
let size = LogicalSize::new(0, 0);
|
||||||
let ime_caps = ImeCapabilities::new().with_purpose().with_cursor_area();
|
let ime_caps = ImeCapabilities::new().without_hint_and_purpose().with_cursor_area();
|
||||||
let request_data = ImeRequestData {
|
let request_data = ImeRequestData {
|
||||||
purpose: Some(ImePurpose::Normal),
|
hint_and_purpose: Some((ImeHint::NONE, ImePurpose::Normal)),
|
||||||
// WARNING: there's nothing sensible to use here by default.
|
// WARNING: there's nothing sensible to use here by default.
|
||||||
cursor_area: Some((position.into(), size.into())),
|
cursor_area: Some((position.into(), size.into())),
|
||||||
..ImeRequestData::default()
|
..ImeRequestData::default()
|
||||||
|
|
@ -1160,9 +1160,9 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
|
||||||
/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported.
|
/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported.
|
||||||
#[deprecated = "use Window::request_ime_update instead"]
|
#[deprecated = "use Window::request_ime_update instead"]
|
||||||
fn set_ime_purpose(&self, purpose: ImePurpose) {
|
fn set_ime_purpose(&self, purpose: ImePurpose) {
|
||||||
if self.ime_capabilities().map(|caps| caps.purpose()).unwrap_or(false) {
|
if self.ime_capabilities().map(|caps| caps.hint_and_purpose()).unwrap_or(false) {
|
||||||
let _ = self.request_ime_update(ImeRequest::Update(ImeRequestData {
|
let _ = self.request_ime_update(ImeRequest::Update(ImeRequestData {
|
||||||
purpose: Some(purpose),
|
hint_and_purpose: Some((ImeHint::NONE, purpose)),
|
||||||
..ImeRequestData::default()
|
..ImeRequestData::default()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -1186,14 +1186,14 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// # use dpi::{Position, Size};
|
/// # use dpi::{Position, Size};
|
||||||
/// # use winit_core::window::{Window, ImePurpose, ImeRequest, ImeCapabilities, ImeRequestData, ImeEnableRequest};
|
/// # use winit_core::window::{Window, ImeHint, ImePurpose, ImeRequest, ImeCapabilities, ImeRequestData, ImeEnableRequest};
|
||||||
/// # fn scope(window: &dyn Window, cursor_pos: Position, cursor_size: Size) {
|
/// # fn scope(window: &dyn Window, cursor_pos: Position, cursor_size: Size) {
|
||||||
/// // Clear previous state by switching off IME
|
/// // Clear previous state by switching off IME
|
||||||
/// window.request_ime_update(ImeRequest::Disable).expect("Disable cannot fail");
|
/// window.request_ime_update(ImeRequest::Disable).expect("Disable cannot fail");
|
||||||
///
|
///
|
||||||
/// let ime_caps = ImeCapabilities::new().with_cursor_area().with_purpose();
|
/// let ime_caps = ImeCapabilities::new().with_cursor_area().with_hint_and_purpose();
|
||||||
/// let request_data = ImeRequestData::default()
|
/// let request_data = ImeRequestData::default()
|
||||||
/// .with_purpose(ImePurpose::Normal)
|
/// .with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||||
/// .with_cursor_area(cursor_pos, cursor_size);
|
/// .with_cursor_area(cursor_pos, cursor_size);
|
||||||
/// let enable_ime = ImeEnableRequest::new(ime_caps, request_data.clone()).unwrap();
|
/// 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");
|
/// window.request_ime_update(ImeRequest::Enable(enable_ime)).expect("Enabling may fail if IME is not supported");
|
||||||
|
|
@ -1598,7 +1598,10 @@ pub enum WindowLevel {
|
||||||
|
|
||||||
/// Generic IME purposes for use in [`Window::set_ime_purpose`].
|
/// Generic IME purposes for use in [`Window::set_ime_purpose`].
|
||||||
///
|
///
|
||||||
|
/// The purpose should reflect the kind of data to be entered.
|
||||||
/// The purpose may improve UX by optimizing the IME for the specific use case,
|
/// The purpose may improve UX by optimizing the IME for the specific use case,
|
||||||
|
/// for example showing relevant characters and hiding unneeded ones,
|
||||||
|
/// or changing the icon of the confrirmation button,
|
||||||
/// if winit can express the purpose to the platform and the platform reacts accordingly.
|
/// if winit can express the purpose to the platform and the platform reacts accordingly.
|
||||||
///
|
///
|
||||||
/// ## Platform-specific
|
/// ## Platform-specific
|
||||||
|
|
@ -1608,14 +1611,31 @@ pub enum WindowLevel {
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum ImePurpose {
|
pub enum ImePurpose {
|
||||||
/// No special hints for the IME (default).
|
/// No special purpose for the IME (default).
|
||||||
Normal,
|
Normal,
|
||||||
/// The IME is used for password input.
|
/// The IME is used for password input.
|
||||||
|
/// The IME will treat the contents as sensitive.
|
||||||
Password,
|
Password,
|
||||||
/// The IME is used to input into a terminal.
|
/// The IME is used to input into a terminal.
|
||||||
///
|
///
|
||||||
/// For example, that could alter OSK on Wayland to show extra buttons.
|
/// For example, that could alter OSK on Wayland to show extra buttons.
|
||||||
Terminal,
|
Terminal,
|
||||||
|
/// Number (including decimal separator and sign)
|
||||||
|
Number,
|
||||||
|
/// Phone number
|
||||||
|
Phone,
|
||||||
|
/// URL
|
||||||
|
Url,
|
||||||
|
/// Email address
|
||||||
|
Email,
|
||||||
|
/// Password composed only of digits (treated as sensitive data)
|
||||||
|
Pin,
|
||||||
|
/// Date
|
||||||
|
Date,
|
||||||
|
/// Time
|
||||||
|
Time,
|
||||||
|
/// Date and time
|
||||||
|
DateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ImePurpose {
|
impl Default for ImePurpose {
|
||||||
|
|
@ -1624,6 +1644,48 @@ impl Default for ImePurpose {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// IME hints
|
||||||
|
///
|
||||||
|
/// The hint should reflect the desired behaviour of the IME
|
||||||
|
/// while entering text.
|
||||||
|
/// The purpose may improve UX by optimizing the IME for the specific use case,
|
||||||
|
/// beyond just the general data type specified in `ImePurpose`.
|
||||||
|
///
|
||||||
|
/// ## Platform-specific
|
||||||
|
///
|
||||||
|
/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct ImeHint: u32 {
|
||||||
|
/// No special behaviour.
|
||||||
|
const NONE = 0;
|
||||||
|
/// Suggest word completions.
|
||||||
|
const COMPLETION = 0x1;
|
||||||
|
/// Suggest word corrections.
|
||||||
|
const SPELLCHECK = 0x2;
|
||||||
|
/// Switch to uppercase letters at the start of a sentence.
|
||||||
|
const AUTO_CAPITALIZATION = 0x4;
|
||||||
|
/// Prefer lowercase letters.
|
||||||
|
const LOWERCASE = 0x8;
|
||||||
|
/// Prefer uppercase letters.
|
||||||
|
const UPPERCASE = 0x10;
|
||||||
|
/// Prefer casing for titles and headings (can be language dependent).
|
||||||
|
const TITLECASE = 0x20;
|
||||||
|
/// Characters should be hidden.
|
||||||
|
///
|
||||||
|
/// This may prevent e.g. layout switching with some IMEs, unless hint is disabled.
|
||||||
|
const HIDDEN_TEXT = 0x40;
|
||||||
|
/// Typed text should not be stored.
|
||||||
|
const SENSITIVE_DATA = 0x80;
|
||||||
|
/// Just Latin characters should be entered.
|
||||||
|
const LATIN = 0x100;
|
||||||
|
/// The text input is multiline.
|
||||||
|
const MULTILINE = 0x200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||||
pub enum ImeSurroundingTextError {
|
pub enum ImeSurroundingTextError {
|
||||||
/// Text exceeds 4000 bytes
|
/// Text exceeds 4000 bytes
|
||||||
|
|
@ -1759,7 +1821,7 @@ impl ImeEnableRequest {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if capabilities.purpose() ^ request_data.purpose.is_some() {
|
if capabilities.hint_and_purpose() ^ request_data.hint_and_purpose.is_some() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1804,23 +1866,23 @@ impl ImeCapabilities {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks `purpose` as supported.
|
/// Marks `hint and purpose` as supported.
|
||||||
///
|
///
|
||||||
/// For more details see [`ImeRequestData::with_purpose`].
|
/// For more details see [`ImeRequestData::with_hint_and_purpose`].
|
||||||
pub const fn with_purpose(self) -> Self {
|
pub const fn with_hint_and_purpose(self) -> Self {
|
||||||
Self(self.0.union(ImeCapabilitiesFlags::PURPOSE))
|
Self(self.0.union(ImeCapabilitiesFlags::HINT_AND_PURPOSE))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks `purpose` as unsupported.
|
/// Marks `hint and purpose` as unsupported.
|
||||||
///
|
///
|
||||||
/// For more details see [`ImeRequestData::with_purpose`].
|
/// For more details see [`ImeRequestData::with_hint_and_purpose`].
|
||||||
pub const fn without_purpose(self) -> Self {
|
pub const fn without_hint_and_purpose(self) -> Self {
|
||||||
Self(self.0.difference(ImeCapabilitiesFlags::PURPOSE))
|
Self(self.0.difference(ImeCapabilitiesFlags::HINT_AND_PURPOSE))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if `purpose` is supported.
|
/// Returns `true` if `hint and purpose` is supported.
|
||||||
pub const fn purpose(&self) -> bool {
|
pub const fn hint_and_purpose(&self) -> bool {
|
||||||
self.0.contains(ImeCapabilitiesFlags::PURPOSE)
|
self.0.contains(ImeCapabilitiesFlags::HINT_AND_PURPOSE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks `cursor_area` as supported.
|
/// Marks `cursor_area` as supported.
|
||||||
|
|
@ -1865,8 +1927,8 @@ impl ImeCapabilities {
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
pub(crate) struct ImeCapabilitiesFlags : u8 {
|
pub(crate) struct ImeCapabilitiesFlags : u8 {
|
||||||
/// Client supports setting IME purpose.
|
/// Client supports setting IME hint and purpose.
|
||||||
const PURPOSE = 1 << 0;
|
const HINT_AND_PURPOSE = 1 << 0;
|
||||||
/// Client supports reporting cursor area for IME popup to
|
/// Client supports reporting cursor area for IME popup to
|
||||||
/// appear.
|
/// appear.
|
||||||
const CURSOR_AREA = 1 << 1;
|
const CURSOR_AREA = 1 << 1;
|
||||||
|
|
@ -1884,10 +1946,10 @@ bitflags! {
|
||||||
#[derive(Debug, PartialEq, Clone, Default)]
|
#[derive(Debug, PartialEq, Clone, Default)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct ImeRequestData {
|
pub struct ImeRequestData {
|
||||||
/// Text input purpose.
|
/// Text input hint and purpose.
|
||||||
///
|
///
|
||||||
/// To support updating it, enable [`ImeCapabilities::PURPOSE`].
|
/// To support updating it, enable [`ImeCapabilities::HINT_AND_PURPOSE`].
|
||||||
pub purpose: Option<ImePurpose>,
|
pub hint_and_purpose: Option<(ImeHint, ImePurpose)>,
|
||||||
/// The IME cursor area which should not be covered by the input method popup.
|
/// The IME cursor area which should not be covered by the input method popup.
|
||||||
///
|
///
|
||||||
/// To support updating it, enable [`ImeCapabilities::CURSOR_AREA`].
|
/// To support updating it, enable [`ImeCapabilities::CURSOR_AREA`].
|
||||||
|
|
@ -1899,9 +1961,9 @@ pub struct ImeRequestData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImeRequestData {
|
impl ImeRequestData {
|
||||||
/// Sets the purpose hint of the current text input.
|
/// Sets the hint and purpose of the current text input content.
|
||||||
pub fn with_purpose(self, purpose: ImePurpose) -> Self {
|
pub fn with_hint_and_purpose(self, hint: ImeHint, purpose: ImePurpose) -> Self {
|
||||||
Self { purpose: Some(purpose), ..self }
|
Self { hint_and_purpose: Some((hint, purpose)), ..self }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the IME cursor editing area.
|
/// Sets the IME cursor editing area.
|
||||||
|
|
@ -2027,7 +2089,7 @@ mod tests {
|
||||||
ImeCapabilities, ImeEnableRequest, ImeRequestData, ImeSurroundingText,
|
ImeCapabilities, ImeEnableRequest, ImeRequestData, ImeSurroundingText,
|
||||||
ImeSurroundingTextError,
|
ImeSurroundingTextError,
|
||||||
};
|
};
|
||||||
use crate::window::ImePurpose;
|
use crate::window::{ImeHint, ImePurpose};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ime_initial_request_caps_match() {
|
fn ime_initial_request_caps_match() {
|
||||||
|
|
@ -2040,21 +2102,21 @@ mod tests {
|
||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
assert!(ImeEnableRequest::new(
|
assert!(ImeEnableRequest::new(
|
||||||
ImeCapabilities::new().with_purpose(),
|
ImeCapabilities::new().with_hint_and_purpose(),
|
||||||
ImeRequestData::default()
|
ImeRequestData::default()
|
||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
|
|
||||||
assert!(ImeEnableRequest::new(
|
assert!(ImeEnableRequest::new(
|
||||||
ImeCapabilities::new().with_cursor_area(),
|
ImeCapabilities::new().with_cursor_area(),
|
||||||
ImeRequestData::default().with_purpose(ImePurpose::Normal)
|
ImeRequestData::default().with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
|
|
||||||
assert!(ImeEnableRequest::new(
|
assert!(ImeEnableRequest::new(
|
||||||
ImeCapabilities::new(),
|
ImeCapabilities::new(),
|
||||||
ImeRequestData::default()
|
ImeRequestData::default()
|
||||||
.with_purpose(ImePurpose::Normal)
|
.with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||||
.with_cursor_area(position, size)
|
.with_cursor_area(position, size)
|
||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
|
|
@ -2062,7 +2124,7 @@ mod tests {
|
||||||
assert!(ImeEnableRequest::new(
|
assert!(ImeEnableRequest::new(
|
||||||
ImeCapabilities::new().with_cursor_area(),
|
ImeCapabilities::new().with_cursor_area(),
|
||||||
ImeRequestData::default()
|
ImeRequestData::default()
|
||||||
.with_purpose(ImePurpose::Normal)
|
.with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||||
.with_cursor_area(position, size)
|
.with_cursor_area(position, size)
|
||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
|
|
@ -2074,9 +2136,9 @@ mod tests {
|
||||||
.is_some());
|
.is_some());
|
||||||
|
|
||||||
assert!(ImeEnableRequest::new(
|
assert!(ImeEnableRequest::new(
|
||||||
ImeCapabilities::new().with_purpose().with_cursor_area(),
|
ImeCapabilities::new().with_hint_and_purpose().with_cursor_area(),
|
||||||
ImeRequestData::default()
|
ImeRequestData::default()
|
||||||
.with_purpose(ImePurpose::Normal)
|
.with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||||
.with_cursor_area(position, size)
|
.with_cursor_area(position, size)
|
||||||
)
|
)
|
||||||
.is_some());
|
.is_some());
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::
|
||||||
};
|
};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use winit_core::event::{Ime, WindowEvent};
|
use winit_core::event::{Ime, WindowEvent};
|
||||||
use winit_core::window::{ImeCapabilities, ImePurpose, ImeRequestData, ImeSurroundingText};
|
use winit_core::window::{
|
||||||
|
ImeCapabilities, ImeHint, ImePurpose, ImeRequestData, ImeSurroundingText,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::state::WinitState;
|
use crate::state::WinitState;
|
||||||
|
|
||||||
|
|
@ -278,9 +280,7 @@ struct DeleteSurroundingText {
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct ClientState {
|
pub struct ClientState {
|
||||||
capabilities: ImeCapabilities,
|
capabilities: ImeCapabilities,
|
||||||
|
|
||||||
content_type: ContentType,
|
content_type: ContentType,
|
||||||
|
|
||||||
/// The IME cursor area which should not be covered by the input method popup.
|
/// The IME cursor area which should not be covered by the input method popup.
|
||||||
cursor_area: (LogicalPosition<u32>, LogicalSize<u32>),
|
cursor_area: (LogicalPosition<u32>, LogicalSize<u32>),
|
||||||
|
|
||||||
|
|
@ -302,8 +302,10 @@ impl ClientState {
|
||||||
surrounding_text: ImeSurroundingText::new(String::new(), 0, 0).unwrap(),
|
surrounding_text: ImeSurroundingText::new(String::new(), 0, 0).unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let unsupported_flags =
|
let unsupported_flags = capabilities
|
||||||
capabilities.without_purpose().without_cursor_area().without_surrounding_text();
|
.without_hint_and_purpose()
|
||||||
|
.without_cursor_area()
|
||||||
|
.without_surrounding_text();
|
||||||
|
|
||||||
if unsupported_flags != ImeCapabilities::new() {
|
if unsupported_flags != ImeCapabilities::new() {
|
||||||
warn!(
|
warn!(
|
||||||
|
|
@ -322,12 +324,10 @@ impl ClientState {
|
||||||
|
|
||||||
/// Updates the fields of the state which are present in update_fields.
|
/// Updates the fields of the state which are present in update_fields.
|
||||||
pub fn update(&mut self, request_data: ImeRequestData, scale_factor: f64) {
|
pub fn update(&mut self, request_data: ImeRequestData, scale_factor: f64) {
|
||||||
if let Some(purpose) = request_data.purpose {
|
if let Some((hint, purpose)) =
|
||||||
if self.capabilities.purpose() {
|
request_data.hint_and_purpose.filter(|_| self.capabilities.hint_and_purpose())
|
||||||
self.content_type = purpose.into();
|
{
|
||||||
} else {
|
self.content_type = (hint, purpose).into();
|
||||||
warn!("discarding ImePurpose update without capability enabled.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((position, size)) = request_data.cursor_area {
|
if let Some((position, size)) = request_data.cursor_area {
|
||||||
|
|
@ -350,7 +350,7 @@ impl ClientState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content_type(&self) -> Option<ContentType> {
|
pub fn content_type(&self) -> Option<ContentType> {
|
||||||
self.capabilities.purpose().then_some(self.content_type)
|
self.capabilities.hint_and_purpose().then_some(self.content_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_area(&self) -> Option<(LogicalPosition<u32>, LogicalSize<u32>)> {
|
pub fn cursor_area(&self) -> Option<(LogicalPosition<u32>, LogicalSize<u32>)> {
|
||||||
|
|
@ -365,20 +365,69 @@ impl ClientState {
|
||||||
/// Arguments to content_type
|
/// Arguments to content_type
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ContentType {
|
pub struct ContentType {
|
||||||
/// Text input purpose
|
/// Text input hint.
|
||||||
purpose: ContentPurpose,
|
|
||||||
hint: ContentHint,
|
hint: ContentHint,
|
||||||
|
/// Text input purpose.
|
||||||
|
purpose: ContentPurpose,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ImePurpose> for ContentType {
|
/// The two options influence each other, so they must be converted together.
|
||||||
fn from(purpose: ImePurpose) -> Self {
|
impl From<(ImeHint, ImePurpose)> for ContentType {
|
||||||
let (hint, purpose) = match purpose {
|
fn from((hint, purpose): (ImeHint, ImePurpose)) -> Self {
|
||||||
ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password),
|
let purpose = match purpose {
|
||||||
ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal),
|
ImePurpose::Password => ContentPurpose::Password,
|
||||||
_ => return Default::default(),
|
ImePurpose::Terminal => ContentPurpose::Terminal,
|
||||||
|
ImePurpose::Phone => ContentPurpose::Phone,
|
||||||
|
ImePurpose::Number => ContentPurpose::Number,
|
||||||
|
ImePurpose::Url => ContentPurpose::Url,
|
||||||
|
ImePurpose::Email => ContentPurpose::Email,
|
||||||
|
ImePurpose::Pin => ContentPurpose::Pin,
|
||||||
|
ImePurpose::Date => ContentPurpose::Date,
|
||||||
|
ImePurpose::Time => ContentPurpose::Time,
|
||||||
|
ImePurpose::DateTime => ContentPurpose::Datetime,
|
||||||
|
_ => ContentPurpose::Normal,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self { hint, purpose }
|
let base_hint = match purpose {
|
||||||
|
// Before the hint API was introduced, password purpose guaranteed the
|
||||||
|
// sensitive hint. Keep this behaviour for the sake of backwards compatibility.
|
||||||
|
ContentPurpose::Password | ContentPurpose::Pin => ContentHint::SensitiveData,
|
||||||
|
_ => ContentHint::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_hint = base_hint;
|
||||||
|
if hint.contains(ImeHint::COMPLETION) {
|
||||||
|
new_hint |= ContentHint::Completion;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::SPELLCHECK) {
|
||||||
|
new_hint |= ContentHint::Spellcheck;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::AUTO_CAPITALIZATION) {
|
||||||
|
new_hint |= ContentHint::AutoCapitalization;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::LOWERCASE) {
|
||||||
|
new_hint |= ContentHint::Lowercase;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::UPPERCASE) {
|
||||||
|
new_hint |= ContentHint::Uppercase;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::TITLECASE) {
|
||||||
|
new_hint |= ContentHint::Titlecase;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::HIDDEN_TEXT) {
|
||||||
|
new_hint |= ContentHint::HiddenText;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::SENSITIVE_DATA) {
|
||||||
|
new_hint |= ContentHint::SensitiveData;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::LATIN) {
|
||||||
|
new_hint |= ContentHint::Latin;
|
||||||
|
}
|
||||||
|
if hint.contains(ImeHint::MULTILINE) {
|
||||||
|
new_hint |= ContentHint::Multiline;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { hint: new_hint, purpose }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../examples
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
//! Simple winit application.
|
//! Simple winit application.
|
||||||
|
//!
|
||||||
|
//! Note that a real application accepting text input **should** support
|
||||||
|
//! the IME interface. See the `ime` example.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
@ -9,22 +12,21 @@ use std::sync::mpsc::{self, Receiver, Sender};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(all(not(android_platform), not(web_platform)))]
|
#[cfg(all(not(android_platform), not(web_platform)))]
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::{cmp, fmt, mem};
|
use std::{fmt, mem};
|
||||||
|
|
||||||
use ::tracing::{error, info};
|
|
||||||
use cursor_icon::CursorIcon;
|
use cursor_icon::CursorIcon;
|
||||||
use dpi::LogicalPosition;
|
|
||||||
#[cfg(not(android_platform))]
|
#[cfg(not(android_platform))]
|
||||||
use rwh_06::{DisplayHandle, HasDisplayHandle};
|
use rwh_06::{DisplayHandle, HasDisplayHandle};
|
||||||
#[cfg(not(android_platform))]
|
#[cfg(not(android_platform))]
|
||||||
use softbuffer::{Context, Surface};
|
use softbuffer::{Context, Surface};
|
||||||
|
use tracing::{error, info};
|
||||||
#[cfg(all(web_platform, not(android_platform)))]
|
#[cfg(all(web_platform, not(android_platform)))]
|
||||||
use web_time::Instant;
|
use web_time::Instant;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::cursor::{Cursor, CustomCursor, CustomCursorSource};
|
use winit::cursor::{Cursor, CustomCursor, CustomCursorSource};
|
||||||
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
|
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
|
||||||
use winit::error::RequestError;
|
use winit::error::RequestError;
|
||||||
use winit::event::{DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, WindowEvent};
|
use winit::event::{DeviceEvent, DeviceId, MouseButton, MouseScrollDelta, WindowEvent};
|
||||||
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
||||||
use winit::icon::{Icon, RgbaIcon};
|
use winit::icon::{Icon, RgbaIcon};
|
||||||
use winit::keyboard::{Key, ModifiersState};
|
use winit::keyboard::{Key, ModifiersState};
|
||||||
|
|
@ -39,28 +41,23 @@ use winit::platform::wayland::{ActiveEventLoopExtWayland, WindowAttributesWaylan
|
||||||
use winit::platform::web::{ActiveEventLoopExtWeb, WindowAttributesWeb};
|
use winit::platform::web::{ActiveEventLoopExtWeb, WindowAttributesWeb};
|
||||||
#[cfg(x11_platform)]
|
#[cfg(x11_platform)]
|
||||||
use winit::platform::x11::{ActiveEventLoopExtX11, WindowAttributesX11};
|
use winit::platform::x11::{ActiveEventLoopExtX11, WindowAttributesX11};
|
||||||
use winit::window::{
|
use winit::window::{CursorGrabMode, ResizeDirection, Theme, Window, WindowAttributes, WindowId};
|
||||||
CursorGrabMode, ImeCapabilities, ImeEnableRequest, ImePurpose, ImeRequestData,
|
|
||||||
ImeSurroundingText, ResizeDirection, Theme, Window, WindowAttributes, WindowId,
|
|
||||||
};
|
|
||||||
use winit_core::application::macos::ApplicationHandlerExtMacOS;
|
use winit_core::application::macos::ApplicationHandlerExtMacOS;
|
||||||
use winit_core::window::ImeRequest;
|
|
||||||
|
|
||||||
#[path = "util/tracing.rs"]
|
#[path = "util/tracing.rs"]
|
||||||
mod tracing;
|
mod tracing_init;
|
||||||
|
|
||||||
#[path = "util/fill.rs"]
|
#[path = "util/fill.rs"]
|
||||||
mod fill;
|
mod fill;
|
||||||
|
|
||||||
/// The amount of points to around the window for drag resize direction calculations.
|
/// The amount of points to around the window for drag resize direction calculations.
|
||||||
const BORDER_SIZE: f64 = 20.;
|
const BORDER_SIZE: f64 = 20.;
|
||||||
const IME_CURSOR_SIZE: PhysicalSize<u32> = PhysicalSize::new(20, 20);
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
#[cfg(web_platform)]
|
#[cfg(web_platform)]
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
tracing::init();
|
tracing_init::init();
|
||||||
|
|
||||||
let event_loop = EventLoop::new()?;
|
let event_loop = EventLoop::new()?;
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
|
|
@ -245,7 +242,6 @@ impl Application {
|
||||||
window.window.set_simple_fullscreen(!window.window.simple_fullscreen());
|
window.window.set_simple_fullscreen(!window.window.simple_fullscreen());
|
||||||
},
|
},
|
||||||
Action::ToggleMaximize => window.toggle_maximize(),
|
Action::ToggleMaximize => window.toggle_maximize(),
|
||||||
Action::ToggleImeInput => window.toggle_ime(),
|
|
||||||
Action::Minimize => window.minimize(),
|
Action::Minimize => window.minimize(),
|
||||||
Action::NextCursor => window.next_cursor(),
|
Action::NextCursor => window.next_cursor(),
|
||||||
Action::NextCustomCursor => {
|
Action::NextCustomCursor => {
|
||||||
|
|
@ -512,41 +508,6 @@ impl ApplicationHandler for Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
WindowEvent::Ime(event) => match event {
|
|
||||||
Ime::Enabled => info!("IME enabled for Window={window_id:?}"),
|
|
||||||
Ime::Preedit(text, caret_pos) => {
|
|
||||||
info!("Preedit: {}, with caret at {:?}", text, caret_pos);
|
|
||||||
},
|
|
||||||
Ime::Commit(text) => {
|
|
||||||
window.text_field_contents.0.push_str(&text);
|
|
||||||
window.text_field_contents.1 += text.len();
|
|
||||||
|
|
||||||
info!("Committed: {}", text);
|
|
||||||
let request_data = window.get_ime_update();
|
|
||||||
window.window.request_ime_update(ImeRequest::Update(request_data)).unwrap();
|
|
||||||
},
|
|
||||||
Ime::DeleteSurrounding { before_bytes, after_bytes } => {
|
|
||||||
let (text, cursor) = &window.text_field_contents;
|
|
||||||
|
|
||||||
// To anyone copying this, keep in mind that this doesn't take text selection
|
|
||||||
// into account. The deletion happens *around* the pre-edit,
|
|
||||||
// and may remove the whole selection or a part of it.
|
|
||||||
let delete_start = cursor.saturating_sub(before_bytes);
|
|
||||||
let delete_end = cmp::min(cursor.saturating_add(after_bytes), text.len());
|
|
||||||
if text.is_char_boundary(delete_start) && text.is_char_boundary(delete_end) {
|
|
||||||
let new_text = {
|
|
||||||
let mut t = String::from(&text[..delete_start]);
|
|
||||||
t.push_str(&text[delete_end..]);
|
|
||||||
t
|
|
||||||
};
|
|
||||||
window.text_field_contents = (new_text, delete_start);
|
|
||||||
info!("IME deleted bytes: {before_bytes}, {after_bytes}");
|
|
||||||
} else {
|
|
||||||
error!("Buggy IME tried to delete with indices not on char boundary.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ime::Disabled => info!("IME disabled for Window={window_id:?}"),
|
|
||||||
},
|
|
||||||
WindowEvent::PinchGesture { delta, .. } => {
|
WindowEvent::PinchGesture { delta, .. } => {
|
||||||
window.zoom += delta;
|
window.zoom += delta;
|
||||||
let zoom = window.zoom;
|
let zoom = window.zoom;
|
||||||
|
|
@ -581,6 +542,7 @@ impl ApplicationHandler for Application {
|
||||||
| WindowEvent::DragMoved { .. }
|
| WindowEvent::DragMoved { .. }
|
||||||
| WindowEvent::DragDropped { .. }
|
| WindowEvent::DragDropped { .. }
|
||||||
| WindowEvent::Destroyed
|
| WindowEvent::Destroyed
|
||||||
|
| WindowEvent::Ime(_)
|
||||||
| WindowEvent::Moved(_) => (),
|
| WindowEvent::Moved(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -635,10 +597,6 @@ impl ApplicationHandlerExtMacOS for Application {
|
||||||
|
|
||||||
/// State of the window.
|
/// State of the window.
|
||||||
struct WindowState {
|
struct WindowState {
|
||||||
ime_enabled: bool,
|
|
||||||
/// The contents of the emulated text field for IME purposes (not displayed).
|
|
||||||
/// (text, cursor position in bytes).
|
|
||||||
text_field_contents: (String, usize),
|
|
||||||
/// Render surface.
|
/// Render surface.
|
||||||
///
|
///
|
||||||
/// NOTE: This surface must be dropped before the `Window`.
|
/// NOTE: This surface must be dropped before the `Window`.
|
||||||
|
|
@ -693,21 +651,6 @@ impl WindowState {
|
||||||
let named_idx = 0;
|
let named_idx = 0;
|
||||||
window.set_cursor(CURSORS[named_idx].into());
|
window.set_cursor(CURSORS[named_idx].into());
|
||||||
|
|
||||||
// Allow IME out of the box.
|
|
||||||
let request_data = ImeRequestData::default()
|
|
||||||
.with_purpose(ImePurpose::Normal)
|
|
||||||
.with_cursor_area(LogicalPosition { x: 0, y: 0 }.into(), IME_CURSOR_SIZE.into())
|
|
||||||
.with_surrounding_text(ImeSurroundingText::new(String::new(), 0, 0).unwrap());
|
|
||||||
let enable_request = ImeEnableRequest::new(
|
|
||||||
ImeCapabilities::new().with_purpose().with_cursor_area().with_surrounding_text(),
|
|
||||||
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 size = window.surface_size();
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
#[cfg(macos_platform)]
|
#[cfg(macos_platform)]
|
||||||
|
|
@ -723,8 +666,6 @@ impl WindowState {
|
||||||
continuous_redraw: false,
|
continuous_redraw: false,
|
||||||
#[cfg(not(android_platform))]
|
#[cfg(not(android_platform))]
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
ime_enabled: true,
|
|
||||||
text_field_contents: (String::new(), 0),
|
|
||||||
cursor_position: Default::default(),
|
cursor_position: Default::default(),
|
||||||
cursor_hidden: Default::default(),
|
cursor_hidden: Default::default(),
|
||||||
modifiers: Default::default(),
|
modifiers: Default::default(),
|
||||||
|
|
@ -738,65 +679,12 @@ impl WindowState {
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ime_update(&self) -> ImeRequestData {
|
|
||||||
let (text, cursor) = &self.text_field_contents;
|
|
||||||
// A rudimentary text field emulation: the caret moves right by a constant amount for each
|
|
||||||
// code point.
|
|
||||||
|
|
||||||
let text_before_caret = if text.is_char_boundary(*cursor) { &text[..*cursor] } else { "" };
|
|
||||||
let chars_before_caret = text_before_caret.chars().count();
|
|
||||||
let cursor_pos = LogicalPosition { x: 10 * chars_before_caret as u32, y: 0 }.into();
|
|
||||||
|
|
||||||
// Limit text field size
|
|
||||||
const MAX_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES;
|
|
||||||
let minimal_offset = cursor / MAX_BYTES * MAX_BYTES;
|
|
||||||
let first_char_boundary =
|
|
||||||
(minimal_offset..*cursor).find(|off| text.is_char_boundary(*off)).unwrap_or(*cursor);
|
|
||||||
let last_char_boundary = (*cursor..(first_char_boundary + MAX_BYTES))
|
|
||||||
.rev()
|
|
||||||
.find(|off| text.is_char_boundary(*off))
|
|
||||||
.unwrap_or(*cursor);
|
|
||||||
let surrounding_text = &text[first_char_boundary..last_char_boundary];
|
|
||||||
let relative_cursor = cursor - first_char_boundary;
|
|
||||||
let surrounding_text =
|
|
||||||
ImeSurroundingText::new(surrounding_text.into(), relative_cursor, relative_cursor)
|
|
||||||
.expect("Bug in example: bad byte calculations");
|
|
||||||
ImeRequestData::default()
|
|
||||||
.with_purpose(ImePurpose::Normal)
|
|
||||||
.with_cursor_area(cursor_pos, IME_CURSOR_SIZE.into())
|
|
||||||
.with_surrounding_text(surrounding_text)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_ime(&mut self) {
|
|
||||||
if self.ime_enabled {
|
|
||||||
self.window.request_ime_update(ImeRequest::Disable).expect("disable can not fail");
|
|
||||||
} else {
|
|
||||||
let enable_request = ImeEnableRequest::new(
|
|
||||||
ImeCapabilities::new().with_purpose().with_cursor_area().with_surrounding_text(),
|
|
||||||
self.get_ime_update(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
self.window.request_ime_update(ImeRequest::Enable(enable_request)).unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
self.ime_enabled = !self.ime_enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn minimize(&mut self) {
|
pub fn minimize(&mut self) {
|
||||||
self.window.set_minimized(true);
|
self.window.set_minimized(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_moved(&mut self, position: PhysicalPosition<f64>) {
|
pub fn cursor_moved(&mut self, position: PhysicalPosition<f64>) {
|
||||||
// the IME really cares about the caret,
|
|
||||||
// but there's nothing else to demonstrate a position
|
|
||||||
self.cursor_position = Some(position);
|
self.cursor_position = Some(position);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_left(&mut self) {
|
pub fn cursor_left(&mut self) {
|
||||||
|
|
@ -1113,7 +1001,6 @@ enum Action {
|
||||||
ToggleCursorVisibility,
|
ToggleCursorVisibility,
|
||||||
CreateNewWindow,
|
CreateNewWindow,
|
||||||
ToggleResizeIncrements,
|
ToggleResizeIncrements,
|
||||||
ToggleImeInput,
|
|
||||||
ToggleDecorations,
|
ToggleDecorations,
|
||||||
ToggleResizable,
|
ToggleResizable,
|
||||||
ToggleFullscreen,
|
ToggleFullscreen,
|
||||||
|
|
@ -1150,7 +1037,6 @@ impl Action {
|
||||||
Action::CloseWindow => "Close window",
|
Action::CloseWindow => "Close window",
|
||||||
Action::ToggleCursorVisibility => "Hide cursor",
|
Action::ToggleCursorVisibility => "Hide cursor",
|
||||||
Action::CreateNewWindow => "Create new window",
|
Action::CreateNewWindow => "Create new window",
|
||||||
Action::ToggleImeInput => "Toggle IME input",
|
|
||||||
Action::ToggleDecorations => "Toggle decorations",
|
Action::ToggleDecorations => "Toggle decorations",
|
||||||
Action::ToggleResizable => "Toggle window resizable state",
|
Action::ToggleResizable => "Toggle window resizable state",
|
||||||
Action::ToggleFullscreen => "Toggle fullscreen",
|
Action::ToggleFullscreen => "Toggle fullscreen",
|
||||||
|
|
@ -1370,7 +1256,6 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[
|
||||||
#[cfg(macos_platform)]
|
#[cfg(macos_platform)]
|
||||||
Binding::new("F", ModifiersState::ALT, Action::ToggleSimpleFullscreen),
|
Binding::new("F", ModifiersState::ALT, Action::ToggleSimpleFullscreen),
|
||||||
Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations),
|
Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations),
|
||||||
Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput),
|
|
||||||
Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab),
|
Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab),
|
||||||
Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements),
|
Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements),
|
||||||
Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable),
|
Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable),
|
||||||
|
Before Width: | Height: | Size: 159 B After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
374
winit/examples/ime.rs
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
//! Showcases the use of an input method engine (IME)
|
||||||
|
//! by emulating a text edit field.
|
||||||
|
//!
|
||||||
|
//! Use CTRL+i to toggle IME support.
|
||||||
|
//! Use CTRL+p to cycle content purpose values.
|
||||||
|
//! Use CTRL+h to cycle content hint permutations.
|
||||||
|
|
||||||
|
use std::cmp;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use dpi::{LogicalPosition, PhysicalSize};
|
||||||
|
use tracing::{error, info};
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::event::{Ime, WindowEvent};
|
||||||
|
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
||||||
|
use winit::keyboard::{Key, ModifiersState, NamedKey};
|
||||||
|
#[cfg(web_platform)]
|
||||||
|
use winit::platform::web::WindowAttributesWeb;
|
||||||
|
use winit::window::{
|
||||||
|
ImeCapabilities, ImeEnableRequest, ImeHint, ImePurpose, ImeRequest, ImeRequestData,
|
||||||
|
ImeSurroundingText, Window, WindowAttributes, WindowId,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[path = "util/fill.rs"]
|
||||||
|
mod fill;
|
||||||
|
#[path = "util/tracing.rs"]
|
||||||
|
mod tracing_init;
|
||||||
|
|
||||||
|
const IME_CURSOR_SIZE: PhysicalSize<u32> = PhysicalSize::new(20, 20);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct App {
|
||||||
|
window: Option<Box<dyn Window>>,
|
||||||
|
input_state: TextInputState,
|
||||||
|
modifiers: ModifiersState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of the undisplayed text input field.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TextInputState {
|
||||||
|
ime_enabled: bool,
|
||||||
|
/// The contents of the emulated text field for IME purposes (not displayed).
|
||||||
|
/// (text, cursor position in bytes).
|
||||||
|
contents: String,
|
||||||
|
/// The purpose of the contents the emulated text field expects
|
||||||
|
purpose: ImePurpose,
|
||||||
|
/// The behaviour hints for the IME regarding the emulated text field
|
||||||
|
hint: ImeHint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextInputState {
|
||||||
|
fn text_and_cursor(&self) -> (&str, usize) {
|
||||||
|
(&self.contents, self.contents.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_text(&mut self, text: &str) {
|
||||||
|
self.contents.push_str(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_text(&mut self, text: String) {
|
||||||
|
self.contents = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop(&mut self) {
|
||||||
|
self.contents.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||||
|
#[cfg(not(web_platform))]
|
||||||
|
let window_attributes = WindowAttributes::default();
|
||||||
|
#[cfg(web_platform)]
|
||||||
|
let window_attributes = WindowAttributes::default()
|
||||||
|
.with_platform_attributes(Box::new(WindowAttributesWeb::default().with_append(true)));
|
||||||
|
self.window = match event_loop.create_window(window_attributes) {
|
||||||
|
Ok(window) => Some(window),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("error creating window: {err}");
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow IME out of the box.
|
||||||
|
let enable_request = ImeEnableRequest::new(
|
||||||
|
ImeCapabilities::new()
|
||||||
|
.with_hint_and_purpose()
|
||||||
|
.with_cursor_area()
|
||||||
|
.with_surrounding_text(),
|
||||||
|
self.get_ime_update(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let enable_ime = ImeRequest::Enable(enable_request);
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
self.window().request_ime_update(enable_ime).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) {
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
info!("Close was requested; stopping");
|
||||||
|
self.window = None;
|
||||||
|
event_loop.exit();
|
||||||
|
},
|
||||||
|
WindowEvent::SurfaceResized(_) => {
|
||||||
|
self.window.as_ref().expect("resize event without a window").request_redraw();
|
||||||
|
},
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
// Redraw the application.
|
||||||
|
//
|
||||||
|
// It's preferable for applications that do not render continuously to render in
|
||||||
|
// this event rather than in AboutToWait, since rendering in here allows
|
||||||
|
// the program to gracefully handle redraws requested by the OS.
|
||||||
|
|
||||||
|
let window = self.window.as_ref().expect("redraw request without a window");
|
||||||
|
|
||||||
|
// Notify that you're about to draw.
|
||||||
|
window.pre_present_notify();
|
||||||
|
|
||||||
|
// Draw.
|
||||||
|
fill::fill_window(window.as_ref());
|
||||||
|
|
||||||
|
// For contiguous redraw loop you can request a redraw from here.
|
||||||
|
// window.request_redraw();
|
||||||
|
},
|
||||||
|
WindowEvent::Ime(event) => {
|
||||||
|
self.handle_ime_event(event);
|
||||||
|
},
|
||||||
|
WindowEvent::ModifiersChanged(modifiers) => {
|
||||||
|
self.modifiers = modifiers.state();
|
||||||
|
info!("Modifiers changed to {:?}", self.modifiers);
|
||||||
|
},
|
||||||
|
WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => {
|
||||||
|
let mods = self.modifiers;
|
||||||
|
|
||||||
|
// Dispatch actions only on press.
|
||||||
|
if event.state.is_pressed() {
|
||||||
|
self.handle_key_pressed(event, mods);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn handle_key_pressed(&mut self, event: winit::event::KeyEvent, mods: ModifiersState) {
|
||||||
|
match event.key_without_modifiers.as_ref() {
|
||||||
|
Key::Character("i") if mods == ModifiersState::CONTROL => self.toggle_ime(),
|
||||||
|
Key::Character("p") if mods == ModifiersState::CONTROL => {
|
||||||
|
self.input_state.purpose = match self.input_state.purpose {
|
||||||
|
ImePurpose::Normal => ImePurpose::Password,
|
||||||
|
ImePurpose::Password => ImePurpose::Terminal,
|
||||||
|
ImePurpose::Terminal => ImePurpose::Number,
|
||||||
|
ImePurpose::Number => ImePurpose::Phone,
|
||||||
|
ImePurpose::Phone => ImePurpose::Url,
|
||||||
|
ImePurpose::Url => ImePurpose::Email,
|
||||||
|
ImePurpose::Email => ImePurpose::Pin,
|
||||||
|
ImePurpose::Pin => ImePurpose::Date,
|
||||||
|
ImePurpose::Date => ImePurpose::Time,
|
||||||
|
ImePurpose::Time => ImePurpose::DateTime,
|
||||||
|
ImePurpose::DateTime => ImePurpose::Normal,
|
||||||
|
_ => ImePurpose::Normal,
|
||||||
|
};
|
||||||
|
if self.input_state.ime_enabled {
|
||||||
|
self.window()
|
||||||
|
.request_ime_update(ImeRequest::Update(self.get_ime_update()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
info!("text input purpose now {:?}", self.input_state.purpose);
|
||||||
|
},
|
||||||
|
Key::Character("h") if mods == ModifiersState::CONTROL => {
|
||||||
|
let bump = |hint: ImeHint| {
|
||||||
|
if hint.is_all() {
|
||||||
|
ImeHint::NONE
|
||||||
|
} else {
|
||||||
|
// Go through all integers. We'll skip invalid ones
|
||||||
|
ImeHint::from_bits_retain(hint.bits().wrapping_add(1))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut new_hint = bump(self.input_state.hint);
|
||||||
|
|
||||||
|
while !ImeHint::all().contains(new_hint) {
|
||||||
|
new_hint = bump(new_hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.input_state.hint = new_hint;
|
||||||
|
|
||||||
|
if self.input_state.ime_enabled {
|
||||||
|
self.window()
|
||||||
|
.request_ime_update(ImeRequest::Update(self.get_ime_update()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
info!("text input IME hint now {:?}", self.input_state.hint);
|
||||||
|
},
|
||||||
|
Key::Named(NamedKey::Backspace) => {
|
||||||
|
self.input_state.pop();
|
||||||
|
self.print_input_state();
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
if let Some(text) = event.text_with_all_modifiers {
|
||||||
|
self.input_state.append_text(&text);
|
||||||
|
if self.input_state.ime_enabled {
|
||||||
|
self.window()
|
||||||
|
.request_ime_update(ImeRequest::Update(self.get_ime_update()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
self.print_input_state();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_ime_event(&mut self, event: Ime) {
|
||||||
|
let window = self.window.as_ref().expect("IME request without a window");
|
||||||
|
match event {
|
||||||
|
Ime::Enabled => info!("IME enabled for Window={:?}", window.id()),
|
||||||
|
Ime::Preedit(text, caret_pos) => info!("Preedit: {text}, with caret at {caret_pos:?}"),
|
||||||
|
Ime::Commit(text) => {
|
||||||
|
self.input_state.append_text(&text);
|
||||||
|
let request_data = self.get_ime_update();
|
||||||
|
window.request_ime_update(ImeRequest::Update(request_data)).unwrap();
|
||||||
|
self.print_input_state();
|
||||||
|
},
|
||||||
|
Ime::DeleteSurrounding { before_bytes, after_bytes } => {
|
||||||
|
let (text, cursor) = &self.input_state.text_and_cursor();
|
||||||
|
|
||||||
|
// To anyone copying this, keep in mind that this doesn't take text
|
||||||
|
// selection into account. The deletion happens
|
||||||
|
// *around* the pre-edit, and may remove the whole
|
||||||
|
// selection or a part of it.
|
||||||
|
let delete_start = cursor.saturating_sub(before_bytes);
|
||||||
|
let delete_end = cmp::min(cursor.saturating_add(after_bytes), text.len());
|
||||||
|
if text.is_char_boundary(delete_start) && text.is_char_boundary(delete_end) {
|
||||||
|
let new_text = {
|
||||||
|
let mut t = String::from(&text[..delete_start]);
|
||||||
|
t.push_str(&text[delete_end..]);
|
||||||
|
t
|
||||||
|
};
|
||||||
|
self.input_state.set_text(new_text);
|
||||||
|
info!("IME deleted bytes: {before_bytes}, {after_bytes}");
|
||||||
|
self.print_input_state();
|
||||||
|
} else {
|
||||||
|
error!("Buggy IME tried to delete with indices not on char boundary.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ime::Disabled => info!("IME disabled for Window={:?}", window.id()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_ime(&mut self) {
|
||||||
|
let enable = !self.input_state.ime_enabled;
|
||||||
|
|
||||||
|
if enable {
|
||||||
|
let enable_request = ImeEnableRequest::new(
|
||||||
|
ImeCapabilities::new()
|
||||||
|
.with_hint_and_purpose()
|
||||||
|
.with_cursor_area()
|
||||||
|
.with_surrounding_text(),
|
||||||
|
self.get_ime_update(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
self.window().request_ime_update(ImeRequest::Enable(enable_request)).unwrap();
|
||||||
|
} else {
|
||||||
|
self.window().request_ime_update(ImeRequest::Disable).expect("disable can not fail");
|
||||||
|
};
|
||||||
|
|
||||||
|
self.input_state.ime_enabled = enable;
|
||||||
|
|
||||||
|
info!("IME enabled now {}", self.input_state.ime_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ime_update(&self) -> ImeRequestData {
|
||||||
|
let text = &self.input_state.contents;
|
||||||
|
let cursor = text.len();
|
||||||
|
// A rudimentary text field emulation: the caret moves right by a constant amount for each
|
||||||
|
// code point.
|
||||||
|
|
||||||
|
let text_before_caret = if text.is_char_boundary(cursor) { &text[..cursor] } else { "" };
|
||||||
|
let chars_before_caret = text_before_caret.chars().count();
|
||||||
|
let cursor_pos = LogicalPosition::<u32> { x: 10 * chars_before_caret as u32, y: 0 };
|
||||||
|
|
||||||
|
// Limit text field size
|
||||||
|
const MAX_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES;
|
||||||
|
let minimal_offset = cursor / MAX_BYTES * MAX_BYTES;
|
||||||
|
let first_char_boundary =
|
||||||
|
(minimal_offset..cursor).find(|off| text.is_char_boundary(*off)).unwrap_or(cursor);
|
||||||
|
let last_char_boundary = (cursor..(first_char_boundary + MAX_BYTES))
|
||||||
|
.rev()
|
||||||
|
.find(|off| text.is_char_boundary(*off))
|
||||||
|
.unwrap_or(cursor);
|
||||||
|
let surrounding_text = &text[first_char_boundary..last_char_boundary];
|
||||||
|
let relative_cursor = cursor - first_char_boundary;
|
||||||
|
let surrounding_text =
|
||||||
|
ImeSurroundingText::new(surrounding_text.into(), relative_cursor, relative_cursor)
|
||||||
|
.expect("Bug in example: bad byte calculations");
|
||||||
|
|
||||||
|
ImeRequestData::default()
|
||||||
|
.with_hint_and_purpose(self.input_state.hint, self.input_state.purpose)
|
||||||
|
.with_cursor_area(cursor_pos.into(), IME_CURSOR_SIZE.into())
|
||||||
|
.with_surrounding_text(surrounding_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_input_state(&self) {
|
||||||
|
let (text, cursor) = &self.input_state.text_and_cursor();
|
||||||
|
// Representing a selection with the cursor and anchor as ends is not
|
||||||
|
// supported yet. Using the same position for anchor to mark no
|
||||||
|
// selection.
|
||||||
|
info!("{}", preedit_with_cursor(text, *cursor, *cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window(&self) -> &dyn Window {
|
||||||
|
self.window.as_ref().unwrap().as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints text of the text field, highlighting cursor position
|
||||||
|
fn preedit_with_cursor(text: &str, cursor: usize, anchor: usize) -> String {
|
||||||
|
preedit_with_cursor_checked(text, cursor, anchor).unwrap_or_else(|e| format!("INVALID: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preedit_with_cursor_checked(text: &str, cursor: usize, anchor: usize) -> Result<String, &str> {
|
||||||
|
let first = cmp::min(cursor, anchor);
|
||||||
|
let before = text.get(0..first).ok_or("first segment ends not on char boundary")?;
|
||||||
|
let second = cmp::max(cursor, anchor);
|
||||||
|
let mid = if second == first {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(text.get(first..second).ok_or("second segment ends not on char boundary")?)
|
||||||
|
};
|
||||||
|
let end = text.get(second..).unwrap();
|
||||||
|
Ok(match (first == cursor, before, mid, end) {
|
||||||
|
(_, before, None, end) => format!("{before}|{end}"),
|
||||||
|
(true, before, Some(mid), end) => format!("{before}|{mid}_{end}"),
|
||||||
|
(false, before, Some(mid), end) => format!("{before}_{mid}|{end}"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
#[cfg(web_platform)]
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
tracing_init::init();
|
||||||
|
|
||||||
|
let event_loop = EventLoop::new()?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
r#"This showcases the use of an input method engine (IME) by emulating a text edit field.
|
||||||
|
Use CTRL+i to toggle IME support.
|
||||||
|
Use CTRL+p to cycle content purpose values.
|
||||||
|
Use CTRL+h to cycle content hint permutations.
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
let app = App {
|
||||||
|
window: None,
|
||||||
|
input_state: TextInputState {
|
||||||
|
ime_enabled: true,
|
||||||
|
contents: String::new(),
|
||||||
|
purpose: ImePurpose::Normal,
|
||||||
|
// While we don't show text and thus we use ImeHint::HIDDEN
|
||||||
|
// it may cause the IME to not do layout switch, etc at all.
|
||||||
|
hint: ImeHint::NONE,
|
||||||
|
},
|
||||||
|
modifiers: ModifiersState::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// For alternative loop run options see `pump_events` and `run_on_demand` examples.
|
||||||
|
event_loop.run_app(app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -84,6 +84,8 @@ changelog entry.
|
||||||
- On Wayland, added implementation for `Window::set_window_icon`
|
- On Wayland, added implementation for `Window::set_window_icon`
|
||||||
- Add `Window::request_ime_update` to atomically apply set of IME changes.
|
- Add `Window::request_ime_update` to atomically apply set of IME changes.
|
||||||
- Add `Ime::DeleteSurrounding` to let the input method delete text.
|
- Add `Ime::DeleteSurrounding` to let the input method delete text.
|
||||||
|
- Add more `ImePurpose` values.
|
||||||
|
- Add `ImeHints` to request particular IME behaviour.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
||||||