Skip to content

Commit

Permalink
Add inline input method support
Browse files Browse the repository at this point in the history
This commit adds support for inline IME handling. It also makes search
bar draw proper Underline cursor instead of using '_' character.

Fixes #1613.
  • Loading branch information
kchibisov committed Jan 21, 2022
1 parent c4d610d commit 1aae1f8
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 36 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- Minimum Rust version has been bumped to 1.56.0

### Added

- Inline input method support

### Changed

- The `--help` output was reworked with a new colorful syntax
- Search bar is now respecting cursor thickness

### Fixed

Expand Down
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ members = [
lto = true
debug = 1
incremental = false

[patch.crates-io]
winit = { git = "https://github.com/rust-windowing/winit", branch = "composition-event" }
5 changes: 5 additions & 0 deletions alacritty/src/display/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ impl<'a> RenderableContent<'a> {
// Find terminal cursor shape.
let cursor_shape = if terminal_content.cursor.shape == CursorShape::Hidden
|| display.cursor_hidden
|| display.ime_input.is_some()
|| search_state.regex().is_some()
{
CursorShape::Hidden
Expand Down Expand Up @@ -372,6 +373,10 @@ pub struct RenderableCursor {
}

impl RenderableCursor {
pub fn new(point: Point<usize>, shape: CursorShape, cursor_color: Rgb, is_wide: bool) -> Self {
Self { point, shape, cursor_color, text_color: cursor_color, is_wide }
}

pub fn color(&self) -> Rgb {
self.cursor_color
}
Expand Down
179 changes: 147 additions & 32 deletions alacritty/src/display/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ use unicode_width::UnicodeWidthChar;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
use wayland_client::EventQueue;

use crossfont::{self, Rasterize, Rasterizer};
use crossfont::{self, Metrics, Rasterize, Rasterizer};

use alacritty_terminal::ansi::NamedColor;
use alacritty_terminal::ansi::{CursorShape, NamedColor};
use alacritty_terminal::event::{EventListener, OnResize};
use alacritty_terminal::grid::Dimensions as _;
use alacritty_terminal::index::{Column, Direction, Line, Point};
use alacritty_terminal::selection::Selection;
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES};

use crate::config::font::Font;
Expand All @@ -38,14 +39,14 @@ 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::hint::{HintMatch, HintState};
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, QuadRenderer};

pub mod content;
Expand Down Expand Up @@ -193,11 +194,33 @@ pub struct Display {
/// Unprocessed display updates.
pub pending_update: DisplayUpdate,

/// Unprocessed IME input.
pub ime_input: Option<IMEInput>,

renderer: QuadRenderer,
glyph_cache: GlyphCache,
meter: Meter,
}

/// A state of IME input into the window.
#[derive(Debug, Default)]
pub struct IMEInput {
/// The text being preedit.
preedit: String,

/// Byte offset for cursor start in text.
cursor_start: Option<usize>,

/// Byte offset for cursor end in text.
_cursor_end: Option<usize>,
}

impl IMEInput {
pub fn new(text: String, cursor_start: Option<usize>, cursor_end: Option<usize>) -> Self {
Self { preedit: text, cursor_start, _cursor_end: cursor_end }
}
}

impl Display {
pub fn new<E>(
config: &UiConfig,
Expand Down Expand Up @@ -335,6 +358,7 @@ impl Display {
visual_bell: VisualBell::from(&config.bell),
colors: List::from(&config.colors),
pending_update: Default::default(),
ime_input: None,
})
}

Expand Down Expand Up @@ -461,6 +485,81 @@ impl Display {
info!("Width: {}, Height: {}", self.size_info.width(), self.size_info.height());
}

/// Draws preedit text *ineline* from `point` using given foreground and background colors.
fn draw_inline_preedit(
&mut self,
point: Point<usize>,
fg: Rgb,
bg: Rgb,
rects: &mut Vec<RenderRect>,
metrics: &Metrics,
config: &UiConfig,
) {
let size_info = &self.size_info;
let ime_input = match self.ime_input.as_ref() {
Some(ime_input) => ime_input,
None => {
let ime_popup_point = Point::new(Line(point.line as i32), point.column);
self.window.update_ime_position(ime_popup_point, size_info);
return;
},
};

let mut preedit = String::new();
let mut is_wide = false;
let mut cursor_index = None;
for (byte, ch) in ime_input.preedit.char_indices() {
let ch_width = ch.width().unwrap_or_default();
if Some(byte) == ime_input.cursor_start {
cursor_index = Some(preedit.chars().count());
is_wide = ch_width == 2;
}

preedit.push(ch);
if ch_width == 2 {
preedit.push(' ');
}
}

// Truncate preedit line.
let num_cols = size_info.columns();
let total_len = preedit.chars().count();
let truncate_len = min((total_len).saturating_sub(num_cols), total_len);
let index = preedit.char_indices().nth(truncate_len).map(|(i, _c)| i).unwrap_or(0);

let glyph_cache = &mut self.glyph_cache;

// Compute start and end point of visible preedit text.
let end = min(point.column.0 + total_len, num_cols);
let start = end.saturating_sub(total_len);
let start = Point::new(point.line, Column(start));
let end = Point::new(point.line, Column(end - 1));

self.renderer.with_api(config, size_info, |mut api| {
api.draw_string(glyph_cache, start, fg, bg, &preedit[index..]);
});

// Add underline under preedit text.
let underline = RenderLine { start, end, color: fg };
rects.extend(underline.rects(Flags::UNDERLINE, metrics, size_info));

// Draw cursor if visible.
let ime_popup_point = match cursor_index {
Some(cursor_index) if cursor_index >= total_len.saturating_sub(num_cols) => {
let cursor_column = end.column - (total_len - (cursor_index + 1));

let cursor = Point::new(start.line, cursor_column);
let cursor = RenderableCursor::new(cursor, CursorShape::HollowBlock, fg, is_wide);
rects.extend(cursor.rects(size_info, config.terminal_config.cursor.thickness()));
Point::new(Line(start.line as i32), cursor_column)
},
// The cursor isn't visible or is at the last column, thus use `end`.
_ => Point::new(Line(end.line as i32), end.column),
};

self.window.update_ime_position(ime_popup_point, size_info);
}

/// Draw the screen.
///
/// A reference to Term whose state is being drawn must be provided.
Expand All @@ -480,6 +579,7 @@ impl Display {
grid_cells.push(cell);
}
let background_color = content.color(NamedColor::Background as usize);
let foreground_color = content.color(NamedColor::Foreground as usize);
let display_offset = content.display_offset();
let cursor = content.cursor();

Expand Down Expand Up @@ -551,9 +651,7 @@ impl Display {

// Push the cursor rects for rendering.
if let Some(cursor) = 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.
Expand All @@ -570,6 +668,46 @@ impl Display {
rects.push(visual_bell_rect);
}

// Handle search and IME drawing/positioning.
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(&size_info, regex, search_label);

// Render the search bar.
self.draw_search(config, &size_info, &search_text);

// Compute IME position.
let line = size_info.screen_lines();
let column = Column(search_text.chars().count() - 1);
let ime_point = Point::new(line, column);

let fg = config.colors.search_bar_foreground();
let bg = config.colors.search_bar_background();
self.draw_inline_preedit(ime_point, fg, bg, &mut rects, &metrics, config);

// Push search bar cursor if there's no IME.
if self.ime_input.is_none() {
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()),
);
}
},
None => {
let fg = foreground_color;
let bg = background_color;
let point =
Point::new(cursor_point.line.0 as usize + display_offset, cursor_point.column);
self.draw_inline_preedit(point, fg, bg, &mut rects, &metrics, config);
},
};

