diff options
author | 2025-02-03 02:33:40 +0100 | |
---|---|---|
committer | 2025-02-03 02:33:40 +0100 | |
commit | c83809adb907498ba2a573ec9fb50936601ac8fc (patch) | |
tree | 5009581a507014cda6850f0780bd315108d56bdd | |
parent | 3a35fd6249eeb324379d3a14b020ccc48ec16fb4 (diff) | |
download | iced-c83809adb907498ba2a573ec9fb50936601ac8fc.tar.gz iced-c83809adb907498ba2a573ec9fb50936601ac8fc.tar.bz2 iced-c83809adb907498ba2a573ec9fb50936601ac8fc.zip |
Implement basic IME selection in `Preedit` overlay
-rw-r--r-- | core/src/input_method.rs | 47 | ||||
-rw-r--r-- | core/src/text.rs | 17 | ||||
-rw-r--r-- | widget/src/text_editor.rs | 27 | ||||
-rw-r--r-- | widget/src/text_input.rs | 13 | ||||
-rw-r--r-- | 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<T = String> { /// Ideally, your widget will show pre-edits on-the-spot; but, since that can /// be tricky, you can instead provide the current pre-edit here and the /// runtime will display it as an overlay (i.e. "Over-the-spot IME"). - preedit: Option<T>, + preedit: Option<Preedit<T>>, }, } +/// The pre-edit of an [`InputMethod`]. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Preedit<T = String> { + /// The current content. + pub content: T, + /// The selected range of the content. + pub selection: Option<Range<usize>>, +} + +impl<T> Preedit<T> { + /// 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<str>, + { + 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<Link, Font> 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<Highlighter: text::Highlighter> { focus: Option<Focus>, - preedit: Option<String>, + preedit: Option<input_method::Preedit>, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, 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<Message> { enum Ime { Toggle(bool), - Preedit(String), + Preedit { + content: String, + selection: Option<Range<usize>>, + }, Commit(String), } @@ -1272,8 +1280,11 @@ impl<Message> Update<Message> { 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::<Renderer>(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<P: text::Paragraph> { placeholder: paragraph::Plain<P>, icon: paragraph::Plain<P>, is_focused: Option<Focus>, - is_ime_open: Option<String>, + is_ime_open: Option<input_method::Preedit>, is_dragging: bool, is_pasting: Option<Value>, last_click: Option<mouse::Click>, 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<P, C> 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<Renderer::Paragraph>, + content: Renderer::Paragraph, + spans: Vec<text::Span<'static, (), Renderer::Font>>, } impl<Renderer> Preedit<Renderer> @@ -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, + ); + } }); } } |