1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2025-04-14 17:53:03 -04:00

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.
This commit is contained in:
Kirill Chibisov 2022-08-29 16:29:13 +03:00 committed by GitHub
parent 791f79a02a
commit 18f9c27939
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 296 additions and 46 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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));
}

View file

@ -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 { .. }

View file

@ -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);

View file

@ -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>()
);
}