Skip to content

Commit

Permalink
Merge pull request #2777 from kenz-gelsoft/explore-input-method2
Browse files Browse the repository at this point in the history
Input Method Support
  • Loading branch information
hecrj authored Feb 3, 2025
2 parents ca009ba + bab1885 commit 5ab0563
Show file tree
Hide file tree
Showing 20 changed files with 850 additions and 168 deletions.
4 changes: 4 additions & 0 deletions core/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Handle events of a user interface.
use crate::input_method;
use crate::keyboard;
use crate::mouse;
use crate::touch;
Expand All @@ -23,6 +24,9 @@ pub enum Event {

/// A touch event
Touch(touch::Event),

/// An input method event
InputMethod(input_method::Event),
}

/// The status of an [`Event`] after being processed.
Expand Down
235 changes: 235 additions & 0 deletions core/src/input_method.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
//! Listen to input method events.
use crate::Point;

use std::ops::Range;

/// The input method strategy of a widget.
#[derive(Debug, Clone, PartialEq)]
pub enum InputMethod<T = String> {
/// No input method strategy has been specified.
None,
/// No input method is allowed.
Disabled,
/// Input methods are allowed, but not open yet.
Allowed,
/// Input method is open.
Open {
/// The position at which the input method dialog should be placed.
position: Point,
/// The [`Purpose`] of the input method.
purpose: Purpose,
/// The preedit to overlay on top of the input method dialog, if needed.
///
/// Ideally, your widget will show pre-edits on-the-spot; but, since that can
/// be tricky, you can instead provide the current pre-edit here and the
/// runtime will display it as an overlay (i.e. "Over-the-spot IME").
preedit: Option<Preedit<T>>,
},
}

/// The pre-edit of an [`InputMethod`].
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Preedit<T = String> {
/// The current content.
pub content: T,
/// The selected range of the content.
pub selection: Option<Range<usize>>,
}

impl<T> Preedit<T> {
/// Creates a new empty [`Preedit`].
pub fn new() -> Self
where
T: Default,
{
Self::default()
}

/// Turns a [`Preedit`] into its owned version.
pub fn to_owned(&self) -> Preedit
where
T: AsRef<str>,
{
Preedit {
content: self.content.as_ref().to_owned(),
selection: self.selection.clone(),
}
}
}

impl Preedit {
/// Borrows the contents of a [`Preedit`].
pub fn as_ref(&self) -> Preedit<&str> {
Preedit {
content: &self.content,
selection: self.selection.clone(),
}
}
}

/// The purpose of an [`InputMethod`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Purpose {
/// No special hints for the IME (default).
#[default]
Normal,
/// The IME is used for secure input (e.g. passwords).
Secure,
/// The IME is used to input into a terminal.
///
/// For example, that could alter OSK on Wayland to show extra buttons.
Terminal,
}

impl InputMethod {
/// Merges two [`InputMethod`] strategies, prioritizing the first one when both open:
/// ```
/// # use iced_core::input_method::{InputMethod, Purpose, Preedit};
/// # use iced_core::Point;
///
/// let open = InputMethod::Open {
/// position: Point::ORIGIN,
/// purpose: Purpose::Normal,
/// preedit: Some(Preedit { content: "1".to_owned(), selection: None }),
/// };
///
/// let open_2 = InputMethod::Open {
/// position: Point::ORIGIN,
/// purpose: Purpose::Secure,
/// preedit: Some(Preedit { content: "2".to_owned(), selection: None }),
/// };
///
/// let mut ime = InputMethod::Disabled;
///
/// ime.merge(&InputMethod::<String>::Allowed);
/// assert_eq!(ime, InputMethod::Allowed);
///
/// ime.merge(&InputMethod::<String>::Disabled);
/// assert_eq!(ime, InputMethod::Allowed);
///
/// ime.merge(&open);
/// assert_eq!(ime, open);
///
/// ime.merge(&open_2);
/// assert_eq!(ime, open);
/// ```
pub fn merge<T: AsRef<str>>(&mut self, other: &InputMethod<T>) {
match (&self, other) {
(InputMethod::Open { .. }, _)
| (
InputMethod::Allowed,
InputMethod::None | InputMethod::Disabled,
)
| (InputMethod::Disabled, InputMethod::None) => {}
_ => {
*self = other.to_owned();
}
}
}

/// Returns true if the [`InputMethod`] is open.
pub fn is_open(&self) -> bool {
matches!(self, Self::Open { .. })
}
}

