summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/src/event.rs4
-rw-r--r--core/src/input_method.rs235
-rw-r--r--core/src/lib.rs2
-rw-r--r--core/src/shell.rs66
-rw-r--r--core/src/text.rs27
-rw-r--r--core/src/text/paragraph.rs6
-rw-r--r--core/src/window/redraw_request.rs12
7 files changed, 316 insertions, 36 deletions
diff --git a/core/src/event.rs b/core/src/event.rs
index b6cf321e..7f0ab914 100644
--- a/core/src/event.rs
+++ b/core/src/event.rs
@@ -1,4 +1,5 @@
//! Handle events of a user interface.
+use crate::input_method;
use crate::keyboard;
use crate::mouse;
use crate::touch;
@@ -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.
diff --git a/core/src/input_method.rs b/core/src/input_method.rs
new file mode 100644
index 00000000..4e8c383b
--- /dev/null
+++ b/core/src/input_method.rs
@@ -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,
+}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 16b3aa0f..c31a8da7 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -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;
@@ -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;
diff --git a/core/src/shell.rs b/core/src/shell.rs
index c2275f71..509e3822 100644
--- a/core/src/shell.rs
+++ b/core/src/shell.rs
@@ -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.
///
@@ -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,
}
@@ -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,
}
}
@@ -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
}
@@ -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
@@ -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);
}
}
diff --git a/core/src/text.rs b/core/src/text.rs
index c144fd24..a7e1f281 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -284,15 +284,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> {
pub fn new(fragment: impl IntoFragment<'a>) -> Self {
Self {
text: fragment.into_fragment(),
- size: None,
- line_height: None,
- font: None,
- color: None,
- highlight: None,
- link: None,
- padding: Padding::ZERO,
- underline: false,
- strikethrough: false,
+ ..Self::default()
}
}
@@ -440,6 +432,23 @@ impl<'a, Link, Font> Span<'a, Link, Font> {
}
}
+impl<Link, Font> Default for Span<'_, Link, Font> {
+ fn default() -> Self {
+ Self {
+ text: Cow::default(),
+ size: None,
+ line_height: None,
+ font: None,
+ color: None,
+ link: None,
+ highlight: None,
+ padding: Padding::default(),
+ underline: false,
+ strikethrough: false,
+ }
+ }
+}
+
impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> {
fn from(value: &'a str) -> Self {
Span::new(value)
diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs
index 924276c3..700c2c75 100644
--- a/core/src/text/paragraph.rs
+++ b/core/src/text/paragraph.rs
@@ -129,6 +129,12 @@ impl<P: Paragraph> Plain<P> {
self.raw.min_width()
}
+ /// Returns the minimum height that can fit the contents of the
+ /// [`Paragraph`].
+ pub fn min_height(&self) -> f32 {
+ self.raw.min_height()
+ }
+
/// Returns the cached [`Paragraph`].
pub fn raw(&self) -> &P {
&self.raw
diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs
index b0c000d6..0ae4face 100644
--- a/core/src/window/redraw_request.rs
+++ b/core/src/window/redraw_request.rs
@@ -8,6 +8,15 @@ pub enum RedrawRequest {
/// Redraw at the given time.
At(Instant),
+
+ /// No redraw is needed.
+ Wait,
+}
+
+impl From<Instant> for RedrawRequest {
+ fn from(time: Instant) -> Self {
+ Self::At(time)
+ }
}
#[cfg(test)]
@@ -34,5 +43,8 @@ mod tests {
assert!(RedrawRequest::At(now) <= RedrawRequest::At(now));
assert!(RedrawRequest::At(now) <= RedrawRequest::At(later));
assert!(RedrawRequest::At(later) >= RedrawRequest::At(now));
+
+ assert!(RedrawRequest::Wait > RedrawRequest::NextFrame);
+ assert!(RedrawRequest::Wait > RedrawRequest::At(later));
}
}