diff options
author | Kirill Chibisov <contact@kchibisov.com> | 2022-08-29 16:29:13 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-29 16:29:13 +0300 |
commit | 18f9c2793924aec91c80a69ccb45f529adaffae5 (patch) | |
tree | 63cda75c8203c39a7437bd1812653f74494f878f | |
parent | 791f79a02a4bbb509c257af2849e411d32f4c18b (diff) | |
download | alacritty-18f9c2793924aec91c80a69ccb45f529adaffae5.tar.gz alacritty-18f9c2793924aec91c80a69ccb45f529adaffae5.zip |
Add inline input method support
This commit adds support for inline IME handling. It also makes the
search bar use underline cursor instead of using '_' character.
Fixes #1613.
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | alacritty/src/display/content.rs | 5 | ||||
-rw-r--r-- | alacritty/src/display/mod.rs | 251 | ||||
-rw-r--r-- | alacritty/src/display/window.rs | 8 | ||||
-rw-r--r-- | alacritty/src/event.rs | 50 | ||||
-rw-r--r-- | alacritty/src/input.rs | 10 | ||||
-rw-r--r-- | alacritty/src/string.rs | 14 |
7 files changed, 296 insertions, 46 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 85364b8e..3f16e7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Escape sequence to set hyperlinks (`OSC 8 ; params ; URI ST`) - Config `hints.enabled.hyperlinks` for hyperlink escape sequence hint highlight - `window.decorations_theme_variant` to control both Wayland CSD and GTK theme variant on X11 +- Support for inline input method ### Changed @@ -42,6 +43,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Config option `window.gtk_theme_variant`, you should use `window.decorations_theme_variant` instead - `--class` now sets both class part of WM_CLASS property and instance - `--class`'s `general` and `instance` options were swapped +- Search bar is now respecting cursor thickness +- On X11 the IME popup window is stuck at the bottom of the window due to Xlib limitations +- IME no longer works in Vi mode when moving around ### Fixed diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index 478982bb..c062ba9b 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -51,6 +51,7 @@ impl<'a> RenderableContent<'a> { let cursor_shape = if terminal_content.cursor.shape == CursorShape::Hidden || display.cursor_hidden || search_state.regex().is_some() + || display.ime.preedit().is_some() { CursorShape::Hidden } else if !term.is_focused && config.terminal_config.cursor.unfocused_hollow { @@ -394,6 +395,10 @@ impl RenderableCursor { } impl RenderableCursor { + pub fn new(point: Point<usize>, shape: CursorShape, cursor_color: Rgb, is_wide: bool) -> Self { + Self { shape, cursor_color, text_color: cursor_color, is_wide, point } + } + pub fn color(&self) -> Rgb { self.cursor_color } diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index 7bd50049..e48d2b68 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -20,8 +20,9 @@ use serde::{Deserialize, Serialize}; use wayland_client::EventQueue; use crossfont::{self, Rasterize, Rasterizer}; +use unicode_width::UnicodeWidthChar; -use alacritty_terminal::ansi::NamedColor; +use alacritty_terminal::ansi::{CursorShape, NamedColor}; use alacritty_terminal::config::MAX_SCROLLBACK_LINES; use alacritty_terminal::event::{EventListener, OnResize, WindowSize}; use alacritty_terminal::grid::Dimensions as TermDimensions; @@ -38,7 +39,7 @@ use crate::config::window::{Dimensions, Identity}; use crate::config::UiConfig; use crate::display::bell::VisualBell; use crate::display::color::List; -use crate::display::content::RenderableContent; +use crate::display::content::{RenderableContent, RenderableCursor}; use crate::display::cursor::IntoRects; use crate::display::damage::RenderDamageIterator; use crate::display::hint::{HintMatch, HintState}; @@ -46,7 +47,7 @@ use crate::display::meter::Meter; use crate::display::window::Window; use crate::event::{Mouse, SearchState}; use crate::message_bar::{MessageBuffer, MessageType}; -use crate::renderer::rects::{RenderLines, RenderRect}; +use crate::renderer::rects::{RenderLine, RenderLines, RenderRect}; use crate::renderer::{self, GlyphCache, Renderer}; use crate::string::{ShortenDirection, StrShortener}; @@ -362,6 +363,9 @@ pub struct Display { /// The renderer update that takes place only once before the actual rendering. pub pending_renderer_update: Option<RendererUpdate>, + /// The ime on the given display. + pub ime: Ime, + // Mouse point position when highlighting hints. hint_mouse_point: Option<Point>, @@ -374,6 +378,77 @@ pub struct Display { meter: Meter, } +/// Input method state. +#[derive(Debug, Default)] +pub struct Ime { + /// Whether the IME is enabled. + enabled: bool, + + /// Current IME preedit. + preedit: Option<Preedit>, +} + +impl Ime { + pub fn new() -> Self { + Default::default() + } + + #[inline] + pub fn set_enabled(&mut self, is_enabled: bool) { + if is_enabled { + self.enabled = is_enabled + } else { + // Clear state when disabling IME. + *self = Default::default(); + } + } + + #[inline] + pub fn is_enabled(&self) -> bool { + self.enabled + } + + #[inline] + pub fn set_preedit(&mut self, preedit: Option<Preedit>) { + self.preedit = preedit; + } + + #[inline] + pub fn preedit(&self) -> Option<&Preedit> { + self.preedit.as_ref() + } +} + +#[derive(Debug, Default)] +pub struct Preedit { + /// The preedit text. + text: String, + + /// Byte offset for cursor start into the preedit text. + /// + /// `None` means that the cursor is invisible. + cursor_byte_offset: Option<usize>, + + /// The cursor offset from the end of the preedit in char width. + cursor_end_offset: Option<usize>, +} + +impl Preedit { + pub fn new(text: String, cursor_byte_offset: Option<usize>) -> Self { + let cursor_end_offset = if let Some(byte_offset) = cursor_byte_offset { + // Convert byte offset into char offset. + let cursor_end_offset = + text[byte_offset..].chars().fold(0, |acc, ch| acc + ch.width().unwrap_or(1)); + + Some(cursor_end_offset) + } else { + None + }; + + Self { text, cursor_byte_offset, cursor_end_offset } + } +} + /// Pending renderer updates. /// /// All renderer updates are cached to be applied just before rendering, to avoid platform-specific @@ -529,6 +604,7 @@ impl Display { hint_state, meter: Meter::new(), size_info, + ime: Ime::new(), highlighted_hint: None, vi_highlighted_hint: None, #[cfg(not(any(target_os = "macos", windows)))] @@ -750,6 +826,7 @@ impl Display { grid_cells.push(cell); } let selection_range = content.selection_range(); + let foreground_color = content.color(NamedColor::Foreground as usize); let background_color = content.color(NamedColor::Background as usize); let display_offset = content.display_offset(); let cursor = content.cursor(); @@ -835,9 +912,7 @@ impl Display { }; // Draw cursor. - for rect in cursor.rects(&size_info, config.terminal_config.cursor.thickness()) { - rects.push(rect); - } + rects.extend(cursor.rects(&size_info, config.terminal_config.cursor.thickness())); // Push visual bell after url/underline/strikeout rects. let visual_bell_intensity = self.visual_bell.intensity(); @@ -853,6 +928,55 @@ impl Display { rects.push(visual_bell_rect); } + // Handle IME positioning and search bar rendering. + let ime_position = match search_state.regex() { + Some(regex) => { + let search_label = match search_state.direction() { + Direction::Right => FORWARD_SEARCH_LABEL, + Direction::Left => BACKWARD_SEARCH_LABEL, + }; + + let search_text = Self::format_search(regex, search_label, size_info.columns()); + + // Render the search bar. + self.draw_search(config, &search_text); + + // Draw search bar cursor. + let line = size_info.screen_lines(); + let column = Column(search_text.chars().count() - 1); + + // Add cursor to search bar if IME is not active. + if self.ime.preedit().is_none() { + let fg = config.colors.footer_bar_foreground(); + let shape = CursorShape::Underline; + let cursor = RenderableCursor::new(Point::new(line, column), shape, fg, false); + rects.extend( + cursor.rects(&size_info, config.terminal_config.cursor.thickness()), + ); + } + + Some(Point::new(line, column)) + }, + None => { + let num_lines = self.size_info.screen_lines(); + term::point_to_viewport(display_offset, cursor_point) + .filter(|point| point.line < num_lines) + }, + }; + + // Handle IME. + if self.ime.is_enabled() { + if let Some(point) = ime_position { + let (fg, bg) = if search_state.regex().is_some() { + (config.colors.footer_bar_foreground(), config.colors.footer_bar_background()) + } else { + (foreground_color, background_color) + }; + + self.draw_ime_preview(point, fg, bg, &mut rects, config); + } + } + if self.debug_damage { self.highlight_damage(&mut rects); } @@ -900,34 +1024,11 @@ impl Display { self.draw_render_timer(config); - // Handle search and IME positioning. - let ime_position = match search_state.regex() { - Some(regex) => { - let search_label = match search_state.direction() { - Direction::Right => FORWARD_SEARCH_LABEL, - Direction::Left => BACKWARD_SEARCH_LABEL, - }; - - let search_text = Self::format_search(regex, search_label, size_info.columns()); - - // Render the search bar. - self.draw_search(config, &search_text); - - // Compute IME position. - let line = Line(size_info.screen_lines() as i32 + 1); - Point::new(line, Column(search_text.chars().count() - 1)) - }, - None => cursor_point, - }; - // Draw hyperlink uri preview. if has_highlighted_hint { self.draw_hyperlink_preview(config, vi_cursor_point, display_offset); } - // Update IME position. - self.window.update_ime_position(ime_position, &self.size_info); - // Frame event should be requested before swaping buffers, since it requires surface // `commit`, which is done by swap buffers under the hood. #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -1015,6 +1116,95 @@ impl Display { dirty } + #[inline(never)] + fn draw_ime_preview( + &mut self, + point: Point<usize>, + fg: Rgb, + bg: Rgb, + rects: &mut Vec<RenderRect>, + config: &UiConfig, + ) { + let preedit = match self.ime.preedit() { + Some(preedit) => preedit, + None => { + // In case we don't have preedit, just set the popup point. + self.window.update_ime_position(point, &self.size_info); + return; + }, + }; + + let num_cols = self.size_info.columns(); + + // Get the visible preedit. + let visible_text: String = match (preedit.cursor_byte_offset, preedit.cursor_end_offset) { + (Some(byte_offset), Some(end_offset)) if end_offset > num_cols => StrShortener::new( + &preedit.text[byte_offset..], + num_cols, + ShortenDirection::Right, + Some(SHORTENER), + ), + _ => { + StrShortener::new(&preedit.text, num_cols, ShortenDirection::Left, Some(SHORTENER)) + }, + } + .collect(); + + let visible_len = visible_text.chars().count(); + + let end = cmp::min(point.column.0 + visible_len, num_cols); + let start = end.saturating_sub(visible_len); + + let start = Point::new(point.line, Column(start)); + let end = Point::new(point.line, Column(end - 1)); + + let glyph_cache = &mut self.glyph_cache; + let metrics = glyph_cache.font_metrics(); + + self.renderer.draw_string( + start, + fg, + bg, + visible_text.chars(), + &self.size_info, + glyph_cache, + ); + + if self.collect_damage() { + let damage = self.damage_from_point(Point::new(start.line, Column(0)), num_cols as u32); + self.damage_rects.push(damage); + self.next_frame_damage_rects.push(damage); + } + + // Add underline for preedit text. + let underline = RenderLine { start, end, color: fg }; + rects.extend(underline.rects(Flags::UNDERLINE, &metrics, &self.size_info)); + + let ime_popup_point = match preedit.cursor_end_offset { + Some(cursor_end_offset) if cursor_end_offset != 0 => { + let is_wide = preedit.text[preedit.cursor_byte_offset.unwrap_or_default()..] + .chars() + .next() + .map(|ch| ch.width() == Some(2)) + .unwrap_or_default(); + + let cursor_column = Column( + (end.column.0 as isize - cursor_end_offset as isize + 1).max(0) as usize, + ); + let cursor_point = Point::new(point.line, cursor_column); + let cursor = + RenderableCursor::new(cursor_point, CursorShape::HollowBlock, fg, is_wide); + rects.extend( + cursor.rects(&self.size_info, config.terminal_config.cursor.thickness()), + ); + cursor_point + }, + _ => end, + }; + + self.window.update_ime_position(ime_popup_point, &self.size_info); + } + /// Format search regex to account for the cursor and fullwidth characters. fn format_search(search_regex: &str, search_label: &str, max_width: usize) -> String { let label_len = search_label.len(); @@ -1033,7 +1223,8 @@ impl Display { Some(SHORTENER), )); - bar_text.push('_'); + // Add place for cursor. + bar_text.push(' '); bar_text } diff --git a/alacritty/src/display/window.rs b/alacritty/src/display/window.rs index 3cc00a98..eac12a22 100644 --- a/alacritty/src/display/window.rs +++ b/alacritty/src/display/window.rs @@ -460,10 +460,14 @@ impl Window { self.wayland_surface.as_ref() } + pub fn set_ime_allowed(&self, allowed: bool) { + self.windowed_context.window().set_ime_allowed(allowed); + } + /// Adjust the IME editor position according to the new location of the cursor. - pub fn update_ime_position(&self, point: Point, size: &SizeInfo) { + pub fn update_ime_position(&self, point: Point<usize>, size: &SizeInfo) { let nspot_x = f64::from(size.padding_x() + point.column.0 as f32 * size.cell_width()); - let nspot_y = f64::from(size.padding_y() + (point.line.0 + 1) as f32 * size.cell_height()); + let nspot_y = f64::from(size.padding_y() + (point.line + 1) as f32 * size.cell_height()); self.window().set_ime_position(PhysicalPosition::new(nspot_x, nspot_y)); } diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 3beb5d1e..b4258a03 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -47,7 +47,7 @@ use crate::daemon::foreground_process_path; use crate::daemon::spawn_daemon; use crate::display::hint::HintMatch; use crate::display::window::Window; -use crate::display::{Display, SizeInfo}; +use crate::display::{Display, Preedit, SizeInfo}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; use crate::message_bar::{Message, MessageBuffer}; use crate::scheduler::{Scheduler, TimerId, Topic}; @@ -476,6 +476,9 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon }; } + // Enable IME so we can input into the search bar with it if we were in Vi mode. + self.window().set_ime_allowed(true); + self.display.pending_update.dirty = true; } @@ -786,7 +789,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon /// Toggle the vi mode status. #[inline] fn toggle_vi_mode(&mut self) { - if self.terminal.mode().contains(TermMode::VI) { + let was_in_vi_mode = self.terminal.mode().contains(TermMode::VI); + if was_in_vi_mode { // If we had search running when leaving Vi mode we should mark terminal fully damaged // to cleanup highlighted results. if self.search_state.dfas.take().is_some() { @@ -803,6 +807,9 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.cancel_search(); } + // We don't want IME in Vi mode. + self.window().set_ime_allowed(was_in_vi_mode); + self.terminal.toggle_vi_mode(); *self.dirty = true; @@ -936,6 +943,9 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { /// Cleanup the search state. fn exit_search(&mut self) { + let vi_mode = self.terminal.mode().contains(TermMode::VI); + self.window().set_ime_allowed(!vi_mode); + self.display.pending_update.dirty = true; self.search_state.history_index = None; @@ -955,7 +965,8 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { // Check terminal cursor style. let terminal_blinking = self.terminal.cursor_style().blinking; let mut blinking = cursor_style.blinking_override().unwrap_or(terminal_blinking); - blinking &= vi_mode || self.terminal().mode().contains(TermMode::SHOW_CURSOR); + blinking &= (vi_mode || self.terminal().mode().contains(TermMode::SHOW_CURSOR)) + && self.display().ime.preedit().is_none(); // Update cursor blinking state. let window_id = self.display.window.id(); @@ -1216,12 +1227,37 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { *self.ctx.dirty = true; } }, - WindowEvent::Ime(ime) => { - if let Ime::Commit(text) = ime { + WindowEvent::Ime(ime) => match ime { + Ime::Commit(text) => { + // Clear preedit. + self.ctx.display.ime.set_preedit(None); + *self.ctx.dirty = true; + for ch in text.chars() { - self.received_char(ch) + self.received_char(ch); } - } + + self.ctx.update_cursor_blinking(); + }, + Ime::Preedit(text, cursor_offset) => { + let preedit = if text.is_empty() { + None + } else { + Some(Preedit::new(text, cursor_offset.map(|offset| offset.0))) + }; + + self.ctx.display.ime.set_preedit(preedit); + self.ctx.update_cursor_blinking(); + *self.ctx.dirty = true; + }, + Ime::Enabled => { + self.ctx.display.ime.set_enabled(true); + *self.ctx.dirty = true; + }, + Ime::Disabled => { + self.ctx.display.ime.set_enabled(false); + *self.ctx.dirty = true; + }, }, WindowEvent::KeyboardInput { is_synthetic: true, .. } | WindowEvent::TouchpadPressure { .. } diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 35aaedda..a612db12 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -754,6 +754,11 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { /// Process key input. pub fn key_input(&mut self, input: KeyboardInput) { + // IME input will be applied on commit and shouldn't trigger key bindings. + if self.ctx.display().ime.preedit().is_some() { + return; + } + // All key bindings are disabled while a hint is being selected. if self.ctx.display().hint_state.active() { *self.ctx.suppress_chars() = false; @@ -801,6 +806,11 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { pub fn received_char(&mut self, c: char) { let suppress_chars = *self.ctx.suppress_chars(); + // Don't insert chars when we have IME running. + if self.ctx.display().ime.preedit().is_some() { + return; + } + // Handle hint selection over anything else. if self.ctx.display().hint_state.active() && !suppress_chars { self.ctx.hint_input(c); diff --git a/alacritty/src/string.rs b/alacritty/src/string.rs index 4a758b34..a111166d 100644 --- a/alacritty/src/string.rs +++ b/alacritty/src/string.rs @@ -5,7 +5,7 @@ use std::str::Chars; use unicode_width::UnicodeWidthChar; /// The action performed by [`StrShortener`]. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TextAction { /// Yield a spacer. Spacer, @@ -93,7 +93,7 @@ impl<'a> StrShortener<'a> { let num_chars = iter.last().map_or(offset, |(idx, _)| idx + 1); let skip_chars = num_chars - offset; - let text_action = if num_chars <= max_width || shortener.is_none() { + let text_action = if current_len < max_width || shortener.is_none() { TextAction::Char } else { TextAction::Shortener @@ -203,8 +203,8 @@ mod tests { &StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>() ); - let s = "こJんにちはP"; - let len = 2 + 1 + 2 + 2 + 2 + 2 + 1; + let s = "ちはP"; + let len = 2 + 2 + 1; assert_eq!( ".", &StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>() @@ -226,7 +226,7 @@ mod tests { ); assert_eq!( - "こ .", + "ち .", &StrShortener::new(s, 3, ShortenDirection::Right, Some('.')).collect::<String>() ); @@ -236,12 +236,12 @@ mod tests { ); assert_eq!( - "こ Jん に ち は P", + "ち は P", &StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>() ); assert_eq!( - "こ Jん に ち は P", + "ち は P", &StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>() ); } |