summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2025-02-03 02:33:40 +0100
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2025-02-03 02:33:40 +0100
commitc83809adb907498ba2a573ec9fb50936601ac8fc (patch)
tree5009581a507014cda6850f0780bd315108d56bdd
parent3a35fd6249eeb324379d3a14b020ccc48ec16fb4 (diff)
downloadiced-c83809adb907498ba2a573ec9fb50936601ac8fc.tar.gz
iced-c83809adb907498ba2a573ec9fb50936601ac8fc.tar.bz2
iced-c83809adb907498ba2a573ec9fb50936601ac8fc.zip
Implement basic IME selection in `Preedit` overlay
-rw-r--r--core/src/input_method.rs47
-rw-r--r--core/src/text.rs17
-rw-r--r--widget/src/text_editor.rs27
-rw-r--r--widget/src/text_input.rs13
-rw-r--r--winit/src/program/window_manager.rs120
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,
+ );
+ }
});
}
}