if let Some(message) = message_buffer.message() {
let search_offset = if search_state.regex().is_some() { 1 } else { 0 };
let text = message.text(&size_info);
Expand Down Expand Up @@ -608,29 +746,6 @@ impl Display {

self.draw_render_timer(config, &size_info);

// 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(&size_info, regex, search_label);

// Render the search bar.
self.draw_search(config, &size_info, &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,
};

// 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))))]
Expand Down Expand Up @@ -715,8 +830,8 @@ impl Display {
}
}

// Add cursor to show whitespace.
formatted_regex.push('_');
// Add spacer for cursor to show whitespace.
formatted_regex.push(' ');

// Truncate beginning of the search regex if it exceeds the viewport width.
let num_cols = size_info.columns();
Expand Down
26 changes: 24 additions & 2 deletions alacritty/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use std::time::{Duration, Instant};
use std::{env, f32, mem};

use glutin::dpi::PhysicalSize;
use glutin::event::{ElementState, Event as GlutinEvent, ModifiersState, MouseButton, WindowEvent};
use glutin::event::{
ElementState, Event as GlutinEvent, ModifiersState, MouseButton, WindowEvent, IME,
};
use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget};
use glutin::platform::run_return::EventLoopExtRunReturn;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
Expand Down Expand Up @@ -43,7 +45,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::{self, Display};
use crate::display::{self, Display, IMEInput};
use crate::input::{self, ActionContext as _, FONT_SIZE_STEP};
use crate::message_bar::{Message, MessageBuffer};
use crate::scheduler::{Scheduler, TimerId, Topic};
Expand Down Expand Up @@ -1106,6 +1108,26 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => {
self.key_input(input);
},
WindowEvent::IME(ime) => match ime {
// TODO decide what to do with it, but in general we should stop update of
// IME input methods.
IME::Enabled => {},
// The text is commited thus should be written to PTY.
IME::Commit(text) => {
self.ctx.display.ime_input = None;
text.chars().for_each(|ch| self.received_char(ch));
},
// This text is being composed, thus should be shown as inline input to
// the user.
IME::Preedit(text, cursor_start, cursor_end) => {
*self.ctx.dirty = true;
self.ctx.display.ime_input =
Some(IMEInput::new(text, cursor_start, cursor_end));
},
IME::Disabled => {
self.ctx.display.ime_input = None;
},
},
WindowEvent::ModifiersChanged(modifiers) => self.modifiers_input(modifiers),
WindowEvent::ReceivedCharacter(c) => self.received_char(c),
WindowEvent::MouseInput { state, button, .. } => {
Expand Down

0 comments on commit 1aae1f8

Please sign in to comment.