diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 1ce0758c2e..739c38dd56 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -8,7 +8,7 @@ use std::{env, fs, num::NonZeroU32, rc::Rc, slice}; use tiny_skia::{Paint, PixmapMut, Rect, Transform}; use winit::{ dpi::{PhysicalPosition, PhysicalSize}, - event::{ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent}, + event::{ElementState, Event, Ime, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent}, event_loop::{ControlFlow, EventLoop}, keyboard::{Key, NamedKey}, window::WindowBuilder, @@ -28,6 +28,7 @@ fn main() { let mut swash_cache = SwashCache::new(); let mut display_scale = window.scale_factor() as f32; + window.set_ime_allowed(true); let scrollbar_width = 12.0; let font_sizes = [ @@ -278,6 +279,21 @@ fn main() { window.request_redraw(); } } + WindowEvent::Ime(ime) => match ime { + Ime::Enabled | Ime::Disabled => {} + Ime::Preedit(preedit, cursor) => { + editor.action(Action::SetPreedit { + preedit, + cursor, + attrs: None, + }); + window.request_redraw(); + } + Ime::Commit(text) => { + editor.insert_string(&text, None); + window.request_redraw(); + } + }, WindowEvent::CursorMoved { device_id: _, position, diff --git a/src/attrs.rs b/src/attrs.rs index ea8b671376..a54df11f90 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -139,6 +139,7 @@ pub struct Attrs<'a> { pub metadata: usize, pub cache_key_flags: CacheKeyFlags, pub metrics_opt: Option, + pub is_preedit: bool, } impl<'a> Attrs<'a> { @@ -155,6 +156,7 @@ impl<'a> Attrs<'a> { metadata: 0, cache_key_flags: CacheKeyFlags::empty(), metrics_opt: None, + is_preedit: false, } } @@ -206,6 +208,12 @@ impl<'a> Attrs<'a> { self } + /// Set preedit + pub fn preedit(mut self, is_preedit: bool) -> Self { + self.is_preedit = is_preedit; + self + } + /// Check if font matches pub fn matches(&self, face: &fontdb::FaceInfo) -> bool { //TODO: smarter way of including emoji @@ -254,6 +262,7 @@ pub struct AttrsOwned { pub metadata: usize, pub cache_key_flags: CacheKeyFlags, pub metrics_opt: Option, + pub is_preedit: bool, } impl AttrsOwned { @@ -267,6 +276,7 @@ impl AttrsOwned { metadata: attrs.metadata, cache_key_flags: attrs.cache_key_flags, metrics_opt: attrs.metrics_opt, + is_preedit: attrs.is_preedit, } } @@ -280,6 +290,7 @@ impl AttrsOwned { metadata: self.metadata, cache_key_flags: self.cache_key_flags, metrics_opt: self.metrics_opt, + is_preedit: self.is_preedit, } } } diff --git a/src/buffer.rs b/src/buffer.rs index a8f7d22475..8613a81eb0 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -811,6 +811,23 @@ impl Buffer { self.shape_until_scroll(font_system, false); } + /// Returns text of the buffer, excluding preedit (if any) + pub fn text_without_preedit(&self) -> String { + let mut output = String::new(); + let mut iter = self + .lines + .iter() + .map(|line| line.text_without_preedit()) + .peekable(); + while let Some(text) = iter.next() { + output.push_str(&text); + if iter.peek().is_some() { + output.push('\n'); + } + } + output + } + /// True if a redraw is needed pub fn redraw(&self) -> bool { self.redraw diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 9eba6c76e3..a8fd7a752e 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -1,5 +1,11 @@ +use core::ops::Range; + +use alloc::borrow::Cow; #[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; use crate::{ Align, AttrsList, FontSystem, LayoutLine, LineEnding, ShapeBuffer, ShapeLine, Shaping, Wrap, @@ -40,11 +46,38 @@ impl BufferLine { } } - /// Get current text + /// Get current text, including preedit (if any) pub fn text(&self) -> &str { &self.text } + /// Returns the range of byte indices of the text corresponding + /// to the preedit + pub fn preedit_range(&self) -> Option> { + self.attrs_list + .spans() + .iter() + .find(|(_, attrs)| attrs.is_preedit) + .map(|(range, _)| (*range).clone()) + } + + /// Get current preedit text + pub fn preedit_text(&self) -> Option<&str> { + let range = self.preedit_range()?; + Some(&self.text[range]) + } + + /// Get current text, excluding preedit (if any) + pub fn text_without_preedit(&self) -> Cow<'_, str> { + if let Some(range) = self.preedit_range() { + let mut text = self.text[..range.start].to_string(); + text.push_str(&self.text[range.end..]); + text.into() + } else { + self.text.as_str().into() + } + } + /// Set text and attributes list /// /// Will reset shape and layout if it differs from current text and attributes list. diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 1231002cec..89f90324f1 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -5,14 +5,12 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use core::{cmp, iter::once}; +use core::{cmp, iter::once, ops::Range}; use unicode_segmentation::UnicodeSegmentation; -#[cfg(feature = "swash")] -use crate::Color; use crate::{ Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem, - Cursor, Edit, FontSystem, LayoutRun, Selection, Shaping, + Color, Cursor, Edit, FontSystem, LayoutRun, Selection, Shaping, }; /// A wrapper of [`Buffer`] for easy editing @@ -561,6 +559,18 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { self.change.take() } + fn preedit_range(&self) -> Option> { + self.with_buffer(|buffer| buffer.lines[self.cursor.line].preedit_range()) + } + + fn preedit_text(&self) -> Option { + self.with_buffer(|buffer| { + buffer.lines[self.cursor.line] + .preedit_text() + .map(Into::into) + }) + } + fn action(&mut self, font_system: &mut FontSystem, action: Action) { let old_cursor = self.cursor; @@ -865,6 +875,57 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { buffer.set_scroll(scroll); }); } + Action::SetPreedit { + preedit, + cursor, + attrs, + } => { + self.selection = Selection::None; + let mut self_cursor = self.cursor; + + // Remove old preedit, if any + self.with_buffer_mut(|buffer| { + let line: &mut BufferLine = &mut buffer.lines[self_cursor.line]; + if let Some(range) = line.preedit_range() { + let end = line.split_off(range.end); + line.split_off(range.start); + line.append(end); + self_cursor.index = range.start; + } + }); + self.cursor = self_cursor; + + if !preedit.is_empty() { + let new_attrs = if let Some(attrs) = attrs { + AttrsList::new(attrs.as_attrs().preedit(true)) + } else { + self.with_buffer(|buffer| { + let attrs_at_cursor = buffer.lines[self_cursor.line] + .attrs_list() + .get_span(self_cursor.index); + AttrsList::new( + attrs_at_cursor + .preedit(true) + .color(Color::rgb(128, 128, 128)), + ) + }) + }; + self.insert_string(&preedit, Some(new_attrs)); + if let Some((start, end)) = cursor { + let end_delta = preedit.len().saturating_sub(end); + self.cursor.index = self.cursor.index.saturating_sub(end_delta); + if start != end { + let start_delta = preedit.len().saturating_sub(start); + let mut select = self.cursor; + select.index = select.index.saturating_sub(start_delta); + self.selection = Selection::Normal(select); + } + } else { + // TODO: hide cursor + } + } + self.set_redraw(true); + } } if old_cursor != self.cursor { diff --git a/src/edit/mod.rs b/src/edit/mod.rs index d9d4fd11db..fe0716b7a8 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -1,10 +1,12 @@ use alloc::sync::Arc; +use core::ops::Range; + #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; use core::cmp; use unicode_segmentation::UnicodeSegmentation; -use crate::{AttrsList, BorrowedWithFontSystem, Buffer, Cursor, FontSystem, Motion}; +use crate::{AttrsList, AttrsOwned, BorrowedWithFontSystem, Buffer, Cursor, FontSystem, Motion}; pub use self::editor::*; mod editor; @@ -20,7 +22,7 @@ pub use self::vi::*; mod vi; /// An action to perform on an [`Editor`] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Action { /// Move the cursor with some motion Motion(Motion), @@ -62,6 +64,20 @@ pub enum Action { Scroll { lines: i32, }, + /// Set preedit text, replacing any previous preedit text + /// + /// If `cursor` is specified, it contains a start and end cursor byte positions + /// within the preedit. If no cursor is specified for a non-empty preedit, + /// the cursor should be hidden. + /// + /// If `attrs` is specified, these attributes will be assigned to the preedit's span. + /// However, regardless of `attrs` setting, the preedit's span will always have + /// `is_preedit` set to `true`. + SetPreedit { + preedit: String, + cursor: Option<(usize, usize)>, + attrs: Option, + }, } #[derive(Debug)] @@ -327,6 +343,13 @@ pub trait Edit<'buffer> { /// Get completed change fn finish_change(&mut self) -> Option; + /// Returns the range of byte indices of the text corresponding + /// to the preedit + fn preedit_range(&self) -> Option>; + + /// Get current preedit text + fn preedit_text(&self) -> Option; + /// Perform an [Action] on the editor fn action(&mut self, font_system: &mut FontSystem, action: Action); diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index d6ef2d8871..4261702991 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -294,6 +294,9 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for SyntaxEditor<'syntax_system, 'bu } let line = &mut buffer.lines[line_i]; + if line.preedit_range().is_some() { + continue; + } if line.metadata().is_some() && line_i < self.syntax_cache.len() { //TODO: duplicated code! if line_i >= scroll.line && total_height < scroll_end { @@ -427,6 +430,14 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for SyntaxEditor<'syntax_system, 'bu self.editor.start_change(); } + fn preedit_range(&self) -> Option> { + self.editor.preedit_range() + } + + fn preedit_text(&self) -> Option { + self.editor.preedit_text() + } + fn finish_change(&mut self) -> Option { self.editor.finish_change() } diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 08dfd480fb..e8aadd88db 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1165,6 +1165,14 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for ViEditor<'syntax_system, 'buffer fn cursor_position(&self) -> Option<(i32, i32)> { self.editor.cursor_position() } + + fn preedit_range(&self) -> Option> { + self.editor.preedit_range() + } + + fn preedit_text(&self) -> Option { + self.editor.preedit_text() + } } impl<'font_system, 'syntax_system, 'buffer>