impl<T> InputMethod<T> {
/// Turns an [`InputMethod`] into its owned version.
pub fn to_owned(&self) -> InputMethod
where
T: AsRef<str>,
{
match self {
Self::None => InputMethod::None,
Self::Disabled => InputMethod::Disabled,
Self::Allowed => InputMethod::Allowed,
Self::Open {
position,
purpose,
preedit,
} => InputMethod::Open {
position: *position,
purpose: *purpose,
preedit: preedit.as_ref().map(Preedit::to_owned),
},
}
}
}

/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events.
///
/// This is also called a "composition event".
///
/// Most keypresses using a latin-like keyboard layout simply generate a
/// [`keyboard::Event::KeyPressed`](crate::keyboard::Event::KeyPressed).
/// However, one couldn't possibly have a key for every single
/// unicode character that the user might want to type. The solution operating systems employ is
/// to allow the user to type these using _a sequence of keypresses_ instead.
///
/// A prominent example of this is accents—many keyboard layouts allow you to first click the
/// "accent key", and then the character you want to apply the accent to. In this case, some
/// platforms will generate the following event sequence:
///
/// ```ignore
/// // Press "`" key
/// Ime::Preedit("`", Some((0, 0)))
/// // Press "E" key
/// Ime::Preedit("", None) // Synthetic event generated to clear preedit.
/// Ime::Commit("é")
/// ```
///
/// Additionally, certain input devices are configured to display a candidate box that allow the
/// user to select the desired character interactively. (To properly position this box, you must use
/// [`Shell::request_input_method`](crate::Shell::request_input_method).)
///
/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the
/// following event sequence could be obtained:
///
/// ```ignore
/// // Press "A" key
/// Ime::Preedit("a", Some((1, 1)))
/// // Press "B" key
/// Ime::Preedit("a b", Some((3, 3)))
/// // Press left arrow key
/// Ime::Preedit("a b", Some((1, 1)))
/// // Press space key
/// Ime::Preedit("啊b", Some((3, 3)))
/// // Press space key
/// Ime::Preedit("", None) // Synthetic event generated to clear preedit.
/// Ime::Commit("啊不")
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Event {
/// Notifies when the IME was opened.
///
/// After getting this event you could receive [`Preedit`][Self::Preedit] and
/// [`Commit`][Self::Commit] events. You should also start performing IME related requests
/// like [`Shell::request_input_method`].
///
/// [`Shell::request_input_method`]: crate::Shell::request_input_method
Opened,

/// Notifies when a new composing text should be set at the cursor position.
///
/// The value represents a pair of the preedit string and the cursor begin position and end
/// position. When it's `None`, the cursor should be hidden. When `String` is an empty string
/// this indicates that preedit was cleared.
///
/// The cursor range is byte-wise indexed.
Preedit(String, Option<Range<usize>>),

/// Notifies when text should be inserted into the editor widget.
///
/// Right before this event, an empty [`Self::Preedit`] event will be issued.
Commit(String),

/// Notifies when the IME was disabled.
///
/// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or
/// [`Commit`][Self::Commit] events until the next [`Opened`][Self::Opened] event. You should
/// also stop issuing IME related requests like [`Shell::request_input_method`] and clear
/// pending preedit text.
///
/// [`Shell::request_input_method`]: crate::Shell::request_input_method
Closed,
}
2 changes: 2 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod event;
pub mod font;
pub mod gradient;
pub mod image;
pub mod input_method;
pub mod keyboard;
pub mod layout;
pub mod mouse;
Expand Down Expand Up @@ -61,6 +62,7 @@ pub use event::Event;
pub use font::Font;
pub use gradient::Gradient;
pub use image::Image;
pub use input_method::InputMethod;
pub use layout::Layout;
pub use length::Length;
pub use overlay::Overlay;
Expand Down
66 changes: 39 additions & 27 deletions core/src/shell.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::event;
use crate::time::Instant;
use crate::window;
use crate::InputMethod;

