Implement basic IME selection in Preedit overlay

This commit is contained in:
Héctor Ramón Jiménez 2025-02-03 02:33:40 +01:00
parent 3a35fd6249
commit c83809adb9
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
5 changed files with 180 additions and 42 deletions

View file

@ -23,10 +23,50 @@ pub enum InputMethod<T = String> {
/// Ideally, your widget will show pre-edits on-the-spot; but, since that can /// 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 /// 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"). /// 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`]. /// The purpose of an [`InputMethod`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Purpose { pub enum Purpose {
@ -84,10 +124,7 @@ impl InputMethod {
*self = Self::Open { *self = Self::Open {
position: *position, position: *position,
purpose: *purpose, purpose: *purpose,
preedit: preedit preedit: preedit.as_ref().map(Preedit::to_owned),
.as_ref()
.map(AsRef::as_ref)
.map(str::to_owned),
}; };
} }
InputMethod::Allowed InputMethod::Allowed

View file

@ -270,6 +270,23 @@ pub struct Span<'a, Link = (), Font = crate::Font> {
pub strikethrough: bool, 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. /// A text highlight.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Highlight { pub struct Highlight {

View file

@ -55,6 +55,7 @@ use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt; use std::fmt;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; pub use text::editor::{Action, Edit, Line, LineEnding, Motion};
@ -365,7 +366,7 @@ where
InputMethod::Open { InputMethod::Open {
position, position,
purpose: input_method::Purpose::Normal, purpose: input_method::Purpose::Normal,
preedit: Some(preedit), preedit: Some(preedit.as_ref()),
} }
} }
} }
@ -496,7 +497,7 @@ where
#[derive(Debug)] #[derive(Debug)]
pub struct State<Highlighter: text::Highlighter> { pub struct State<Highlighter: text::Highlighter> {
focus: Option<Focus>, focus: Option<Focus>,
preedit: Option<String>, preedit: Option<input_method::Preedit>,
last_click: Option<mouse::Click>, last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>, drag_click: Option<mouse::click::Kind>,
partial_scroll: f32, partial_scroll: f32,
@ -751,11 +752,15 @@ where
} }
Update::InputMethod(update) => match update { Update::InputMethod(update) => match update {
Ime::Toggle(is_open) => { 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() { if state.focus.is_some() {
state.preedit = Some(text); state.preedit = Some(input_method::Preedit {
content,
selection,
});
} }
} }
Ime::Commit(text) => { Ime::Commit(text) => {
@ -1202,7 +1207,10 @@ enum Update<Message> {
enum Ime { enum Ime {
Toggle(bool), Toggle(bool),
Preedit(String), Preedit {
content: String,
selection: Option<Range<usize>>,
},
Commit(String), Commit(String),
} }
@ -1272,8 +1280,11 @@ impl<Message> Update<Message> {
input_method::Event::Opened input_method::Event::Opened
)))) ))))
} }
input_method::Event::Preedit(content, _range) => { input_method::Event::Preedit(content, selection) => {
Some(Update::InputMethod(Ime::Preedit(content))) Some(Update::InputMethod(Ime::Preedit {
content,
selection,
}))
} }
input_method::Event::Commit(content) => { input_method::Event::Commit(content) => {
Some(Update::InputMethod(Ime::Commit(content))) Some(Update::InputMethod(Ime::Commit(content)))

View file

@ -440,7 +440,7 @@ where
} else { } else {
input_method::Purpose::Normal input_method::Purpose::Normal
}, },
preedit: Some(preedit), preedit: Some(preedit.as_ref()),
} }
} }
@ -1256,13 +1256,16 @@ where
state.is_ime_open = state.is_ime_open =
matches!(event, input_method::Event::Opened) 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); let state = state::<Renderer>(tree);
if state.is_focused.is_some() { 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) => { input_method::Event::Commit(text) => {
@ -1514,7 +1517,7 @@ pub struct State<P: text::Paragraph> {
placeholder: paragraph::Plain<P>, placeholder: paragraph::Plain<P>,
icon: paragraph::Plain<P>, icon: paragraph::Plain<P>,
is_focused: Option<Focus>, is_focused: Option<Focus>,
is_ime_open: Option<String>, is_ime_open: Option<input_method::Preedit>,
is_dragging: bool, is_dragging: bool,
is_pasting: Option<Value>, is_pasting: Option<Value>,
last_click: Option<mouse::Click>, last_click: Option<mouse::Click>,

View file

@ -1,5 +1,6 @@
use crate::conversion; use crate::conversion;
use crate::core::alignment; use crate::core::alignment;
use crate::core::input_method;
use crate::core::mouse; use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text; use crate::core::text;
@ -12,11 +13,13 @@ use crate::core::{
use crate::graphics::Compositor; use crate::graphics::Compositor;
use crate::program::{Program, State}; use crate::program::{Program, State};
use std::collections::BTreeMap;
use std::sync::Arc;
use winit::dpi::{LogicalPosition, LogicalSize}; use winit::dpi::{LogicalPosition, LogicalSize};
use winit::monitor::MonitorHandle; use winit::monitor::MonitorHandle;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::Arc;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct WindowManager<P, C> pub struct WindowManager<P, C>
where where
@ -226,16 +229,26 @@ where
self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); self.raw.set_ime_purpose(conversion::ime_purpose(purpose));
if let Some(content) = preedit { if let Some(preedit) = preedit {
if content.is_empty() { if preedit.content.is_empty() {
self.preedit = None; self.preedit = None;
} else if let Some(preedit) = &mut self.preedit { } else if let Some(overlay) = &mut self.preedit {
preedit.update(position, &content, &self.renderer); overlay.update(
position,
&preedit,
self.state.background_color(),
&self.renderer,
);
} else { } else {
let mut preedit = Preedit::new(); let mut overlay = Preedit::new();
preedit.update(position, &content, &self.renderer); overlay.update(
position,
&preedit,
self.state.background_color(),
&self.renderer,
);
self.preedit = Some(preedit); self.preedit = Some(overlay);
} }
} }
} else { } else {
@ -263,7 +276,8 @@ where
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
position: Point, position: Point,
content: text::paragraph::Plain<Renderer::Paragraph>, content: Renderer::Paragraph,
spans: Vec<text::Span<'static, (), Renderer::Font>>,
} }
impl<Renderer> Preedit<Renderer> impl<Renderer> Preedit<Renderer>
@ -273,15 +287,57 @@ where
fn new() -> Self { fn new() -> Self {
Self { Self {
position: Point::ORIGIN, 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.position = position;
self.content.update(Text { let spans = match &preedit.selection {
content: text, 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, bounds: Size::INFINITY,
size: renderer.default_size(), size: renderer.default_size(),
line_height: text::LineHeight::default(), line_height: text::LineHeight::default(),
@ -292,6 +348,7 @@ where
wrapping: text::Wrapping::None, wrapping: text::Wrapping::None,
}); });
} }
}
fn draw( fn draw(
&self, &self,
@ -300,6 +357,8 @@ where
background: Color, background: Color,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
use text::Paragraph as _;
if self.content.min_width() < 1.0 { if self.content.min_width() < 1.0 {
return; return;
} }
@ -329,7 +388,7 @@ where
); );
renderer.fill_paragraph( renderer.fill_paragraph(
self.content.raw(), &self.content,
bounds.position(), bounds.position(),
color, color,
bounds, bounds,
@ -347,6 +406,17 @@ where
}, },
color, 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,
);
}
}); });
} }
} }