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:
parent
791f79a02a
commit
18f9c27939
7 changed files with 296 additions and 46 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 { .. }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>()
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue