From 7db5256b720c3ecbe7c1cce7a1b47fd03151e03a Mon Sep 17 00:00:00 2001 From: KENZ Date: Fri, 10 Jan 2025 07:12:31 +0900 Subject: Draft `input_method` support --- widget/src/combo_box.rs | 6 ++- widget/src/lazy/component.rs | 2 + widget/src/scrollable.rs | 19 ++++++++- widget/src/text_editor.rs | 69 ++++++++++++++++++++++++++++++-- widget/src/text_input.rs | 95 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 183 insertions(+), 8 deletions(-) (limited to 'widget/src') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 500d2bec..d7c7c922 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -564,6 +564,7 @@ where } } } + shell.update_caret_info(local_shell.caret_info()); // Then finally react to them here for message in local_messages { @@ -742,6 +743,8 @@ where published_message_to_shell = true; // Unfocus the input + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); self.text_input.update( &mut tree.children[0], Event::Mouse(mouse::Event::ButtonPressed( @@ -751,9 +754,10 @@ where mouse::Cursor::Unavailable, renderer, clipboard, - &mut Shell::new(&mut vec![]), + &mut local_shell, viewport, ); + shell.update_caret_info(local_shell.caret_info()); } }); diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 15b8b62e..b9fbde58 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -355,6 +355,7 @@ where } } } + shell.update_caret_info(local_shell.caret_info()); if !local_messages.is_empty() { let mut heads = self.state.take().unwrap().into_heads(); @@ -640,6 +641,7 @@ where } } } + shell.update_caret_info(local_shell.caret_info()); if !local_messages.is_empty() { let mut inner = diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 312aee29..7df7a0e5 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -33,8 +33,9 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - self, Background, Clipboard, Color, Element, Event, Layout, Length, - Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + self, Background, CaretInfo, Clipboard, Color, Element, Event, Layout, + Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, + Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -729,6 +730,7 @@ 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(), @@ -743,6 +745,19 @@ where ..bounds }, ); + + 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 matches!( diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index f1ec589b..2931e7f6 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -33,6 +33,7 @@ //! ``` use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; +use crate::core::input_method; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; @@ -46,8 +47,8 @@ use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::window; use crate::core::{ - Background, Border, Color, Element, Event, Length, Padding, Pixels, Point, - Rectangle, Shell, Size, SmolStr, Theme, Vector, + Background, Border, CaretInfo, Color, Element, Event, Length, Padding, + Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; use std::borrow::Cow; @@ -322,6 +323,46 @@ where self.class = class.into(); self } + + fn caret_rect( + &self, + tree: &widget::Tree, + renderer: &Renderer, + layout: Layout<'_>, + ) -> Option { + let bounds = layout.bounds(); + + let internal = self.content.0.borrow_mut(); + let state = tree.state.downcast_ref::>(); + + let text_bounds = bounds.shrink(self.padding); + let translation = text_bounds.position() - Point::ORIGIN; + + if let Some(_) = state.focus.as_ref() { + 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 + } + } } /// The content of a [`TextEditor`]. @@ -605,7 +646,7 @@ where event: Event, layout: Layout<'_>, cursor: mouse::Cursor, - _renderer: &Renderer, + renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, @@ -701,6 +742,11 @@ where })); shell.capture_event(); } + Update::Commit(text) => { + shell.publish(on_edit(Action::Edit(Edit::Paste( + Arc::new(text), + )))); + } Update::Binding(binding) => { fn apply_binding< H: text::Highlighter, @@ -825,6 +871,19 @@ where } }; + shell.update_caret_info(if state.is_focused() { + let rect = self + .caret_rect(tree, renderer, layout) + .unwrap_or(Rectangle::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); } else if self @@ -1129,6 +1188,7 @@ enum Update { Drag(Point), Release, Scroll(f32), + Commit(String), Binding(Binding), } @@ -1191,6 +1251,9 @@ impl Update { } _ => None, }, + Event::InputMethod(input_method::Event::Commit(text)) => { + Some(Update::Commit(text)) + } Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 57ebe46a..f2756b5b 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -36,6 +36,7 @@ mod value; pub mod cursor; pub use cursor::Cursor; +use iced_runtime::core::input_method; pub use value::Value; use editor::Editor; @@ -56,8 +57,8 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Border, Color, Element, Event, Layout, Length, Padding, Pixels, - Point, Rectangle, Shell, Size, Theme, Vector, Widget, + Background, Border, CaretInfo, Color, Element, Event, Layout, Length, + Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -391,6 +392,58 @@ where } } + fn caret_rect( + &self, + tree: &Tree, + layout: Layout<'_>, + value: Option<&Value>, + ) -> Option { + let state = tree.state.downcast_ref::>(); + let value = value.unwrap_or(&self.value); + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + if let Some(_) = state + .is_focused + .as_ref() + .filter(|focus| focus.is_window_focused) + { + let caret_index = match state.cursor.state(value) { + cursor::State::Index(position) => position, + cursor::State::Selection { start, end } => { + let left = start.min(end); + left + } + }; + let text = state.value.raw(); + let (caret_x, 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 x = (text_bounds.x + caret_x).floor(); + Some(Rectangle { + x: (alignment_offset - offset) + x, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }) + } else { + None + } + } + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -1197,6 +1250,31 @@ where state.keyboard_modifiers = *modifiers; } + Event::InputMethod(input_method::Event::Commit(string)) => { + let state = state::(tree); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = &self.on_input else { + return; + }; + + state.is_pasting = None; + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + + editor.paste(Value::new(&string)); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + update_cache(state, &self.value); + + shell.capture_event(); + } + } Event::Window(window::Event::Unfocused) => { let state = state::(tree); @@ -1256,6 +1334,19 @@ 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::::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); } else if self -- cgit From 0c6d4eb23f07e0ab424dc22dd198924b8540192a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 2 Feb 2025 17:50:12 +0100 Subject: Run `cargo fmt` and fix lints --- widget/src/text_editor.rs | 9 +++++---- widget/src/text_input.rs | 25 ++++++++++--------------- 2 files changed, 15 insertions(+), 19 deletions(-) (limited to 'widget/src') diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 2931e7f6..529c8b90 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -338,7 +338,7 @@ where let text_bounds = bounds.shrink(self.padding); let translation = text_bounds.position() - Point::ORIGIN; - if let Some(_) = state.focus.as_ref() { + if state.focus.is_some() { let position = match internal.editor.cursor() { Cursor::Caret(position) => position, Cursor::Selection(ranges) => ranges @@ -872,10 +872,11 @@ where }; shell.update_caret_info(if state.is_focused() { - let rect = self - .caret_rect(tree, renderer, layout) - .unwrap_or(Rectangle::default()); + 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, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index f2756b5b..ba5d1843 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -407,18 +407,15 @@ where let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - if let Some(_) = state + if state .is_focused - .as_ref() - .filter(|focus| focus.is_window_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 } => { - let left = start.min(end); - left - } + cursor::State::Selection { start, end } => start.min(end), }; + let text = state.value.raw(); let (caret_x, offset) = measure_cursor_and_scroll_offset( text, @@ -433,6 +430,7 @@ where ); let x = (text_bounds.x + caret_x).floor(); + Some(Rectangle { x: (alignment_offset - offset) + x, y: text_bounds.y, @@ -1250,7 +1248,7 @@ where state.keyboard_modifiers = *modifiers; } - Event::InputMethod(input_method::Event::Commit(string)) => { + Event::InputMethod(input_method::Event::Commit(text)) => { let state = state::(tree); if let Some(focus) = &mut state.is_focused { @@ -1258,21 +1256,18 @@ where return; }; - state.is_pasting = None; - let mut editor = Editor::new(&mut self.value, &mut state.cursor); + editor.paste(Value::new(text)); - editor.paste(Value::new(&string)); + focus.updated_at = Instant::now(); + state.is_pasting = None; let message = (on_input)(editor.contents()); shell.publish(message); - - focus.updated_at = Instant::now(); + shell.capture_event(); update_cache(state, &self.value); - - shell.capture_event(); } } Event::Window(window::Event::Unfocused) => { -- cgit From ae10adda74320e8098bfeb401f12a278e1e7b3e2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 2 Feb 2025 20:45:29 +0100 Subject: Refactor and simplify `input_method` API --- widget/src/action.rs | 14 ++-- widget/src/canvas.rs | 15 +---- widget/src/combo_box.rs | 16 +---- widget/src/lazy/component.rs | 29 ++------- widget/src/pane_grid.rs | 2 +- widget/src/scrollable.rs | 18 ++---- widget/src/shader.rs | 14 +--- widget/src/text_editor.rs | 133 ++++++++++++++++++++++---------------- widget/src/text_input.rs | 148 ++++++++++++++++++++++++------------------- 9 files changed, 186 insertions(+), 203 deletions(-) (limited to 'widget/src') 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_to_publish: Option, - redraw_request: Option, + redraw_request: window::RedrawRequest, event_status: event::Status, } @@ -14,7 +14,7 @@ impl Action { 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 Action { /// 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 Action { /// 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 Action { /// widget implementations. pub fn into_inner( self, - ) -> ( - Option, - Option, - event::Status, - ) { + ) -> (Option, 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, renderer: &Renderer, layout: Layout<'_>, - ) -> Option { - 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::>(); 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_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, } 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 { Drag(Point), Release, Scroll(f32), - Commit(String), + InputMethod(Ime), Binding(Binding), } +enum Ime { + Toggle(bool), + Preedit(String), + Commit(String), +} + impl Update { fn from_event( event: Event, @@ -1252,9 +1268,20 @@ impl Update { } _ => 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, layout: Layout<'_>, - value: Option<&Value>, - ) -> Option { - let state = tree.state.downcast_ref::>(); - 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::(tree); + Event::InputMethod(event) => match event { + input_method::Event::Opened | input_method::Event::Closed => { + let state = state::(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::(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::(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::(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::::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( tree.state.downcast_mut::>() } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, is_window_focused: bool, + is_ime_open: Option, } impl State

