diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/src/event.rs | 4 | ||||
-rw-r--r-- | core/src/input_method.rs | 235 | ||||
-rw-r--r-- | core/src/lib.rs | 2 | ||||
-rw-r--r-- | core/src/shell.rs | 66 | ||||
-rw-r--r-- | core/src/text.rs | 27 | ||||
-rw-r--r-- | core/src/text/paragraph.rs | 6 | ||||
-rw-r--r-- | core/src/window/redraw_request.rs | 12 |
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)); } } |