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 position = LogicalPosition::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 {
|
||||
purpose: Some(ImePurpose::Normal),
|
||||
hint_and_purpose: Some((ImeHint::NONE, ImePurpose::Normal)),
|
||||
// WARNING: there's nothing sensible to use here by default.
|
||||
cursor_area: Some((position.into(), size.into())),
|
||||
..ImeRequestData::default()
|
||||
|
|
@ -1160,9 +1160,9 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
|
|||
/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported.
|
||||
#[deprecated = "use Window::request_ime_update instead"]
|
||||
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 {
|
||||
purpose: Some(purpose),
|
||||
hint_and_purpose: Some((ImeHint::NONE, purpose)),
|
||||
..ImeRequestData::default()
|
||||
}));
|
||||
}
|
||||
|
|
@ -1186,14 +1186,14 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
|
|||
///
|
||||
/// ```no_run
|
||||
/// # 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) {
|
||||
/// // Clear previous state by switching off IME
|
||||
/// 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()
|
||||
/// .with_purpose(ImePurpose::Normal)
|
||||
/// .with_hint_and_purpose(ImeHint::NONE, 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");
|
||||
|
|
@ -1598,7 +1598,10 @@ pub enum WindowLevel {
|
|||
|
||||
/// 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,
|
||||
/// 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.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
|
|
@ -1608,14 +1611,31 @@ pub enum WindowLevel {
|
|||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum ImePurpose {
|
||||
/// No special hints for the IME (default).
|
||||
/// No special purpose for the IME (default).
|
||||
Normal,
|
||||
/// The IME is used for password input.
|
||||
/// The IME will treat the contents as sensitive.
|
||||
Password,
|
||||
/// The IME is used to input into a terminal.
|
||||
///
|
||||
/// For example, that could alter OSK on Wayland to show extra buttons.
|
||||
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 {
|
||||
|
|
@ -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)]
|
||||
pub enum ImeSurroundingTextError {
|
||||
/// Text exceeds 4000 bytes
|
||||
|
|
@ -1759,7 +1821,7 @@ impl ImeEnableRequest {
|
|||
return None;
|
||||
}
|
||||
|
||||
if capabilities.purpose() ^ request_data.purpose.is_some() {
|
||||
if capabilities.hint_and_purpose() ^ request_data.hint_and_purpose.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
@ -1804,23 +1866,23 @@ impl ImeCapabilities {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
/// Marks `purpose` as supported.
|
||||
/// Marks `hint and purpose` as supported.
|
||||
///
|
||||
/// For more details see [`ImeRequestData::with_purpose`].
|
||||
pub const fn with_purpose(self) -> Self {
|
||||
Self(self.0.union(ImeCapabilitiesFlags::PURPOSE))
|
||||
/// For more details see [`ImeRequestData::with_hint_and_purpose`].
|
||||
pub const fn with_hint_and_purpose(self) -> Self {
|
||||
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`].
|
||||
pub const fn without_purpose(self) -> Self {
|
||||
Self(self.0.difference(ImeCapabilitiesFlags::PURPOSE))
|
||||
/// For more details see [`ImeRequestData::with_hint_and_purpose`].
|
||||
pub const fn without_hint_and_purpose(self) -> Self {
|
||||
Self(self.0.difference(ImeCapabilitiesFlags::HINT_AND_PURPOSE))
|
||||
}
|
||||
|
||||
/// Returns `true` if `purpose` is supported.
|
||||
pub const fn purpose(&self) -> bool {
|
||||
self.0.contains(ImeCapabilitiesFlags::PURPOSE)
|
||||
/// Returns `true` if `hint and purpose` is supported.
|
||||
pub const fn hint_and_purpose(&self) -> bool {
|
||||
self.0.contains(ImeCapabilitiesFlags::HINT_AND_PURPOSE)
|
||||
}
|
||||
|
||||
/// Marks `cursor_area` as supported.
|
||||
|
|
@ -1865,8 +1927,8 @@ impl ImeCapabilities {
|
|||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub(crate) struct ImeCapabilitiesFlags : u8 {
|
||||
/// Client supports setting IME purpose.
|
||||
const PURPOSE = 1 << 0;
|
||||
/// Client supports setting IME hint and purpose.
|
||||
const HINT_AND_PURPOSE = 1 << 0;
|
||||
/// Client supports reporting cursor area for IME popup to
|
||||
/// appear.
|
||||
const CURSOR_AREA = 1 << 1;
|
||||
|
|
@ -1884,10 +1946,10 @@ bitflags! {
|
|||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct ImeRequestData {
|
||||
/// Text input purpose.
|
||||
/// Text input hint and purpose.
|
||||
///
|
||||
/// To support updating it, enable [`ImeCapabilities::PURPOSE`].
|
||||
pub purpose: Option<ImePurpose>,
|
||||
/// To support updating it, enable [`ImeCapabilities::HINT_AND_PURPOSE`].
|
||||
pub hint_and_purpose: Option<(ImeHint, ImePurpose)>,
|
||||
/// The IME cursor area which should not be covered by the input method popup.
|
||||
///
|
||||
/// To support updating it, enable [`ImeCapabilities::CURSOR_AREA`].
|
||||
|
|
@ -1899,9 +1961,9 @@ pub struct ImeRequestData {
|
|||
}
|
||||
|
||||
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 hint and purpose of the current text input content.
|
||||
pub fn with_hint_and_purpose(self, hint: ImeHint, purpose: ImePurpose) -> Self {
|
||||
Self { hint_and_purpose: Some((hint, purpose)), ..self }
|
||||
}
|
||||
|
||||
/// Sets the IME cursor editing area.
|
||||
|
|
@ -2027,7 +2089,7 @@ mod tests {
|
|||
ImeCapabilities, ImeEnableRequest, ImeRequestData, ImeSurroundingText,
|
||||
ImeSurroundingTextError,
|
||||
};
|
||||
use crate::window::ImePurpose;
|
||||
use crate::window::{ImeHint, ImePurpose};
|
||||
|
||||
#[test]
|
||||
fn ime_initial_request_caps_match() {
|
||||
|
|
@ -2040,21 +2102,21 @@ mod tests {
|
|||
)
|
||||
.is_none());
|
||||
assert!(ImeEnableRequest::new(
|
||||
ImeCapabilities::new().with_purpose(),
|
||||
ImeCapabilities::new().with_hint_and_purpose(),
|
||||
ImeRequestData::default()
|
||||
)
|
||||
.is_none());
|
||||
|
||||
assert!(ImeEnableRequest::new(
|
||||
ImeCapabilities::new().with_cursor_area(),
|
||||
ImeRequestData::default().with_purpose(ImePurpose::Normal)
|
||||
ImeRequestData::default().with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||
)
|
||||
.is_none());
|
||||
|
||||
assert!(ImeEnableRequest::new(
|
||||
ImeCapabilities::new(),
|
||||
ImeRequestData::default()
|
||||
.with_purpose(ImePurpose::Normal)
|
||||
.with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||
.with_cursor_area(position, size)
|
||||
)
|
||||
.is_none());
|
||||
|
|
@ -2062,7 +2124,7 @@ mod tests {
|
|||
assert!(ImeEnableRequest::new(
|
||||
ImeCapabilities::new().with_cursor_area(),
|
||||
ImeRequestData::default()
|
||||
.with_purpose(ImePurpose::Normal)
|
||||
.with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||
.with_cursor_area(position, size)
|
||||
)
|
||||
.is_none());
|
||||
|
|
@ -2074,9 +2136,9 @@ mod tests {
|
|||
.is_some());
|
||||
|
||||
assert!(ImeEnableRequest::new(
|
||||
ImeCapabilities::new().with_purpose().with_cursor_area(),
|
||||
ImeCapabilities::new().with_hint_and_purpose().with_cursor_area(),
|
||||
ImeRequestData::default()
|
||||
.with_purpose(ImePurpose::Normal)
|
||||
.with_hint_and_purpose(ImeHint::NONE, ImePurpose::Normal)
|
||||
.with_cursor_area(position, size)
|
||||
)
|
||||
.is_some());
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::
|
|||
};
|
||||
use tracing::warn;
|
||||
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;
|
||||
|
||||
|
|
@ -278,9 +280,7 @@ struct DeleteSurroundingText {
|
|||
#[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<u32>, LogicalSize<u32>),
|
||||
|
||||
|
|
@ -302,8 +302,10 @@ impl ClientState {
|
|||
surrounding_text: ImeSurroundingText::new(String::new(), 0, 0).unwrap(),
|
||||
};
|
||||
|
||||
let unsupported_flags =
|
||||
capabilities.without_purpose().without_cursor_area().without_surrounding_text();
|
||||
let unsupported_flags = capabilities
|
||||
.without_hint_and_purpose()
|
||||
.without_cursor_area()
|
||||
.without_surrounding_text();
|
||||
|
||||
if unsupported_flags != ImeCapabilities::new() {
|
||||
warn!(
|
||||
|
|
@ -322,12 +324,10 @@ impl ClientState {
|
|||
|
||||
/// 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.purpose() {
|
||||
self.content_type = purpose.into();
|
||||
} else {
|
||||
warn!("discarding ImePurpose update without capability enabled.");
|
||||
}
|
||||
if let Some((hint, purpose)) =
|
||||
request_data.hint_and_purpose.filter(|_| self.capabilities.hint_and_purpose())
|
||||
{
|
||||
self.content_type = (hint, purpose).into();
|
||||
}
|
||||
|
||||
if let Some((position, size)) = request_data.cursor_area {
|
||||
|
|
@ -350,7 +350,7 @@ impl ClientState {
|
|||
}
|
||||
|
||||
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>)> {
|
||||
|
|
@ -365,20 +365,69 @@ impl ClientState {
|
|||
/// Arguments to content_type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ContentType {
|
||||
/// Text input purpose
|
||||
purpose: ContentPurpose,
|
||||
/// Text input hint.
|
||||
hint: ContentHint,
|
||||
/// Text input purpose.
|
||||
purpose: ContentPurpose,
|
||||
}
|
||||
|
||||
impl From<ImePurpose> 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(),
|
||||
/// The two options influence each other, so they must be converted together.
|
||||
impl From<(ImeHint, ImePurpose)> for ContentType {
|
||||
fn from((hint, purpose): (ImeHint, ImePurpose)) -> Self {
|
||||
let purpose = match purpose {
|
||||
ImePurpose::Password => ContentPurpose::Password,
|
||||
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.
|
||||
//!
|
||||
//! Note that a real application accepting text input **should** support
|
||||
//! the IME interface. See the `ime` example.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
|
|
@ -9,22 +12,21 @@ use std::sync::mpsc::{self, Receiver, Sender};
|
|||
use std::sync::Arc;
|
||||
#[cfg(all(not(android_platform), not(web_platform)))]
|
||||
use std::time::Instant;
|
||||
use std::{cmp, fmt, mem};
|
||||
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))]
|
||||
use softbuffer::{Context, Surface};
|
||||
use tracing::{error, info};
|
||||
#[cfg(all(web_platform, not(android_platform)))]
|
||||
use web_time::Instant;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::cursor::{Cursor, CustomCursor, CustomCursorSource};
|
||||
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
|
||||
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::icon::{Icon, RgbaIcon};
|
||||
use winit::keyboard::{Key, ModifiersState};
|
||||
|
|
@ -39,28 +41,23 @@ 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, ImeCapabilities, ImeEnableRequest, ImePurpose, ImeRequestData,
|
||||
ImeSurroundingText, ResizeDirection, Theme, Window, WindowAttributes, WindowId,
|
||||
};
|
||||
use winit::window::{CursorGrabMode, ResizeDirection, Theme, Window, WindowAttributes, WindowId};
|
||||
use winit_core::application::macos::ApplicationHandlerExtMacOS;
|
||||
use winit_core::window::ImeRequest;
|
||||
|
||||
#[path = "util/tracing.rs"]
|
||||
mod tracing;
|
||||
mod tracing_init;
|
||||
|
||||
#[path = "util/fill.rs"]
|
||||
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<u32> = PhysicalSize::new(20, 20);
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
#[cfg(web_platform)]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
tracing::init();
|
||||
tracing_init::init();
|
||||
|
||||
let event_loop = EventLoop::new()?;
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
|
|
@ -245,7 +242,6 @@ impl Application {
|
|||
window.window.set_simple_fullscreen(!window.window.simple_fullscreen());
|
||||
},
|
||||
Action::ToggleMaximize => window.toggle_maximize(),
|
||||
Action::ToggleImeInput => window.toggle_ime(),
|
||||
Action::Minimize => window.minimize(),
|
||||
Action::NextCursor => window.next_cursor(),
|
||||
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, .. } => {
|
||||
window.zoom += delta;
|
||||
let zoom = window.zoom;
|
||||
|
|
@ -581,6 +542,7 @@ impl ApplicationHandler for Application {
|
|||
| WindowEvent::DragMoved { .. }
|
||||
| WindowEvent::DragDropped { .. }
|
||||
| WindowEvent::Destroyed
|
||||
| WindowEvent::Ime(_)
|
||||
| WindowEvent::Moved(_) => (),
|
||||
}
|
||||
}
|
||||
|
|
@ -635,10 +597,6 @@ impl ApplicationHandlerExtMacOS for Application {
|
|||
|
||||
/// State of the window.
|
||||
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.
|
||||
///
|
||||
/// NOTE: This surface must be dropped before the `Window`.
|
||||
|
|
@ -693,21 +651,6 @@ impl WindowState {
|
|||
let named_idx = 0;
|
||||
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 mut state = Self {
|
||||
#[cfg(macos_platform)]
|
||||
|
|
@ -723,8 +666,6 @@ impl WindowState {
|
|||
continuous_redraw: false,
|
||||
#[cfg(not(android_platform))]
|
||||
start_time: Instant::now(),
|
||||
ime_enabled: true,
|
||||
text_field_contents: (String::new(), 0),
|
||||
cursor_position: Default::default(),
|
||||
cursor_hidden: Default::default(),
|
||||
modifiers: Default::default(),
|
||||
|
|
@ -738,65 +679,12 @@ impl WindowState {
|
|||
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) {
|
||||
self.window.set_minimized(true);
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
|
|
@ -1113,7 +1001,6 @@ enum Action {
|
|||
ToggleCursorVisibility,
|
||||
CreateNewWindow,
|
||||
ToggleResizeIncrements,
|
||||
ToggleImeInput,
|
||||
ToggleDecorations,
|
||||
ToggleResizable,
|
||||
ToggleFullscreen,
|
||||
|
|
@ -1150,7 +1037,6 @@ impl Action {
|
|||
Action::CloseWindow => "Close window",
|
||||
Action::ToggleCursorVisibility => "Hide cursor",
|
||||
Action::CreateNewWindow => "Create new window",
|
||||
Action::ToggleImeInput => "Toggle IME input",
|
||||
Action::ToggleDecorations => "Toggle decorations",
|
||||
Action::ToggleResizable => "Toggle window resizable state",
|
||||
Action::ToggleFullscreen => "Toggle fullscreen",
|
||||
|
|
@ -1370,7 +1256,6 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[
|
|||
#[cfg(macos_platform)]
|
||||
Binding::new("F", ModifiersState::ALT, Action::ToggleSimpleFullscreen),
|
||||
Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations),
|
||||
Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput),
|
||||
Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab),
|
||||
Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements),
|
||||
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`
|
||||
- Add `Window::request_ime_update` to atomically apply set of IME changes.
|
||||
- Add `Ime::DeleteSurrounding` to let the input method delete text.
|
||||
- Add more `ImePurpose` values.
|
||||
- Add `ImeHints` to request particular IME behaviour.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||