diff options
-rw-r--r-- | core/src/input_method.rs | 129 | ||||
-rw-r--r-- | core/src/lib.rs | 2 | ||||
-rw-r--r-- | core/src/shell.rs | 78 | ||||
-rw-r--r-- | core/src/text/paragraph.rs | 6 | ||||
-rw-r--r-- | core/src/window/redraw_request.rs | 12 | ||||
-rw-r--r-- | runtime/src/user_interface.rs | 43 | ||||
-rw-r--r-- | widget/src/action.rs | 14 | ||||
-rw-r--r-- | widget/src/canvas.rs | 15 | ||||
-rw-r--r-- | widget/src/combo_box.rs | 16 | ||||
-rw-r--r-- | widget/src/lazy/component.rs | 29 | ||||
-rw-r--r-- | widget/src/pane_grid.rs | 2 | ||||
-rw-r--r-- | widget/src/scrollable.rs | 18 | ||||
-rw-r--r-- | widget/src/shader.rs | 14 | ||||
-rw-r--r-- | widget/src/text_editor.rs | 133 | ||||
-rw-r--r-- | widget/src/text_input.rs | 148 | ||||
-rw-r--r-- | winit/src/conversion.rs | 36 | ||||
-rw-r--r-- | winit/src/program.rs | 156 | ||||
-rw-r--r-- | winit/src/program/state.rs | 12 | ||||
-rw-r--r-- | winit/src/program/window_manager.rs | 145 |
19 files changed, 538 insertions, 470 deletions
diff --git a/core/src/input_method.rs b/core/src/input_method.rs index d282d404..c293582d 100644 --- a/core/src/input_method.rs +++ b/core/src/input_method.rs @@ -1,17 +1,112 @@ //! 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 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<T>, + }, +} + +/// 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 second one when both ready: + /// ``` + /// # use iced_core::input_method::{InputMethod, Purpose}; + /// # use iced_core::Point; + /// + /// let open = InputMethod::Open { + /// position: Point::ORIGIN, + /// purpose: Purpose::Normal, + /// preedit: None, + /// }; + /// + /// let open_2 = InputMethod::Open { + /// position: Point::ORIGIN, + /// purpose: Purpose::Secure, + /// preedit: 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_2); + /// ``` + pub fn merge<T: AsRef<str>>(&mut self, other: &InputMethod<T>) { + match other { + InputMethod::Disabled => {} + InputMethod::Open { + position, + purpose, + preedit, + } => { + *self = Self::Open { + position: *position, + purpose: *purpose, + preedit: preedit + .as_ref() + .map(AsRef::as_ref) + .map(str::to_owned), + }; + } + InputMethod::Allowed if matches!(self, Self::Disabled) => { + *self = Self::Allowed; + } + InputMethod::Allowed => {} + } + } +} + /// 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 -/// [`WindowEvent::KeyboardInput`]. However, one couldn't possibly have a key for every single -/// unicode character that the user might want to type -/// - so the solution operating systems employ is to allow the user to type these using _a sequence -/// of keypresses_ instead. +/// [`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 +/// 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: /// @@ -19,13 +114,13 @@ use std::ops::Range; /// // Press "`" key /// Ime::Preedit("`", Some((0, 0))) /// // Press "E" key -/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// 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 -/// [`Window::set_ime_cursor_area`].) +/// [`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: @@ -40,17 +135,19 @@ use std::ops::Range; /// // Press space key /// Ime::Preedit("啊b", Some((3, 3))) /// // Press space key -/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// 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 enabled. + /// 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 [`Window::set_ime_cursor_area`]. - Enabled, + /// 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. /// @@ -63,14 +160,16 @@ pub enum Event { /// Notifies when text should be inserted into the editor widget. /// - /// Right before this event winit will send empty [`Self::Preedit`] event. + /// 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 [`Enabled`][Self::Enabled] event. You should - /// also stop issuing IME related requests like [`Window::set_ime_cursor_area`] and clear + /// [`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. - Disabled, + /// + /// [`Shell::request_input_method`]: crate::Shell::request_input_method + Closed, } diff --git a/core/src/lib.rs b/core/src/lib.rs index c7c38044..c31a8da7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -62,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; @@ -73,7 +74,6 @@ pub use renderer::Renderer; pub use rotation::Rotation; pub use settings::Settings; pub use shadow::Shadow; -pub use shell::CaretInfo; pub use shell::Shell; pub use size::Size; pub use svg::Svg; diff --git a/core/src/shell.rs b/core/src/shell.rs index d2c1b9ec..e87d1696 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -1,15 +1,6 @@ -use crate::time::Instant; +use crate::event; use crate::window; -use crate::{event, Point}; - -/// TODO -#[derive(Clone, Copy, Debug)] -pub struct CaretInfo { - /// TODO - pub position: Point, - /// TODO - pub input_method_allowed: bool, -} +use crate::InputMethod; /// A connection to the state of a shell. /// @@ -21,10 +12,10 @@ pub struct CaretInfo { 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, - caret_info: Option<CaretInfo>, } impl<'a, Message> Shell<'a, Message> { @@ -33,10 +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, - caret_info: None, + input_method: InputMethod::Disabled, } } @@ -70,35 +61,38 @@ 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 } - /// TODO - pub fn update_caret_info(&mut self, caret_info: Option<CaretInfo>) { - self.caret_info = caret_info.or(self.caret_info); + /// Requests the current [`InputMethod`] strategy. + pub fn request_input_method<T: AsRef<str>>( + &mut self, + ime: &InputMethod<T>, + ) { + self.input_method.merge(ime); } - /// TODO - pub fn caret_info(&self) -> Option<CaretInfo> { - self.caret_info + /// 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. @@ -143,22 +137,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.update_caret_info(other.caret_info()); - 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/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)); } } diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 6d3360d0..59e497c5 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -6,7 +6,7 @@ use crate::core::renderer; use crate::core::widget; use crate::core::window; use crate::core::{ - CaretInfo, Clipboard, Element, Layout, Rectangle, Shell, Size, Vector, + Clipboard, Element, InputMethod, Layout, Rectangle, Shell, Size, Vector, }; use crate::overlay; @@ -188,8 +188,8 @@ where use std::mem::ManuallyDrop; let mut outdated = false; - let mut redraw_request = None; - let mut caret_info = None; + let mut redraw_request = window::RedrawRequest::Wait; + let mut input_method = InputMethod::Disabled; let mut manual_overlay = ManuallyDrop::new( self.root @@ -223,17 +223,8 @@ where ); event_statuses.push(shell.event_status()); - - match (redraw_request, shell.redraw_request()) { - (None, Some(at)) => { - redraw_request = Some(at); - } - (Some(current), Some(new)) if new < current => { - redraw_request = Some(new); - } - _ => {} - } - caret_info = caret_info.or(shell.caret_info()); + redraw_request = redraw_request.min(shell.redraw_request()); + input_method.merge(shell.input_method()); if shell.is_layout_invalid() { let _ = ManuallyDrop::into_inner(manual_overlay); @@ -327,16 +318,8 @@ where self.overlay = None; } - match (redraw_request, shell.redraw_request()) { - (None, Some(at)) => { - redraw_request = Some(at); - } - (Some(current), Some(new)) if new < current => { - redraw_request = Some(new); - } - _ => {} - } - caret_info = caret_info.or(shell.caret_info()); + redraw_request = redraw_request.min(shell.redraw_request()); + input_method.merge(shell.input_method()); shell.revalidate_layout(|| { self.base = self.root.as_widget().layout( @@ -362,7 +345,7 @@ where } else { State::Updated { redraw_request, - caret_info, + input_method, } }, event_statuses, @@ -644,7 +627,7 @@ impl Default for Cache { } /// The resulting state after updating a [`UserInterface`]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum State { /// The [`UserInterface`] is outdated and needs to be rebuilt. Outdated, @@ -652,9 +635,9 @@ pub enum State { /// The [`UserInterface`] is up-to-date and can be reused without /// rebuilding. Updated { - /// The [`window::RedrawRequest`] when a redraw should be performed. - redraw_request: Option<window::RedrawRequest>, - /// TODO - caret_info: Option<CaretInfo>, + /// The [`window::RedrawRequest`] describing when a redraw should be performed. + redraw_request: window::RedrawRequest, + /// The current [`InputMethod`] strategy of the user interface. + input_method: InputMethod, }, } diff --git a/widget/src/action.rs b/widget/src/action.rs index 1dd3a787..cc31e76a 100644 --- a/widget/src/action.rs +++ b/widget/src/action.rs @@ -6,7 +6,7 @@ use crate::core::window; #[derive(Debug, Clone)] pub struct Action<Message> { message_to_publish: Option<Message>, - redraw_request: Option<window::RedrawRequest>, + redraw_request: window::RedrawRequest, event_status: event::Status, } @@ -14,7 +14,7 @@ impl<Message> Action<Message> { fn new() -> Self { Self { message_to_publish: None, - redraw_request: None, + redraw_request: window::RedrawRequest::Wait, event_status: event::Status::Ignored, } } @@ -46,7 +46,7 @@ impl<Message> Action<Message> { /// soon as possible; without publishing any `Message`. pub fn request_redraw() -> Self { Self { - redraw_request: Some(window::RedrawRequest::NextFrame), + redraw_request: window::RedrawRequest::NextFrame, ..Self::new() } } @@ -58,7 +58,7 @@ impl<Message> Action<Message> { /// blinking caret on a text input. pub fn request_redraw_at(at: Instant) -> Self { Self { - redraw_request: Some(window::RedrawRequest::At(at)), + redraw_request: window::RedrawRequest::At(at), ..Self::new() } } @@ -75,11 +75,7 @@ impl<Message> Action<Message> { /// widget implementations. pub fn into_inner( self, - ) -> ( - Option<Message>, - Option<window::RedrawRequest>, - event::Status, - ) { + ) -> (Option<Message>, window::RedrawRequest, event::Status) { ( self.message_to_publish, self.redraw_request, diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 23cc3f2b..d10771f0 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -238,27 +238,18 @@ where { let (message, redraw_request, event_status) = action.into_inner(); + shell.request_redraw_at(redraw_request); + if let Some(message) = message { shell.publish(message); } - if let Some(redraw_request) = redraw_request { - match redraw_request { - window::RedrawRequest::NextFrame => { - shell.request_redraw(); - } - window::RedrawRequest::At(at) => { - shell.request_redraw_at(at); - } - } - } - if event_status == event::Status::Captured { shell.capture_event(); } } - if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) { + if shell.redraw_request() != window::RedrawRequest::NextFrame { let mouse_interaction = self .mouse_interaction(tree, layout, cursor, viewport, renderer); diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index d7c7c922..05793155 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -63,7 +63,6 @@ use crate::core::renderer; use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; -use crate::core::window; use crate::core::{ Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme, Vector, @@ -554,17 +553,8 @@ where shell.capture_event(); } - if let Some(redraw_request) = local_shell.redraw_request() { - match redraw_request { - window::RedrawRequest::NextFrame => { - shell.request_redraw(); - } - window::RedrawRequest::At(at) => { - shell.request_redraw_at(at); - } - } - } - shell.update_caret_info(local_shell.caret_info()); + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); // Then finally react to them here for message in local_messages { @@ -757,7 +747,7 @@ where &mut local_shell, viewport, ); - shell.update_caret_info(local_shell.caret_info()); + shell.request_input_method(local_shell.input_method()); } }); diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index b9fbde58..c93b7c42 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -6,7 +6,6 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; -use crate::core::window; use crate::core::{ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, @@ -344,18 +343,8 @@ where } local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if let Some(redraw_request) = local_shell.redraw_request() { - match redraw_request { - window::RedrawRequest::NextFrame => { - shell.request_redraw(); - } - window::RedrawRequest::At(at) => { - shell.request_redraw_at(at); - } - } - } - shell.update_caret_info(local_shell.caret_info()); + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); if !local_messages.is_empty() { let mut heads = self.state.take().unwrap().into_heads(); @@ -630,18 +619,8 @@ where } local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if let Some(redraw_request) = local_shell.redraw_request() { - match redraw_request { - window::RedrawRequest::NextFrame => { - shell.request_redraw(); - } - window::RedrawRequest::At(at) => { - shell.request_redraw_at(at); - } - } - } - shell.update_caret_info(local_shell.caret_info()); + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); if !local_messages.is_empty() { let mut inner = diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 5c3b343c..e972b983 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -687,7 +687,7 @@ where _ => {} } - if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) { + if shell.redraw_request() != window::RedrawRequest::NextFrame { let interaction = self .grid_interaction(action, layout, cursor) .or_else(|| { diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 7df7a0e5..0a93584e 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -33,7 +33,7 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - self, Background, CaretInfo, Clipboard, Color, Element, Event, Layout, + self, Background, Clipboard, Color, Element, Event, InputMethod, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; @@ -730,7 +730,6 @@ where let translation = state.translation(self.direction, bounds, content_bounds); - let children_may_have_caret = shell.caret_info().is_none(); self.content.as_widget_mut().update( &mut tree.children[0], event.clone(), @@ -746,17 +745,10 @@ where }, ); - if children_may_have_caret { - if let Some(caret_info) = shell.caret_info() { - shell.update_caret_info(Some(CaretInfo { - position: Point::new( - caret_info.position.x - translation.x, - caret_info.position.y - translation.y, - ), - input_method_allowed: caret_info - .input_method_allowed, - })); - } + if let InputMethod::Open { position, .. } = + shell.input_method_mut() + { + *position = *position + translation; } }; diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 8ec57482..48c96321 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -9,7 +9,6 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; -use crate::core::window; use crate::core::{Clipboard, Element, Event, Length, Rectangle, Shell, Size}; use crate::renderer::wgpu::primitive; @@ -105,21 +104,12 @@ where { let (message, redraw_request, event_status) = action.into_inner(); + shell.request_redraw_at(redraw_request); + if let Some(message) = message { shell.publish(message); } - if let Some(redraw_request) = redraw_request { - match redraw_request { - window::RedrawRequest::NextFrame => { - shell.request_redraw(); - } - window::RedrawRequest::At(at) => { - shell.request_redraw_at(at); - } - } - } - if event_status == event::Status::Captured { shell.capture_event(); } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 529c8b90..4f985f28 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -47,7 +47,7 @@ use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::window; use crate::core::{ - Background, Border, CaretInfo, Color, Element, Event, Length, Padding, + Background, Border, Color, Element, Event, InputMethod, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; @@ -324,43 +324,49 @@ where self } - fn caret_rect( + fn input_method<'b>( &self, - tree: &widget::Tree, + state: &'b State<Highlighter>, renderer: &Renderer, layout: Layout<'_>, - ) -> Option<Rectangle> { - let bounds = layout.bounds(); + ) -> InputMethod<&'b str> { + let Some(Focus { + is_window_focused: true, + is_ime_open, + .. + }) = &state.focus + else { + return InputMethod::Disabled; + }; + + let Some(preedit) = &is_ime_open else { + return InputMethod::Allowed; + }; + let bounds = layout.bounds(); let internal = self.content.0.borrow_mut(); - let state = tree.state.downcast_ref::<State<Highlighter>>(); let text_bounds = bounds.shrink(self.padding); let translation = text_bounds.position() - Point::ORIGIN; - if state.focus.is_some() { - let position = match internal.editor.cursor() { - Cursor::Caret(position) => position, - Cursor::Selection(ranges) => ranges - .first() - .cloned() - .unwrap_or(Rectangle::default()) - .position(), - }; - Some(Rectangle::new( - position + translation, - Size::new( - 1.0, - self.line_height - .to_absolute( - self.text_size - .unwrap_or_else(|| renderer.default_size()), - ) - .into(), - ), - )) - } else { - None + let cursor = match internal.editor.cursor() { + Cursor::Caret(position) => position, + Cursor::Selection(ranges) => { + ranges.first().cloned().unwrap_or_default().position() + } + }; + + let line_height = self.line_height.to_absolute( + self.text_size.unwrap_or_else(|| renderer.default_size()), + ); + + let position = + cursor + translation + Vector::new(0.0, f32::from(line_height)); + + InputMethod::Open { + position, + purpose: input_method::Purpose::Normal, + preedit: Some(preedit), } } } @@ -499,11 +505,12 @@ pub struct State<Highlighter: text::Highlighter> { highlighter_format_address: usize, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, is_window_focused: bool, + is_ime_open: Option<String>, } impl Focus { @@ -516,6 +523,7 @@ impl Focus { updated_at: now, now, is_window_focused: true, + is_ime_open: None, } } @@ -742,11 +750,23 @@ where })); shell.capture_event(); } - Update::Commit(text) => { - shell.publish(on_edit(Action::Edit(Edit::Paste( - Arc::new(text), - )))); - } + Update::InputMethod(update) => match update { + Ime::Toggle(is_open) => { + if let Some(focus) = &mut state.focus { + focus.is_ime_open = is_open.then(String::new); + } + } + Ime::Preedit(text) => { + if let Some(focus) = &mut state.focus { + focus.is_ime_open = Some(text); + } + } + Ime::Commit(text) => { + shell.publish(on_edit(Action::Edit(Edit::Paste( + Arc::new(text), + )))); + } + }, Update::Binding(binding) => { fn apply_binding< H: text::Highlighter, @@ -871,22 +891,12 @@ where } }; - shell.update_caret_info(if state.is_focused() { - let rect = - self.caret_rect(tree, renderer, layout).unwrap_or_default(); - - let bottom_left = Point::new(rect.x, rect.y + rect.height); - - Some(CaretInfo { - position: bottom_left, - input_method_allowed: true, - }) - } else { - None - }); - if is_redraw { self.last_status = Some(status); + + shell.request_input_method( + &self.input_method(state, renderer, layout), + ); } else if self .last_status .is_some_and(|last_status| status != last_status) @@ -1189,10 +1199,16 @@ enum Update<Message> { Drag(Point), Release, Scroll(f32), - Commit(String), + InputMethod(Ime), Binding(Binding<Message>), } +enum Ime { + Toggle(bool), + Preedit(String), + Commit(String), +} + impl<Message> Update<Message> { fn from_event<H: Highlighter>( event: Event, @@ -1252,9 +1268,20 @@ impl<Message> Update<Message> { } _ => None, }, - Event::InputMethod(input_method::Event::Commit(text)) => { - Some(Update::Commit(text)) - } + Event::InputMethod(event) => match event { + input_method::Event::Opened | input_method::Event::Closed => { + Some(Update::InputMethod(Ime::Toggle(matches!( + event, + input_method::Event::Opened + )))) + } + input_method::Event::Preedit(content, _range) => { + Some(Update::InputMethod(Ime::Preedit(content))) + } + input_method::Event::Commit(content) => { + Some(Update::InputMethod(Ime::Commit(content))) + } + }, Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index ba5d1843..d0e93927 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -57,7 +57,7 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Border, CaretInfo, Color, Element, Event, Layout, Length, + Background, Border, Color, Element, Event, InputMethod, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; @@ -392,14 +392,24 @@ where } } - fn caret_rect( + fn input_method<'b>( &self, - tree: &Tree, + state: &'b State<Renderer::Paragraph>, layout: Layout<'_>, - value: Option<&Value>, - ) -> Option<Rectangle> { - let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); - let value = value.unwrap_or(&self.value); + value: &Value, + ) -> InputMethod<&'b str> { + let Some(Focus { + is_window_focused: true, + is_ime_open, + .. + }) = &state.is_focused + else { + return InputMethod::Disabled; + }; + + let Some(preedit) = is_ime_open else { + return InputMethod::Allowed; + }; let secure_value = self.is_secure.then(|| value.secure()); let value = secure_value.as_ref().unwrap_or(value); @@ -407,38 +417,32 @@ where let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - if state - .is_focused - .is_some_and(|focus| focus.is_window_focused) - { - let caret_index = match state.cursor.state(value) { - cursor::State::Index(position) => position, - cursor::State::Selection { start, end } => start.min(end), - }; + let caret_index = match state.cursor.state(value) { + cursor::State::Index(position) => position, + cursor::State::Selection { start, end } => start.min(end), + }; - let text = state.value.raw(); - let (caret_x, offset) = measure_cursor_and_scroll_offset( - text, - text_bounds, - caret_index, - ); + let text = state.value.raw(); + let (cursor_x, scroll_offset) = + measure_cursor_and_scroll_offset(text, text_bounds, caret_index); - let alignment_offset = alignment_offset( - text_bounds.width, - text.min_width(), - self.alignment, - ); + let alignment_offset = alignment_offset( + text_bounds.width, + text.min_width(), + self.alignment, + ); - let x = (text_bounds.x + caret_x).floor(); + let x = (text_bounds.x + cursor_x).floor() - scroll_offset + + alignment_offset; - Some(Rectangle { - x: (alignment_offset - offset) + x, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }) - } else { - None + InputMethod::Open { + position: Point::new(x, text_bounds.y), + purpose: if self.is_secure { + input_method::Purpose::Secure + } else { + input_method::Purpose::Normal + }, + preedit: Some(preedit), } } @@ -725,6 +729,7 @@ where updated_at: now, now, is_window_focused: true, + is_ime_open: None, }) } else { None @@ -1248,28 +1253,46 @@ where state.keyboard_modifiers = *modifiers; } - Event::InputMethod(input_method::Event::Commit(text)) => { - let state = state::<Renderer>(tree); + Event::InputMethod(event) => match event { + input_method::Event::Opened | input_method::Event::Closed => { + let state = state::<Renderer>(tree); - if let Some(focus) = &mut state.is_focused { - let Some(on_input) = &self.on_input else { - return; - }; + if let Some(focus) = &mut state.is_focused { + focus.is_ime_open = + matches!(event, input_method::Event::Opened) + .then(String::new); + } + } + input_method::Event::Preedit(content, _range) => { + let state = state::<Renderer>(tree); - let mut editor = - Editor::new(&mut self.value, &mut state.cursor); - editor.paste(Value::new(text)); + if let Some(focus) = &mut state.is_focused { + focus.is_ime_open = Some(content.to_owned()); + } + } + input_method::Event::Commit(text) => { + let state = state::<Renderer>(tree); - focus.updated_at = Instant::now(); - state.is_pasting = None; + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = &self.on_input else { + return; + }; - let message = (on_input)(editor.contents()); - shell.publish(message); - shell.capture_event(); + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.paste(Value::new(text)); + + focus.updated_at = Instant::now(); + state.is_pasting = None; - update_cache(state, &self.value); + let message = (on_input)(editor.contents()); + shell.publish(message); + shell.capture_event(); + + update_cache(state, &self.value); + } } - } + }, Event::Window(window::Event::Unfocused) => { let state = state::<Renderer>(tree); @@ -1329,21 +1352,14 @@ where Status::Active }; - shell.update_caret_info(if state.is_focused() { - let rect = self - .caret_rect(tree, layout, Some(&self.value)) - .unwrap_or(Rectangle::with_size(Size::<f32>::default())); - let bottom_left = Point::new(rect.x, rect.y + rect.height); - Some(CaretInfo { - position: bottom_left, - input_method_allowed: true, - }) - } else { - None - }); - if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.last_status = Some(status); + + shell.request_input_method(&self.input_method( + state, + layout, + &self.value, + )); } else if self .last_status .is_some_and(|last_status| status != last_status) @@ -1517,11 +1533,12 @@ fn state<Renderer: text::Renderer>( tree.state.downcast_mut::<State<Renderer::Paragraph>>() } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, is_window_focused: bool, + is_ime_open: Option<String>, } impl<P: text::Paragraph> State<P> { @@ -1548,6 +1565,7 @@ impl<P: text::Paragraph> State<P> { updated_at: now, now, is_window_focused: true, + is_ime_open: None, }); self.move_cursor_to_end(); diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index c7f9aaaf..ab84afff 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -141,6 +141,7 @@ pub fn window_event( scale_factor: f64, modifiers: winit::keyboard::ModifiersState, ) -> Option<Event> { + use winit::event::Ime; use winit::event::WindowEvent; match event { @@ -284,19 +285,15 @@ pub fn window_event( self::modifiers(new_modifiers.state()), ))) } - WindowEvent::Ime(ime) => { - use winit::event::Ime; - println!("ime event: {:?}", ime); - Some(Event::InputMethod(match ime { - Ime::Enabled => input_method::Event::Enabled, - Ime::Preedit(s, size) => input_method::Event::Preedit( - s, - size.map(|(start, end)| (start..end)), - ), - Ime::Commit(s) => input_method::Event::Commit(s), - Ime::Disabled => input_method::Event::Disabled, - })) - } + WindowEvent::Ime(event) => Some(Event::InputMethod(match event { + Ime::Enabled => input_method::Event::Opened, + Ime::Preedit(content, size) => input_method::Event::Preedit( + content, + size.map(|(start, end)| (start..end)), + ), + Ime::Commit(content) => input_method::Event::Commit(content), + Ime::Disabled => input_method::Event::Closed, + })), WindowEvent::Focused(focused) => Some(Event::Window(if focused { window::Event::Focused } else { @@ -1174,7 +1171,7 @@ pub fn resize_direction( } } -/// Converts some [`window::Icon`] into it's `winit` counterpart. +/// Converts some [`window::Icon`] into its `winit` counterpart. /// /// Returns `None` if there is an error during the conversion. pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { @@ -1183,6 +1180,17 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } +/// Convertions some [`input_method::Purpose`] to its `winit` counterpart. +pub fn ime_purpose( + purpose: input_method::Purpose, +) -> winit::window::ImePurpose { + match purpose { + input_method::Purpose::Normal => winit::window::ImePurpose::Normal, + input_method::Purpose::Secure => winit::window::ImePurpose::Password, + input_method::Purpose::Terminal => winit::window::ImePurpose::Terminal, + } +} + // See: https://en.wikipedia.org/wiki/Private_Use_Areas fn is_private_use(c: char) -> bool { ('\u{E000}'..='\u{F8FF}').contains(&c) diff --git a/winit/src/program.rs b/winit/src/program.rs index 688d6731..302bc6c3 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -3,8 +3,6 @@ mod state; mod window_manager; pub use state::State; -use winit::dpi::LogicalPosition; -use winit::dpi::LogicalSize; use crate::conversion; use crate::core; @@ -581,8 +579,6 @@ async fn run_instance<P, C>( let mut clipboard = Clipboard::unconnected(); let mut compositor_receiver: Option<oneshot::Receiver<_>> = None; - let mut preedit = Preedit::<P>::new(); - debug.startup_finished(); loop { @@ -878,29 +874,15 @@ async fn run_instance<P, C>( if let user_interface::State::Updated { redraw_request, - caret_info, + input_method, } = ui_state { - match redraw_request { - Some(window::RedrawRequest::NextFrame) => { - window.raw.request_redraw(); - window.redraw_at = None; - } - Some(window::RedrawRequest::At(at)) => { - window.redraw_at = Some(at); - } - None => {} - } - - if let Some(caret_info) = caret_info { - update_input_method( - window, - &mut preedit, - &caret_info, - ); - } + window.request_redraw(redraw_request); + window.request_input_method(input_method); } + window.draw_preedit(); + debug.render_started(); match compositor.present( &mut window.renderer, @@ -1048,31 +1030,14 @@ async fn run_instance<P, C>( match ui_state { user_interface::State::Updated { redraw_request: _redraw_request, - caret_info, + input_method, } => { #[cfg(not( feature = "unconditional-rendering" ))] - match _redraw_request { - Some( - window::RedrawRequest::NextFrame, - ) => { - window.raw.request_redraw(); - window.redraw_at = None; - } - Some(window::RedrawRequest::At(at)) => { - window.redraw_at = Some(at); - } - None => {} - } + window.request_redraw(_redraw_request); - if let Some(caret_info) = caret_info { - update_input_method( - window, - &mut preedit, - &caret_info, - ); - } + window.request_input_method(input_method); } user_interface::State::Outdated => { uis_stale = true; @@ -1165,111 +1130,6 @@ async fn run_instance<P, C>( let _ = ManuallyDrop::into_inner(user_interfaces); } -fn update_input_method<P, C>( - window: &mut crate::program::window_manager::Window<P, C>, - preedit: &mut Preedit<P>, - caret_info: &crate::core::CaretInfo, -) where - P: Program, - C: Compositor<Renderer = P::Renderer> + 'static, -{ - window.raw.set_ime_allowed(caret_info.input_method_allowed); - window.raw.set_ime_cursor_area( - LogicalPosition::new(caret_info.position.x, caret_info.position.y), - LogicalSize::new(10, 10), - ); - - let text = window.state.preedit(); - if !text.is_empty() { - preedit.update(text.as_str(), &window.renderer); - preedit.fill( - &mut window.renderer, - window.state.text_color(), - window.state.background_color(), - caret_info.position, - ); - } -} - -struct Preedit<P: Program> { - content: Option<<P::Renderer as core::text::Renderer>::Paragraph>, -} - -impl<P: Program> Preedit<P> { - fn new() -> Self { - Self { content: None } - } - - fn update(&mut self, text: &str, renderer: &P::Renderer) { - use core::text::Paragraph as _; - use core::text::Renderer as _; - - self.content = Some( - <P::Renderer as core::text::Renderer>::Paragraph::with_text( - core::Text::<&str, <P::Renderer as core::text::Renderer>::Font> { - content: text, - bounds: Size::INFINITY, - size: renderer.default_size(), - line_height: core::text::LineHeight::default(), - font: renderer.default_font(), - horizontal_alignment: core::alignment::Horizontal::Left, - vertical_alignment: core::alignment::Vertical::Top, //Bottom, - shaping: core::text::Shaping::Advanced, - wrapping: core::text::Wrapping::None, - }, - ), - ); - } - - fn fill( - &self, - renderer: &mut P::Renderer, - fore_color: core::Color, - bg_color: core::Color, - caret_position: Point, - ) { - use core::text::Paragraph as _; - use core::text::Renderer as _; - use core::Renderer as _; - - let Some(ref content) = self.content else { - return; - }; - if content.min_width() < 1.0 { - return; - } - - let top_left = Point::new( - caret_position.x, - caret_position.y - content.min_height(), - ); - let bounds = core::Rectangle::new(top_left, content.min_bounds()); - renderer.with_layer(bounds, |renderer| { - renderer.fill_quad( - core::renderer::Quad { - bounds, - ..Default::default() - }, - core::Background::Color(bg_color), - ); - - let underline = 2.; - renderer.fill_quad( - core::renderer::Quad { - bounds: bounds.shrink(core::Padding { - top: bounds.height - underline, - ..Default::default() - }), - ..Default::default() - }, - core::Background::Color(fore_color), - ); - - renderer.fill_paragraph(content, top_left, fore_color, bounds); - }); - } -} - /// Builds a window's [`UserInterface`] for the [`Program`]. fn build_user_interface<'a, P: Program>( program: &'a P, diff --git a/winit/src/program/state.rs b/winit/src/program/state.rs index 6361d1ba..e883d04a 100644 --- a/winit/src/program/state.rs +++ b/winit/src/program/state.rs @@ -4,7 +4,7 @@ use crate::core::{Color, Size}; use crate::graphics::Viewport; use crate::program::Program; -use winit::event::{Ime, Touch, WindowEvent}; +use winit::event::{Touch, WindowEvent}; use winit::window::Window; use std::fmt::{Debug, Formatter}; @@ -22,7 +22,6 @@ where modifiers: winit::keyboard::ModifiersState, theme: P::Theme, style: theme::Style, - preedit: String, } impl<P: Program> Debug for State<P> @@ -74,7 +73,6 @@ where modifiers: winit::keyboard::ModifiersState::default(), theme, style, - preedit: String::default(), } } @@ -138,11 +136,6 @@ where self.style.text_color } - /// TODO - pub fn preedit(&self) -> String { - self.preedit.clone() - } - /// Processes the provided window event and updates the [`State`] accordingly. pub fn update( &mut self, @@ -186,9 +179,6 @@ where WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = new_modifiers.state(); } - WindowEvent::Ime(Ime::Preedit(text, _)) => { - self.preedit = text.clone(); - } #[cfg(feature = "debug")] WindowEvent::KeyboardInput { event: diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index a3c991df..cd49a8b4 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -1,13 +1,20 @@ +use crate::conversion; +use crate::core::alignment; use crate::core::mouse; +use crate::core::renderer; +use crate::core::text; use crate::core::theme; use crate::core::time::Instant; -use crate::core::window::Id; -use crate::core::{Point, Size}; +use crate::core::window::{Id, RedrawRequest}; +use crate::core::{ + Color, InputMethod, Padding, Point, Rectangle, Size, Text, Vector, +}; use crate::graphics::Compositor; use crate::program::{Program, State}; use std::collections::BTreeMap; use std::sync::Arc; +use winit::dpi::{LogicalPosition, LogicalSize}; use winit::monitor::MonitorHandle; #[allow(missing_debug_implementations)] @@ -65,6 +72,7 @@ where renderer, mouse_interaction: mouse::Interaction::None, redraw_at: None, + preedit: None, }, ); @@ -155,6 +163,7 @@ where pub surface: C::Surface, pub renderer: P::Renderer, pub redraw_at: Option<Instant>, + preedit: Option<Preedit<P::Renderer>>, } impl<P, C> Window<P, C> @@ -179,4 +188,136 @@ where Size::new(size.width, size.height) } + + pub fn request_redraw(&mut self, redraw_request: RedrawRequest) { + match redraw_request { + RedrawRequest::NextFrame => { + self.raw.request_redraw(); + self.redraw_at = None; + } + RedrawRequest::At(at) => { + self.redraw_at = Some(at); + } + RedrawRequest::Wait => {} + } + } + + pub fn request_input_method(&mut self, input_method: InputMethod) { + self.raw.set_ime_allowed(match input_method { + InputMethod::Disabled => false, + InputMethod::Allowed | InputMethod::Open { .. } => true, + }); + + if let InputMethod::Open { + position, + purpose, + preedit, + } = input_method + { + self.raw.set_ime_cursor_area( + LogicalPosition::new(position.x, position.y), + LogicalSize::new(10, 10), + ); + + self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); + + if let Some(content) = preedit { + if let Some(preedit) = &mut self.preedit { + preedit.update(&content, &self.renderer); + } else { + let mut preedit = Preedit::new(); + preedit.update(&content, &self.renderer); + + self.preedit = Some(preedit); + } + } + } else { + self.preedit = None; + } + } + + pub fn draw_preedit(&mut self) { + if let Some(preedit) = &self.preedit { + preedit.draw( + &mut self.renderer, + self.state.text_color(), + self.state.background_color(), + ); + } + } +} + +struct Preedit<Renderer> +where + Renderer: text::Renderer, +{ + position: Point, + content: text::paragraph::Plain<Renderer::Paragraph>, +} + +impl<Renderer> Preedit<Renderer> +where + Renderer: text::Renderer, +{ + fn new() -> Self { + Self { + position: Point::ORIGIN, + content: text::paragraph::Plain::default(), + } + } + + fn update(&mut self, text: &str, renderer: &Renderer) { + self.content.update(Text { + content: text, + bounds: Size::INFINITY, + size: renderer.default_size(), + line_height: text::LineHeight::default(), + font: renderer.default_font(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, //Bottom, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::None, + }); + } + + fn draw(&self, renderer: &mut Renderer, color: Color, background: Color) { + if self.content.min_width() < 1.0 { + return; + } + + let top_left = + self.position - Vector::new(0.0, self.content.min_height()); + + let bounds = Rectangle::new(top_left, self.content.min_bounds()); + + renderer.with_layer(bounds, |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds, + ..Default::default() + }, + background, + ); + + renderer.fill_paragraph( + self.content.raw(), + top_left, + color, + bounds, + ); + + const UNDERLINE: f32 = 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: bounds.shrink(Padding { + top: bounds.height - UNDERLINE, + ..Default::default() + }), + ..Default::default() + }, + color, + ); + }); + } } |