Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Input Method Support #2777

Merged
merged 15 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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