{ @@ -1548,6 +1565,7 @@ impl State

{ updated_at: now, now, is_window_focused: true, + is_ime_open: None, }); self.move_cursor_to_end(); -- cgit From d28af5739bfaafa141dc8071a0c910e8693f3b3c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 00:51:57 +0100 Subject: Track pre-edits separately from focus in text inputs --- widget/src/text_editor.rs | 15 ++++++--------- widget/src/text_input.rs | 19 +++++++------------ 2 files changed, 13 insertions(+), 21 deletions(-) (limited to 'widget/src') diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 4f985f28..72e15c28 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -332,14 +332,13 @@ where ) -> 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 { + let Some(preedit) = &state.preedit else { return InputMethod::Allowed; }; @@ -497,6 +496,7 @@ where #[derive(Debug)] pub struct State { focus: Option, + preedit: Option, last_click: Option, drag_click: Option, partial_scroll: f32, @@ -510,7 +510,6 @@ struct Focus { updated_at: Instant, now: Instant, is_window_focused: bool, - is_ime_open: Option, } impl Focus { @@ -523,7 +522,6 @@ impl Focus { updated_at: now, now, is_window_focused: true, - is_ime_open: None, } } @@ -573,6 +571,7 @@ where fn state(&self) -> widget::tree::State { widget::tree::State::new(State { focus: None, + preedit: None, last_click: None, drag_click: None, partial_scroll: 0.0, @@ -752,13 +751,11 @@ where } 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); - } + state.preedit = is_open.then(String::new); } Ime::Preedit(text) => { - if let Some(focus) = &mut state.focus { - focus.is_ime_open = Some(text); + if state.focus.is_some() { + state.preedit = Some(text); } } Ime::Commit(text) => { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index d0e93927..58bbc0d6 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -400,14 +400,13 @@ where ) -> 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 { + let Some(preedit) = &state.is_ime_open else { return InputMethod::Allowed; }; @@ -729,7 +728,6 @@ where updated_at: now, now, is_window_focused: true, - is_ime_open: None, }) } else { None @@ -1257,17 +1255,15 @@ where input_method::Event::Opened | input_method::Event::Closed => { let state = state::(tree); - if let Some(focus) = &mut state.is_focused { - focus.is_ime_open = - matches!(event, input_method::Event::Opened) - .then(String::new); - } + state.is_ime_open = + matches!(event, input_method::Event::Opened) + .then(String::new); } input_method::Event::Preedit(content, _range) => { let state = state::(tree); - if let Some(focus) = &mut state.is_focused { - focus.is_ime_open = Some(content.to_owned()); + if state.is_focused.is_some() { + state.is_ime_open = Some(content.to_owned()); } } input_method::Event::Commit(text) => { @@ -1519,6 +1515,7 @@ pub struct State { placeholder: paragraph::Plain

, icon: paragraph::Plain

, is_focused: Option, + is_ime_open: Option, is_dragging: bool, is_pasting: Option, last_click: Option, @@ -1538,7 +1535,6 @@ struct Focus { updated_at: Instant, now: Instant, is_window_focused: bool, - is_ime_open: Option, } impl State

{ @@ -1565,7 +1561,6 @@ impl State

{ updated_at: now, now, is_window_focused: true, - is_ime_open: None, }); self.move_cursor_to_end(); -- cgit From 3a35fd6249eeb324379d3a14b020ccc48ec16fb4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 01:30:41 +0100 Subject: Clamp pre-edit inside viewport bounds --- widget/src/text_input.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'widget/src') diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 58bbc0d6..a1a1d3b5 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -413,8 +413,7 @@ where let secure_value = self.is_secure.then(|| value.secure()); let value = secure_value.as_ref().unwrap_or(value); - let mut children_layout = layout.children(); - let text_bounds = children_layout.next().unwrap().bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); let caret_index = match state.cursor.state(value) { cursor::State::Index(position) => position, @@ -435,7 +434,7 @@ where + alignment_offset; InputMethod::Open { - position: Point::new(x, text_bounds.y), + position: Point::new(x, text_bounds.y + text_bounds.height), purpose: if self.is_secure { input_method::Purpose::Secure } else { -- cgit From c83809adb907498ba2a573ec9fb50936601ac8fc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 02:33:40 +0100 Subject: Implement basic IME selection in `Preedit` overlay --- widget/src/text_editor.rs | 27 +++++++++++++++++++-------- widget/src/text_input.rs | 13 ++++++++----- 2 files changed, 27 insertions(+), 13 deletions(-) (limited to 'widget/src') diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 72e15c28..26d05ccd 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -55,6 +55,7 @@ use std::borrow::Cow; use std::cell::RefCell; use std::fmt; use std::ops::DerefMut; +use std::ops::Range; use std::sync::Arc; pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; @@ -365,7 +366,7 @@ where InputMethod::Open { position, purpose: input_method::Purpose::Normal, - preedit: Some(preedit), + preedit: Some(preedit.as_ref()), } } } @@ -496,7 +497,7 @@ where #[derive(Debug)] pub struct State { focus: Option, - preedit: Option, + preedit: Option, last_click: Option, drag_click: Option, partial_scroll: f32, @@ -751,11 +752,15 @@ where } Update::InputMethod(update) => match update { Ime::Toggle(is_open) => { - state.preedit = is_open.then(String::new); + state.preedit = + is_open.then(input_method::Preedit::new); } - Ime::Preedit(text) => { + Ime::Preedit { content, selection } => { if state.focus.is_some() { - state.preedit = Some(text); + state.preedit = Some(input_method::Preedit { + content, + selection, + }); } } Ime::Commit(text) => { @@ -1202,7 +1207,10 @@ enum Update { enum Ime { Toggle(bool), - Preedit(String), + Preedit { + content: String, + selection: Option>, + }, Commit(String), } @@ -1272,8 +1280,11 @@ impl Update { input_method::Event::Opened )))) } - input_method::Event::Preedit(content, _range) => { - Some(Update::InputMethod(Ime::Preedit(content))) + input_method::Event::Preedit(content, selection) => { + Some(Update::InputMethod(Ime::Preedit { + content, + selection, + })) } input_method::Event::Commit(content) => { Some(Update::InputMethod(Ime::Commit(content))) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index a1a1d3b5..4c9e46c1 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -440,7 +440,7 @@ where } else { input_method::Purpose::Normal }, - preedit: Some(preedit), + preedit: Some(preedit.as_ref()), } } @@ -1256,13 +1256,16 @@ where state.is_ime_open = matches!(event, input_method::Event::Opened) - .then(String::new); + .then(input_method::Preedit::new); } - input_method::Event::Preedit(content, _range) => { + input_method::Event::Preedit(content, selection) => { let state = state::(tree); if state.is_focused.is_some() { - state.is_ime_open = Some(content.to_owned()); + state.is_ime_open = Some(input_method::Preedit { + content: content.to_owned(), + selection: selection.clone(), + }); } } input_method::Event::Commit(text) => { @@ -1514,7 +1517,7 @@ pub struct State { placeholder: paragraph::Plain

, icon: paragraph::Plain

, is_focused: Option, - is_ime_open: Option, + is_ime_open: Option, is_dragging: bool, is_pasting: Option, last_click: Option, -- cgit From e8c680ce66b6b766a196e799b209e73e0bf416ab Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 16:55:10 +0100 Subject: Request redraws on `InputMethod` events --- widget/src/text_editor.rs | 4 ++++ widget/src/text_input.rs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 26d05ccd..486741c6 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -754,6 +754,8 @@ where Ime::Toggle(is_open) => { state.preedit = is_open.then(input_method::Preedit::new); + + shell.request_redraw(); } Ime::Preedit { content, selection } => { if state.focus.is_some() { @@ -761,6 +763,8 @@ where content, selection, }); + + shell.request_redraw(); } } Ime::Commit(text) => { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 4c9e46c1..b22ee1ca 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -36,13 +36,13 @@ mod value; pub mod cursor; pub use cursor::Cursor; -use iced_runtime::core::input_method; pub use value::Value; use editor::Editor; use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; +use crate::core::input_method; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout; @@ -1257,6 +1257,8 @@ where state.is_ime_open = matches!(event, input_method::Event::Opened) .then(input_method::Preedit::new); + + shell.request_redraw(); } input_method::Event::Preedit(content, selection) => { let state = state::(tree); @@ -1266,6 +1268,8 @@ where content: content.to_owned(), selection: selection.clone(), }); + + shell.request_redraw(); } } input_method::Event::Commit(text) => { -- cgit From 141290c7402a4e087ce18d60b210f4feeafcebee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 17:12:08 +0100 Subject: Fix `InputMethod` conflicts with multiple scrollables --- widget/src/scrollable.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 0a93584e..053b7df6 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -727,6 +727,8 @@ where _ => mouse::Cursor::Unavailable, }; + let had_input_method = shell.input_method().is_open(); + let translation = state.translation(self.direction, bounds, content_bounds); @@ -745,10 +747,12 @@ where }, ); - if let InputMethod::Open { position, .. } = - shell.input_method_mut() - { - *position = *position + translation; + if !had_input_method { + if let InputMethod::Open { position, .. } = + shell.input_method_mut() + { + *position = *position + translation; + } } }; -- cgit From bab18858cd60168b63ae442026f45a90eb6be731 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 18:38:20 +0100 Subject: Handle pre-edits and commits only if `text_editor` is focused --- widget/src/text_editor.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'widget/src') diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 486741c6..cfdf6b5d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -758,14 +758,10 @@ where shell.request_redraw(); } Ime::Preedit { content, selection } => { - if state.focus.is_some() { - state.preedit = Some(input_method::Preedit { - content, - selection, - }); + state.preedit = + Some(input_method::Preedit { content, selection }); - shell.request_redraw(); - } + shell.request_redraw(); } Ime::Commit(text) => { shell.publish(on_edit(Action::Edit(Edit::Paste( @@ -1284,15 +1280,20 @@ impl Update { input_method::Event::Opened )))) } - input_method::Event::Preedit(content, selection) => { + input_method::Event::Preedit(content, selection) + if state.focus.is_some() => + { Some(Update::InputMethod(Ime::Preedit { content, selection, })) } - input_method::Event::Commit(content) => { + input_method::Event::Commit(content) + if state.focus.is_some() => + { Some(Update::InputMethod(Ime::Commit(content))) } + _ => None, }, Event::Keyboard(keyboard::Event::KeyPressed { key, -- cgit