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 --- core/src/event.rs | 4 ++ core/src/input_method.rs | 24 +++++++ core/src/lib.rs | 2 + core/src/shell.rs | 25 ++++++- runtime/src/user_interface.rs | 14 +++- 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 ++++++++++++++++++++++++- winit/src/conversion.rs | 11 +++ winit/src/program.rs | 162 ++++++++++++++++++++++++++++++++++++++---- winit/src/program/state.rs | 14 +++- 13 files changed, 420 insertions(+), 27 deletions(-) create mode 100644 core/src/input_method.rs diff --git a/core/src/event.rs b/core/src/event.rs index b6cf321e..323c5ffd 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 00000000..ceda39c3 --- /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 16b3aa0f..c7c38044 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 12ebbaa8..d2c1b9ec 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 b2826f71..6d3360d0 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 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 diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 462be65b..a289f060 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 d8436212..688d6731 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 e883d04a..2b5710ac 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: -- 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 ++++++++++--------------- winit/src/program/state.rs | 6 ++---- 3 files changed, 17 insertions(+), 23 deletions(-) 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) => { diff --git a/winit/src/program/state.rs b/winit/src/program/state.rs index 2b5710ac..6361d1ba 100644 --- a/winit/src/program/state.rs +++ b/winit/src/program/state.rs @@ -186,10 +186,8 @@ where WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = new_modifiers.state(); } - WindowEvent::Ime(ime) => { - if let Ime::Preedit(text, _) = ime { - self.preedit = text.clone(); - } + WindowEvent::Ime(Ime::Preedit(text, _)) => { + self.preedit = text.clone(); } #[cfg(feature = "debug")] WindowEvent::KeyboardInput { -- cgit From d5ee9c27955e6dfeb645e2641f3d24b006685484 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 2 Feb 2025 17:55:16 +0100 Subject: Copy `winit` docs for `input_method::Event` --- core/src/input_method.rs | 76 ++++++++++++++++++++++++++++++++++++++++-------- winit/src/conversion.rs | 5 +++- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/core/src/input_method.rs b/core/src/input_method.rs index ceda39c3..d282d404 100644 --- a/core/src/input_method.rs +++ b/core/src/input_method.rs @@ -1,24 +1,76 @@ //! Listen to input method events. +use std::ops::Range; -/// A input method event. +/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events. /// -/// _**Note:** This type is largely incomplete! If you need to track -/// additional events, feel free to [open an issue] and share your use case!_ +/// This is also called a "composition event". /// -/// [open an issue]: https://github.com/iced-rs/iced/issues -#[derive(Debug, Clone, PartialEq, Eq)] +/// 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. +/// +/// A prominent example of this is accents - many keyboard layouts allow you to first click the +/// "accent key", and then the character you want to apply the accent to. In this case, some +/// platforms will generate the following event sequence: +/// +/// ```ignore +/// // Press "`" key +/// Ime::Preedit("`", Some((0, 0))) +/// // Press "E" key +/// Ime::Preedit("", None) // Synthetic event generated by winit 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`].) +/// +/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the +/// following event sequence could be obtained: +/// +/// ```ignore +/// // Press "A" key +/// Ime::Preedit("a", Some((1, 1))) +/// // Press "B" key +/// Ime::Preedit("a b", Some((3, 3))) +/// // Press left arrow key +/// Ime::Preedit("a b", Some((1, 1))) +/// // Press space key +/// Ime::Preedit("啊b", Some((3, 3))) +/// // Press space key +/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// Ime::Commit("啊不") +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] 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. + /// Notifies when the IME was enabled. + /// + /// 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, - /// new composing text should be set at the cursor position. - Preedit(String, Option<(usize, usize)>), + /// Notifies when a new composing text should be set at the cursor position. + /// + /// The value represents a pair of the preedit string and the cursor begin position and end + /// position. When it's `None`, the cursor should be hidden. When `String` is an empty string + /// this indicates that preedit was cleared. + /// + /// The cursor range is byte-wise indexed. + Preedit(String, Option>), - /// text should be inserted into the editor widget. + /// Notifies when text should be inserted into the editor widget. + /// + /// Right before this event winit will send empty [`Self::Preedit`] event. Commit(String), - /// the IME was disabled. + /// 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 + /// pending preedit text. Disabled, } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index a289f060..c7f9aaaf 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -289,7 +289,10 @@ pub fn window_event( 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::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, })) -- 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 --- core/src/input_method.rs | 129 +++++++++++++++++++++++++---- core/src/lib.rs | 2 +- core/src/shell.rs | 78 ++++++++---------- core/src/text/paragraph.rs | 6 ++ core/src/window/redraw_request.rs | 12 +++ runtime/src/user_interface.rs | 43 +++------- 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 +++++++++++++++++++--------------- winit/src/conversion.rs | 36 +++++---- winit/src/program.rs | 156 ++---------------------------------- winit/src/program/state.rs | 12 +-- 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 { + /// 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, + }, +} + +/// 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::::Allowed); + /// assert_eq!(ime, InputMethod::Allowed); + /// + /// ime.merge(&InputMethod::::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>(&mut self, other: &InputMethod) { + 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, event_status: event::Status, - redraw_request: Option, + redraw_request: window::RedrawRequest, + input_method: InputMethod, is_layout_invalid: bool, are_widgets_invalid: bool, - caret_info: Option, } 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, + ) { + 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 { + pub fn redraw_request(&self) -> window::RedrawRequest { self.redraw_request } - /// TODO - pub fn update_caret_info(&mut self, caret_info: Option) { - self.caret_info = caret_info.or(self.caret_info); + /// Requests the current [`InputMethod`] strategy. + pub fn request_input_method>( + &mut self, + ime: &InputMethod, + ) { + self.input_method.merge(ime); } - /// TODO - pub fn caret_info(&self) -> Option { - 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(&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 Plain

{ 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 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, - /// TODO - caret_info: Option, + /// 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_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(); 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 { + 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 { @@ -1183,6 +1180,17 @@ pub fn icon(icon: window::Icon) -> Option { 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( let mut clipboard = Clipboard::unconnected(); let mut compositor_receiver: Option> = None; - let mut preedit = Preedit::

::new(); - debug.startup_finished(); loop { @@ -878,29 +874,15 @@ async fn run_instance( 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( 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( 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 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 Debug for State

@@ -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, + preedit: Option>, } impl Window @@ -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 +where + Renderer: text::Renderer, +{ + position: Point, + content: text::paragraph::Plain, +} + +impl Preedit +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, + ); + }); + } } -- cgit From db990b77e4aa8d001c774703301342c951d6caaa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 2 Feb 2025 21:06:50 +0100 Subject: Add neutral `None` variant to `InputMethod` --- core/src/input_method.rs | 13 ++++++++++--- core/src/shell.rs | 2 +- runtime/src/user_interface.rs | 2 +- winit/src/program/window_manager.rs | 11 +++++++---- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/core/src/input_method.rs b/core/src/input_method.rs index c293582d..b25f29aa 100644 --- a/core/src/input_method.rs +++ b/core/src/input_method.rs @@ -6,6 +6,8 @@ use std::ops::Range; /// The input method strategy of a widget. #[derive(Debug, Clone, PartialEq)] pub enum InputMethod { + /// No input method strategy has been specified. + None, /// No input method is allowed. Disabled, /// Input methods are allowed, but not open yet. @@ -73,7 +75,7 @@ impl InputMethod { /// ``` pub fn merge>(&mut self, other: &InputMethod) { match other { - InputMethod::Disabled => {} + InputMethod::None => {} InputMethod::Open { position, purpose, @@ -88,10 +90,15 @@ impl InputMethod { .map(str::to_owned), }; } - InputMethod::Allowed if matches!(self, Self::Disabled) => { + InputMethod::Allowed + if matches!(self, Self::None | Self::Disabled) => + { *self = Self::Allowed; } - InputMethod::Allowed => {} + InputMethod::Disabled if matches!(self, Self::None) => { + *self = Self::Disabled; + } + _ => {} } } } diff --git a/core/src/shell.rs b/core/src/shell.rs index e87d1696..d01233c7 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -27,7 +27,7 @@ impl<'a, Message> Shell<'a, Message> { redraw_request: window::RedrawRequest::Wait, is_layout_invalid: false, are_widgets_invalid: false, - input_method: InputMethod::Disabled, + input_method: InputMethod::None, } } diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 59e497c5..4bb7bba7 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -189,7 +189,7 @@ where let mut outdated = false; let mut redraw_request = window::RedrawRequest::Wait; - let mut input_method = InputMethod::Disabled; + let mut input_method = InputMethod::None; let mut manual_overlay = ManuallyDrop::new( self.root diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index cd49a8b4..32faa059 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -203,10 +203,13 @@ where } 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, - }); + match input_method { + InputMethod::None => {} + InputMethod::Disabled => self.raw.set_ime_allowed(false), + InputMethod::Allowed | InputMethod::Open { .. } => { + self.raw.set_ime_allowed(true) + } + } if let InputMethod::Open { position, -- 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 +++++++------------ winit/src/program/window_manager.rs | 6 ++++-- 3 files changed, 17 insertions(+), 23 deletions(-) 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(); diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 32faa059..08d19d16 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -205,9 +205,11 @@ where pub fn request_input_method(&mut self, input_method: InputMethod) { match input_method { InputMethod::None => {} - InputMethod::Disabled => self.raw.set_ime_allowed(false), + InputMethod::Disabled => { + self.raw.set_ime_allowed(false); + } InputMethod::Allowed | InputMethod::Open { .. } => { - self.raw.set_ime_allowed(true) + self.raw.set_ime_allowed(true); } } -- 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 ++-- winit/src/program/window_manager.rs | 46 ++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 14 deletions(-) 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 { diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 08d19d16..35a8d7dc 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -221,17 +221,19 @@ where { self.raw.set_ime_cursor_area( LogicalPosition::new(position.x, position.y), - LogicalSize::new(10, 10), + LogicalSize::new(10, 10), // TODO? ); 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); + if content.is_empty() { + self.preedit = None; + } else if let Some(preedit) = &mut self.preedit { + preedit.update(position, &content, &self.renderer); } else { let mut preedit = Preedit::new(); - preedit.update(&content, &self.renderer); + preedit.update(position, &content, &self.renderer); self.preedit = Some(preedit); } @@ -247,6 +249,10 @@ where &mut self.renderer, self.state.text_color(), self.state.background_color(), + &Rectangle::new( + Point::ORIGIN, + self.state.viewport().logical_size(), + ), ); } } @@ -271,7 +277,9 @@ where } } - fn update(&mut self, text: &str, renderer: &Renderer) { + fn update(&mut self, position: Point, text: &str, renderer: &Renderer) { + self.position = position; + self.content.update(Text { content: text, bounds: Size::INFINITY, @@ -279,21 +287,37 @@ where line_height: text::LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, //Bottom, + vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, }); } - fn draw(&self, renderer: &mut Renderer, color: Color, background: Color) { + fn draw( + &self, + renderer: &mut Renderer, + color: Color, + background: Color, + viewport: &Rectangle, + ) { if self.content.min_width() < 1.0 { return; } - let top_left = - self.position - Vector::new(0.0, self.content.min_height()); + let mut bounds = Rectangle::new( + self.position - Vector::new(0.0, self.content.min_height()), + self.content.min_bounds(), + ); + + bounds.x = bounds + .x + .max(viewport.x) + .min(viewport.x + viewport.width - bounds.width); - let bounds = Rectangle::new(top_left, self.content.min_bounds()); + bounds.y = bounds + .y + .max(viewport.y) + .min(viewport.y + viewport.height - bounds.height); renderer.with_layer(bounds, |renderer| { renderer.fill_quad( @@ -306,7 +330,7 @@ where renderer.fill_paragraph( self.content.raw(), - top_left, + bounds.position(), color, bounds, ); -- 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 --- core/src/input_method.rs | 47 ++++++++++++-- core/src/text.rs | 17 +++++ widget/src/text_editor.rs | 27 +++++--- widget/src/text_input.rs | 13 ++-- winit/src/program/window_manager.rs | 120 ++++++++++++++++++++++++++++-------- 5 files changed, 181 insertions(+), 43 deletions(-) diff --git a/core/src/input_method.rs b/core/src/input_method.rs index b25f29aa..f10a1c3b 100644 --- a/core/src/input_method.rs +++ b/core/src/input_method.rs @@ -23,10 +23,50 @@ pub enum InputMethod { /// Ideally, your widget will show pre-edits on-the-spot; but, since that can /// be tricky, you can instead provide the current pre-edit here and the /// runtime will display it as an overlay (i.e. "Over-the-spot IME"). - preedit: Option, + preedit: Option>, }, } +/// The pre-edit of an [`InputMethod`]. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Preedit { + /// The current content. + pub content: T, + /// The selected range of the content. + pub selection: Option>, +} + +impl Preedit { + /// Creates a new empty [`Preedit`]. + pub fn new() -> Self + where + T: Default, + { + Self::default() + } + + /// Turns a [`Preedit`] into its owned version. + pub fn to_owned(&self) -> Preedit + where + T: AsRef, + { + Preedit { + content: self.content.as_ref().to_owned(), + selection: self.selection.clone(), + } + } +} + +impl Preedit { + /// Borrows the contents of a [`Preedit`]. + pub fn as_ref(&self) -> Preedit<&str> { + Preedit { + content: &self.content, + selection: self.selection.clone(), + } + } +} + /// The purpose of an [`InputMethod`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Purpose { @@ -84,10 +124,7 @@ impl InputMethod { *self = Self::Open { position: *position, purpose: *purpose, - preedit: preedit - .as_ref() - .map(AsRef::as_ref) - .map(str::to_owned), + preedit: preedit.as_ref().map(Preedit::to_owned), }; } InputMethod::Allowed diff --git a/core/src/text.rs b/core/src/text.rs index c144fd24..8dde9e21 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -270,6 +270,23 @@ pub struct Span<'a, Link = (), Font = crate::Font> { pub strikethrough: bool, } +impl Default for Span<'_, Link, Font> { + fn default() -> Self { + Self { + text: Cow::default(), + size: None, + line_height: None, + font: None, + color: None, + link: None, + highlight: None, + padding: Padding::default(), + underline: false, + strikethrough: false, + } + } +} + /// A text highlight. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Highlight { 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, diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 35a8d7dc..86cee973 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -1,5 +1,6 @@ use crate::conversion; use crate::core::alignment; +use crate::core::input_method; use crate::core::mouse; use crate::core::renderer; use crate::core::text; @@ -12,11 +13,13 @@ use crate::core::{ 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; +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::sync::Arc; + #[allow(missing_debug_implementations)] pub struct WindowManager where @@ -226,16 +229,26 @@ where self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); - if let Some(content) = preedit { - if content.is_empty() { + if let Some(preedit) = preedit { + if preedit.content.is_empty() { self.preedit = None; - } else if let Some(preedit) = &mut self.preedit { - preedit.update(position, &content, &self.renderer); + } else if let Some(overlay) = &mut self.preedit { + overlay.update( + position, + &preedit, + self.state.background_color(), + &self.renderer, + ); } else { - let mut preedit = Preedit::new(); - preedit.update(position, &content, &self.renderer); - - self.preedit = Some(preedit); + let mut overlay = Preedit::new(); + overlay.update( + position, + &preedit, + self.state.background_color(), + &self.renderer, + ); + + self.preedit = Some(overlay); } } } else { @@ -263,7 +276,8 @@ where Renderer: text::Renderer, { position: Point, - content: text::paragraph::Plain, + content: Renderer::Paragraph, + spans: Vec>, } impl Preedit @@ -273,24 +287,67 @@ where fn new() -> Self { Self { position: Point::ORIGIN, - content: text::paragraph::Plain::default(), + spans: Vec::new(), + content: Renderer::Paragraph::default(), } } - fn update(&mut self, position: Point, text: &str, renderer: &Renderer) { + fn update( + &mut self, + position: Point, + preedit: &input_method::Preedit, + background: Color, + renderer: &Renderer, + ) { self.position = position; - 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, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::None, - }); + let spans = match &preedit.selection { + Some(selection) => { + vec![ + text::Span { + text: Cow::Borrowed( + &preedit.content[..selection.start], + ), + ..text::Span::default() + }, + text::Span { + text: Cow::Borrowed( + if selection.start == selection.end { + "\u{200A}" + } else { + &preedit.content[selection.start..selection.end] + }, + ), + color: Some(background), + ..text::Span::default() + }, + text::Span { + text: Cow::Borrowed(&preedit.content[selection.end..]), + ..text::Span::default() + }, + ] + } + _ => vec![text::Span { + text: Cow::Borrowed(&preedit.content), + ..text::Span::default() + }], + }; + + if spans != self.spans.as_slice() { + use text::Paragraph as _; + + self.content = Renderer::Paragraph::with_spans(Text { + content: &spans, + 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, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::None, + }); + } } fn draw( @@ -300,6 +357,8 @@ where background: Color, viewport: &Rectangle, ) { + use text::Paragraph as _; + if self.content.min_width() < 1.0 { return; } @@ -329,7 +388,7 @@ where ); renderer.fill_paragraph( - self.content.raw(), + &self.content, bounds.position(), color, bounds, @@ -347,6 +406,17 @@ where }, color, ); + + for span_bounds in self.content.span_bounds(1) { + renderer.fill_quad( + renderer::Quad { + bounds: span_bounds + + (bounds.position() - Point::ORIGIN), + ..Default::default() + }, + color, + ); + } }); } } -- cgit From c9abe25d3167bb12d935e3a095160a897dd98176 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 02:38:20 +0100 Subject: Use `text::Span::new` in `window_manager` --- core/src/text.rs | 44 +++++++++++++++---------------------- winit/src/program/window_manager.rs | 35 ++++++++--------------------- 2 files changed, 27 insertions(+), 52 deletions(-) diff --git a/core/src/text.rs b/core/src/text.rs index 8dde9e21..a7e1f281 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -270,23 +270,6 @@ pub struct Span<'a, Link = (), Font = crate::Font> { pub strikethrough: bool, } -impl Default for Span<'_, Link, Font> { - fn default() -> Self { - Self { - text: Cow::default(), - size: None, - line_height: None, - font: None, - color: None, - link: None, - highlight: None, - padding: Padding::default(), - underline: false, - strikethrough: false, - } - } -} - /// A text highlight. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Highlight { @@ -301,15 +284,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { pub fn new(fragment: impl IntoFragment<'a>) -> Self { Self { text: fragment.into_fragment(), - size: None, - line_height: None, - font: None, - color: None, - highlight: None, - link: None, - padding: Padding::ZERO, - underline: false, - strikethrough: false, + ..Self::default() } } @@ -457,6 +432,23 @@ impl<'a, Link, Font> Span<'a, Link, Font> { } } +impl Default for Span<'_, Link, Font> { + fn default() -> Self { + Self { + text: Cow::default(), + size: None, + line_height: None, + font: None, + color: None, + link: None, + highlight: None, + padding: Padding::default(), + underline: false, + strikethrough: false, + } + } +} + impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> { fn from(value: &'a str) -> Self { Span::new(value) diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 86cee973..ae214e7c 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -16,7 +16,6 @@ use crate::program::{Program, State}; use winit::dpi::{LogicalPosition, LogicalSize}; use winit::monitor::MonitorHandle; -use std::borrow::Cow; use std::collections::BTreeMap; use std::sync::Arc; @@ -304,33 +303,17 @@ where let spans = match &preedit.selection { Some(selection) => { vec![ - text::Span { - text: Cow::Borrowed( - &preedit.content[..selection.start], - ), - ..text::Span::default() - }, - text::Span { - text: Cow::Borrowed( - if selection.start == selection.end { - "\u{200A}" - } else { - &preedit.content[selection.start..selection.end] - }, - ), - color: Some(background), - ..text::Span::default() - }, - text::Span { - text: Cow::Borrowed(&preedit.content[selection.end..]), - ..text::Span::default() - }, + text::Span::new(&preedit.content[..selection.start]), + text::Span::new(if selection.start == selection.end { + "\u{200A}" + } else { + &preedit.content[selection.start..selection.end] + }) + .color(background), + text::Span::new(&preedit.content[selection.end..]), ] } - _ => vec![text::Span { - text: Cow::Borrowed(&preedit.content), - ..text::Span::default() - }], + _ => vec![text::Span::new(&preedit.content)], }; if spans != self.spans.as_slice() { -- cgit From ba755c69d648cace61f23537266f2e556ee70c15 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 03:34:41 +0100 Subject: Fulfill `InputMethod` requests only during `RedrawRequested` --- core/src/shell.rs | 3 +++ winit/src/program.rs | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/shell.rs b/core/src/shell.rs index d01233c7..a13492d5 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -78,6 +78,9 @@ impl<'a, Message> Shell<'a, Message> { } /// Requests the current [`InputMethod`] strategy. + /// + /// __Important__: This request will only be honored by the + /// [`Shell`] only during a [`window::Event::RedrawRequested`]. pub fn request_input_method>( &mut self, ime: &InputMethod, diff --git a/winit/src/program.rs b/winit/src/program.rs index 302bc6c3..7ead4c3b 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1025,19 +1025,19 @@ async fn run_instance( ); #[cfg(feature = "unconditional-rendering")] - window.raw.request_redraw(); + window.request_redraw( + window::RedrawRequest::NextFrame, + ); match ui_state { user_interface::State::Updated { redraw_request: _redraw_request, - input_method, + .. } => { #[cfg(not( feature = "unconditional-rendering" ))] window.request_redraw(_redraw_request); - - window.request_input_method(input_method); } user_interface::State::Outdated => { uis_stale = true; -- cgit From 76c25d2fb2a6d83c6dbb71a92c184209c1d36acc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 3 Feb 2025 03:36:18 +0100 Subject: Fix typo in `core::Event` documentation --- core/src/event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/event.rs b/core/src/event.rs index 323c5ffd..7f0ab914 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -25,7 +25,7 @@ pub enum Event { /// A touch event Touch(touch::Event), - /// A input method event + /// An input method event InputMethod(input_method::Event), } -- 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(-) 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 --- core/src/input_method.rs | 64 ++++++++++++++++++++++++++++++------------------ widget/src/scrollable.rs | 12 ++++++--- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/core/src/input_method.rs b/core/src/input_method.rs index f10a1c3b..4e8c383b 100644 --- a/core/src/input_method.rs +++ b/core/src/input_method.rs @@ -82,21 +82,21 @@ pub enum Purpose { } impl InputMethod { - /// Merges two [`InputMethod`] strategies, prioritizing the second one when both ready: + /// Merges two [`InputMethod`] strategies, prioritizing the first one when both open: /// ``` - /// # use iced_core::input_method::{InputMethod, Purpose}; + /// # use iced_core::input_method::{InputMethod, Purpose, Preedit}; /// # use iced_core::Point; /// /// let open = InputMethod::Open { /// position: Point::ORIGIN, /// purpose: Purpose::Normal, - /// preedit: None, + /// preedit: Some(Preedit { content: "1".to_owned(), selection: None }), /// }; /// /// let open_2 = InputMethod::Open { /// position: Point::ORIGIN, /// purpose: Purpose::Secure, - /// preedit: None, + /// preedit: Some(Preedit { content: "2".to_owned(), selection: None }), /// }; /// /// let mut ime = InputMethod::Disabled; @@ -111,31 +111,47 @@ impl InputMethod { /// assert_eq!(ime, open); /// /// ime.merge(&open_2); - /// assert_eq!(ime, open_2); + /// assert_eq!(ime, open); /// ``` pub fn merge>(&mut self, other: &InputMethod) { - match other { - InputMethod::None => {} - InputMethod::Open { + match (&self, other) { + (InputMethod::Open { .. }, _) + | ( + InputMethod::Allowed, + InputMethod::None | InputMethod::Disabled, + ) + | (InputMethod::Disabled, InputMethod::None) => {} + _ => { + *self = other.to_owned(); + } + } + } + + /// Returns true if the [`InputMethod`] is open. + pub fn is_open(&self) -> bool { + matches!(self, Self::Open { .. }) + } +} + +impl InputMethod { + /// Turns an [`InputMethod`] into its owned version. + pub fn to_owned(&self) -> InputMethod + where + T: AsRef, + { + match self { + Self::None => InputMethod::None, + Self::Disabled => InputMethod::Disabled, + Self::Allowed => InputMethod::Allowed, + Self::Open { position, purpose, preedit, - } => { - *self = Self::Open { - position: *position, - purpose: *purpose, - preedit: preedit.as_ref().map(Preedit::to_owned), - }; - } - InputMethod::Allowed - if matches!(self, Self::None | Self::Disabled) => - { - *self = Self::Allowed; - } - InputMethod::Disabled if matches!(self, Self::None) => { - *self = Self::Disabled; - } - _ => {} + } => InputMethod::Open { + position: *position, + purpose: *purpose, + preedit: preedit.as_ref().map(Preedit::to_owned), + }, } } } 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(-) 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