aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKirill Chibisov <contact@kchibisov.com>2022-08-29 16:29:13 +0300
committerGitHub <noreply@github.com>2022-08-29 16:29:13 +0300
commit18f9c2793924aec91c80a69ccb45f529adaffae5 (patch)
tree63cda75c8203c39a7437bd1812653f74494f878f
parent791f79a02a4bbb509c257af2849e411d32f4c18b (diff)
downloadalacritty-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.md4
-rw-r--r--alacritty/src/display/content.rs5
-rw-r--r--alacritty/src/display/mod.rs251
-rw-r--r--alacritty/src/display/window.rs8
-rw-r--r--alacritty/src/event.rs50
-rw-r--r--alacritty/src/input.rs10
-rw-r--r--alacritty/src/string.rs14
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>()
);
}