/// A connection to the state of a shell.
///
Expand All @@ -12,7 +12,8 @@ use crate::window;
pub struct Shell<'a, Message> {
messages: &'a mut Vec<Message>,
event_status: event::Status,
redraw_request: Option<window::RedrawRequest>,
redraw_request: window::RedrawRequest,
input_method: InputMethod,
is_layout_invalid: bool,
are_widgets_invalid: bool,
}
Expand All @@ -23,9 +24,10 @@ impl<'a, Message> Shell<'a, Message> {
Self {
messages,
event_status: event::Status::Ignored,
redraw_request: None,
redraw_request: window::RedrawRequest::Wait,
is_layout_invalid: false,
are_widgets_invalid: false,
input_method: InputMethod::None,
}
}

Expand Down Expand Up @@ -59,24 +61,19 @@ impl<'a, Message> Shell<'a, Message> {

/// Requests a new frame to be drawn as soon as possible.
pub fn request_redraw(&mut self) {
self.redraw_request = Some(window::RedrawRequest::NextFrame);
}

/// Requests a new frame to be drawn at the given [`Instant`].
pub fn request_redraw_at(&mut self, at: Instant) {
match self.redraw_request {
None => {
self.redraw_request = Some(window::RedrawRequest::At(at));
}
Some(window::RedrawRequest::At(current)) if at < current => {
self.redraw_request = Some(window::RedrawRequest::At(at));
}
_ => {}
}
self.redraw_request = window::RedrawRequest::NextFrame;
}

/// Requests a new frame to be drawn at the given [`window::RedrawRequest`].
pub fn request_redraw_at(
&mut self,
redraw_request: impl Into<window::RedrawRequest>,
) {
self.redraw_request = self.redraw_request.min(redraw_request.into());
}

/// Returns the request a redraw should happen, if any.
pub fn redraw_request(&self) -> Option<window::RedrawRequest> {
pub fn redraw_request(&self) -> window::RedrawRequest {
self.redraw_request
}

Expand All @@ -87,11 +84,32 @@ impl<'a, Message> Shell<'a, Message> {
/// method.
pub fn replace_redraw_request(
shell: &mut Self,
redraw_request: Option<window::RedrawRequest>,
redraw_request: window::RedrawRequest,
) {
shell.redraw_request = redraw_request;
}

/// Requests the current [`InputMethod`] strategy.
///
/// __Important__: This request will only be honored by the
/// [`Shell`] only during a [`window::Event::RedrawRequested`].
pub fn request_input_method<T: AsRef<str>>(
&mut self,
ime: &InputMethod<T>,
) {
self.input_method.merge(ime);
}

/// Returns the current [`InputMethod`] strategy.
pub fn input_method(&self) -> &InputMethod {
&self.input_method
}

/// Returns the current [`InputMethod`] strategy.
pub fn input_method_mut(&mut self) -> &mut InputMethod {
&mut self.input_method
}

/// Returns whether the current layout is invalid or not.
pub fn is_layout_invalid(&self) -> bool {
self.is_layout_invalid
Expand Down Expand Up @@ -134,20 +152,14 @@ impl<'a, Message> Shell<'a, Message> {
pub fn merge<B>(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) {
self.messages.extend(other.messages.drain(..).map(f));

if let Some(new) = other.redraw_request {
self.redraw_request = Some(
self.redraw_request
.map(|current| if current < new { current } else { new })
.unwrap_or(new),
);
}

self.is_layout_invalid =
self.is_layout_invalid || other.is_layout_invalid;

self.are_widgets_invalid =
self.are_widgets_invalid || other.are_widgets_invalid;

self.redraw_request = self.redraw_request.min(other.redraw_request);
self.event_status = self.event_status.merge(other.event_status);
self.input_method.merge(&other.input_method);
}
}
Loading

0 comments on commit 5ab0563

Please sign in to comment.