diff options
Diffstat (limited to 'widget')
-rw-r--r-- | widget/src/action.rs | 14 | ||||
-rw-r--r-- | widget/src/canvas.rs | 15 | ||||
-rw-r--r-- | widget/src/combo_box.rs | 18 | ||||
-rw-r--r-- | widget/src/lazy/component.rs | 27 | ||||
-rw-r--r-- | widget/src/pane_grid.rs | 2 | ||||
-rw-r--r-- | widget/src/scrollable.rs | 15 | ||||
-rw-r--r-- | widget/src/shader.rs | 14 | ||||
-rw-r--r-- | widget/src/text_editor.rs | 111 | ||||
-rw-r--r-- | widget/src/text_input.rs | 111 |
9 files changed, 249 insertions, 78 deletions
diff --git a/widget/src/action.rs b/widget/src/action.rs index 1dd3a787..cc31e76a 100644 --- a/widget/src/action.rs +++ b/widget/src/action.rs @@ -6,7 +6,7 @@ use crate::core::window; #[derive(Debug, Clone)] pub struct Action<Message> { message_to_publish: Option<Message>, - redraw_request: Option<window::RedrawRequest>, + redraw_request: window::RedrawRequest, event_status: event::Status, } @@ -14,7 +14,7 @@ impl<Message> Action<Message> { fn new() -> Self { Self { message_to_publish: None, - redraw_request: None, + redraw_request: window::RedrawRequest::Wait, event_status: event::Status::Ignored, } } @@ -46,7 +46,7 @@ impl<Message> Action<Message> { /// soon as possible; without publishing any `Message`. pub fn request_redraw() -> Self { Self { - redraw_request: Some(window::RedrawRequest::NextFrame), + redraw_request: window::RedrawRequest::NextFrame, ..Self::new() } } @@ -58,7 +58,7 @@ impl<Message> Action<Message> { /// blinking caret on a text input. pub fn request_redraw_at(at: Instant) -> Self { Self { - redraw_request: Some(window::RedrawRequest::At(at)), + redraw_request: window::RedrawRequest::At(at), ..Self::new() } } @@ -75,11 +75,7 @@ impl<Message> Action<Message> { /// widget implementations. pub fn into_inner( self, - ) -> ( - Option<Message>, - Option<window::RedrawRequest>, - event::Status, - ) { + ) -> (Option<Message>, window::RedrawRequest, event::Status) { ( self.message_to_publish, self.redraw_request, diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 23cc3f2b..d10771f0 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -238,27 +238,18 @@ where { let (message, redraw_request, event_status) = action.into_inner(); + shell.request_redraw_at(redraw_request); + if let Some(message) = message { shell.publish(message); } - if let Some(redraw_request) = redraw_request { - match redraw_request { - window::RedrawRequest::NextFrame => { - shell.request_redraw(); - } - window::RedrawRequest::At(at) => { - shell.request_redraw_at(at); - } - } - } - if event_status == event::Status::Captured { shell.capture_event(); } } - if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) { + if shell.redraw_request() != window::RedrawRequest::NextFrame { let mouse_interaction = self .mouse_interaction(tree, layout, cursor, viewport, renderer); diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 500d2bec..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,16 +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.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 { @@ -742,6 +733,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 +744,10 @@ where mouse::Cursor::Unavailable, renderer, clipboard, - &mut Shell::new(&mut vec![]), + &mut local_shell, viewport, ); + shell.request_input_method(local_shell.input_method()); } }); diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 15b8b62e..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,17 +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.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(); @@ -629,17 +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.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 2dbe0479..a51a8625 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, Clipboard, Color, Element, Event, InputMethod, Layout, + Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, + Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -728,6 +729,8 @@ where _ => mouse::Cursor::Unavailable, }; + let had_input_method = shell.input_method().is_open(); + let translation = state.translation(self.direction, bounds, content_bounds); @@ -745,6 +748,14 @@ where ..bounds }, ); + + if !had_input_method { + if let InputMethod::Open { position, .. } = + shell.input_method_mut() + { + *position = *position + translation; + } + } }; if matches!( 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 f1ec589b..486741c6 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,14 +47,15 @@ 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, Color, Element, Event, InputMethod, Length, Padding, + Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; 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}; @@ -322,6 +324,51 @@ where self.class = class.into(); self } + + fn input_method<'b>( + &self, + state: &'b State<Highlighter>, + renderer: &Renderer, + layout: Layout<'_>, + ) -> InputMethod<&'b str> { + let Some(Focus { + is_window_focused: true, + .. + }) = &state.focus + else { + return InputMethod::Disabled; + }; + + let Some(preedit) = &state.preedit else { + return InputMethod::Allowed; + }; + + let bounds = layout.bounds(); + let internal = self.content.0.borrow_mut(); + + let text_bounds = bounds.shrink(self.padding); + let translation = text_bounds.position() - Point::ORIGIN; + + 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.as_ref()), + } + } } /// The content of a [`TextEditor`]. @@ -450,6 +497,7 @@ where #[derive(Debug)] pub struct State<Highlighter: text::Highlighter> { focus: Option<Focus>, + preedit: Option<input_method::Preedit>, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, partial_scroll: f32, @@ -458,7 +506,7 @@ pub struct State<Highlighter: text::Highlighter> { highlighter_format_address: usize, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, @@ -524,6 +572,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, @@ -605,7 +654,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 +750,29 @@ where })); shell.capture_event(); } + Update::InputMethod(update) => match update { + 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() { + state.preedit = Some(input_method::Preedit { + content, + selection, + }); + + shell.request_redraw(); + } + } + Ime::Commit(text) => { + shell.publish(on_edit(Action::Edit(Edit::Paste( + Arc::new(text), + )))); + } + }, Update::Binding(binding) => { fn apply_binding< H: text::Highlighter, @@ -827,6 +899,10 @@ where 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) @@ -1129,9 +1205,19 @@ enum Update<Message> { Drag(Point), Release, Scroll(f32), + InputMethod(Ime), Binding(Binding<Message>), } +enum Ime { + Toggle(bool), + Preedit { + content: String, + selection: Option<Range<usize>>, + }, + Commit(String), +} + impl<Message> Update<Message> { fn from_event<H: Highlighter>( event: Event, @@ -1191,6 +1277,23 @@ impl<Message> Update<Message> { } _ => None, }, + 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, selection) => { + Some(Update::InputMethod(Ime::Preedit { + content, + selection, + })) + } + 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 57ebe46a..b22ee1ca 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -42,6 +42,7 @@ 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; @@ -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, Color, Element, Event, InputMethod, 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 input_method<'b>( + &self, + state: &'b State<Renderer::Paragraph>, + layout: Layout<'_>, + value: &Value, + ) -> InputMethod<&'b str> { + let Some(Focus { + is_window_focused: true, + .. + }) = &state.is_focused + else { + return InputMethod::Disabled; + }; + + let Some(preedit) = &state.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); + + let text_bounds = layout.children().next().unwrap().bounds(); + + 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 (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 x = (text_bounds.x + cursor_x).floor() - scroll_offset + + alignment_offset; + + InputMethod::Open { + position: Point::new(x, text_bounds.y + text_bounds.height), + purpose: if self.is_secure { + input_method::Purpose::Secure + } else { + input_method::Purpose::Normal + }, + preedit: Some(preedit.as_ref()), + } + } + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -1197,6 +1250,51 @@ where state.keyboard_modifiers = *modifiers; } + Event::InputMethod(event) => match event { + input_method::Event::Opened | input_method::Event::Closed => { + let state = state::<Renderer>(tree); + + 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::<Renderer>(tree); + + if state.is_focused.is_some() { + state.is_ime_open = Some(input_method::Preedit { + content: content.to_owned(), + selection: selection.clone(), + }); + + shell.request_redraw(); + } + } + input_method::Event::Commit(text) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = &self.on_input else { + return; + }; + + 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; + + let message = (on_input)(editor.contents()); + shell.publish(message); + shell.capture_event(); + + update_cache(state, &self.value); + } + } + }, Event::Window(window::Event::Unfocused) => { let state = state::<Renderer>(tree); @@ -1258,6 +1356,12 @@ where 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) @@ -1417,6 +1521,7 @@ pub struct State<P: text::Paragraph> { placeholder: paragraph::Plain<P>, icon: paragraph::Plain<P>, is_focused: Option<Focus>, + is_ime_open: Option<input_method::Preedit>, is_dragging: bool, is_pasting: Option<Value>, last_click: Option<mouse::Click>, @@ -1431,7 +1536,7 @@ fn state<Renderer: text::Renderer>( tree.state.downcast_mut::<State<Renderer::Paragraph>>() } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, |