diff --git a/core/src/event.rs b/core/src/event.rs index b6cf321ec3..323c5ffd61 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), + + /// A 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 0000000000..ceda39c389 --- /dev/null +++ b/core/src/input_method.rs @@ -0,0 +1,24 @@ +//! Listen to input method events. + +/// A input method event. +/// +/// _**Note:** This type is largely incomplete! If you need to track +/// additional events, feel free to [open an issue] and share your use case!_ +/// +/// [open an issue]: https://github.com/iced-rs/iced/issues +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Event { + // These events correspond to underlying winit ime events. + // https://docs.rs/winit/latest/winit/event/enum.Ime.html + /// the IME was enabled. + Enabled, + + /// new composing text should be set at the cursor position. + Preedit(String, Option<(usize, usize)>), + + /// text should be inserted into the editor widget. + Commit(String), + + /// the IME was disabled. + Disabled, +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 16b3aa0f27..c7c3804455 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; @@ -72,6 +73,7 @@ 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 12ebbaa89a..d2c1b9ecf7 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -1,6 +1,15 @@ -use crate::event; use crate::time::Instant; 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, +} /// A connection to the state of a shell. /// @@ -15,6 +24,7 @@ pub struct Shell<'a, Message> { redraw_request: Option, is_layout_invalid: bool, are_widgets_invalid: bool, + caret_info: Option, } impl<'a, Message> Shell<'a, Message> { @@ -26,6 +36,7 @@ impl<'a, Message> Shell<'a, Message> { redraw_request: None, is_layout_invalid: false, are_widgets_invalid: false, + caret_info: None, } } @@ -80,6 +91,16 @@ impl<'a, Message> Shell<'a, Message> { self.redraw_request } + /// TODO + pub fn update_caret_info(&mut self, caret_info: Option) { + self.caret_info = caret_info.or(self.caret_info); + } + + /// TODO + pub fn caret_info(&self) -> Option { + self.caret_info + } + /// Returns whether the current layout is invalid or not. pub fn is_layout_invalid(&self) -> bool { self.is_layout_invalid @@ -130,6 +151,8 @@ impl<'a, Message> Shell<'a, Message> { ); } + self.update_caret_info(other.caret_info()); + self.is_layout_invalid = self.is_layout_invalid || other.is_layout_invalid; diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index b2826f7114..6d3360d0c1 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -5,7 +5,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget; use crate::core::window; -use crate::core::{Clipboard, Element, Layout, Rectangle, Shell, Size, Vector}; +use crate::core::{ + CaretInfo, Clipboard, Element, Layout, Rectangle, Shell, Size, Vector, +}; use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -187,6 +189,7 @@ where let mut outdated = false; let mut redraw_request = None; + let mut caret_info = None; let mut manual_overlay = ManuallyDrop::new( self.root @@ -230,6 +233,7 @@ where } _ => {} } + caret_info = caret_info.or(shell.caret_info()); if shell.is_layout_invalid() { let _ = ManuallyDrop::into_inner(manual_overlay); @@ -332,6 +336,7 @@ where } _ => {} } + caret_info = caret_info.or(shell.caret_info()); shell.revalidate_layout(|| { self.base = self.root.as_widget().layout( @@ -355,7 +360,10 @@ where if outdated { State::Outdated } else { - State::Updated { redraw_request } + State::Updated { + redraw_request, + caret_info, + } }, event_statuses, ) @@ -646,5 +654,7 @@ pub enum State { Updated { /// The [`window::RedrawRequest`] when a redraw should be performed. redraw_request: Option, + /// TODO + caret_info: Option, }, } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 500d2bec56..d7c7c92244 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 15b8b62ed3..b9fbde58d3 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 312aee2968..45be2fac98 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,18 @@ 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 f1ec589b04..2931e7f616 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 57ebe46a12..f2756b5be1 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 diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 462be65b03..a289f0609f 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -2,6 +2,7 @@ //! //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.13/runtime +use crate::core::input_method; use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; @@ -283,6 +284,16 @@ 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), + Ime::Commit(s) => input_method::Event::Commit(s), + Ime::Disabled => input_method::Event::Disabled, + })) + } WindowEvent::Focused(focused) => Some(Event::Window(if focused { window::Event::Focused } else { diff --git a/winit/src/program.rs b/winit/src/program.rs index d84362121b..688d6731b6 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -3,6 +3,8 @@ mod state; mod window_manager; pub use state::State; +use winit::dpi::LogicalPosition; +use winit::dpi::LogicalSize; use crate::conversion; use crate::core; @@ -579,6 +581,8 @@ async fn run_instance( let mut clipboard = Clipboard::unconnected(); let mut compositor_receiver: Option> = None; + let mut preedit = Preedit::

::new(); + debug.startup_finished(); loop { @@ -873,17 +877,27 @@ async fn run_instance( }); if let user_interface::State::Updated { - redraw_request: Some(redraw_request), + redraw_request, + caret_info, } = ui_state { match redraw_request { - window::RedrawRequest::NextFrame => { + Some(window::RedrawRequest::NextFrame) => { window.raw.request_redraw(); window.redraw_at = None; } - window::RedrawRequest::At(at) => { + 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, + ); } } @@ -1032,24 +1046,37 @@ async fn run_instance( window.raw.request_redraw(); match ui_state { - #[cfg(not( - feature = "unconditional-rendering" - ))] user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - window.raw.request_redraw(); - window.redraw_at = None; + redraw_request: _redraw_request, + caret_info, + } => { + #[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::RedrawRequest::At(at) => { - window.redraw_at = Some(at); + + if let Some(caret_info) = caret_info { + update_input_method( + window, + &mut preedit, + &caret_info, + ); } - }, + } user_interface::State::Outdated => { uis_stale = true; } - user_interface::State::Updated { .. } => {} } for (event, status) in window_events @@ -1138,6 +1165,111 @@ async fn run_instance( let _ = ManuallyDrop::into_inner(user_interfaces); } +fn update_input_method( + window: &mut crate::program::window_manager::Window, + preedit: &mut Preedit

, + caret_info: &crate::core::CaretInfo, +) where + P: Program, + C: Compositor + '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 { + content: Option<::Paragraph>, +} + +impl Preedit

{ + 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( + ::Paragraph::with_text( + core::Text::<&str, ::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 e883d04acd..2b5710ac9d 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::{Touch, WindowEvent}; +use winit::event::{Ime, Touch, WindowEvent}; use winit::window::Window; use std::fmt::{Debug, Formatter}; @@ -22,6 +22,7 @@ where modifiers: winit::keyboard::ModifiersState, theme: P::Theme, style: theme::Style, + preedit: String, } impl Debug for State

@@ -73,6 +74,7 @@ where modifiers: winit::keyboard::ModifiersState::default(), theme, style, + preedit: String::default(), } } @@ -136,6 +138,11 @@ 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, @@ -179,6 +186,11 @@ where WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = new_modifiers.state(); } + WindowEvent::Ime(ime) => { + if let Ime::Preedit(text, _) = ime { + self.preedit = text.clone(); + } + } #[cfg(feature = "debug")] WindowEvent::KeyboardInput { event: