Implement basic IME selection in Preedit overlay
This commit is contained in:
parent
3a35fd6249
commit
c83809adb9
5 changed files with 180 additions and 42 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)))
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue