Skip to content

Commit 481577f

Browse files
committed
Fix IME preedit overlay and add tests
1 parent b65c72f commit 481577f

File tree

3 files changed

+168
-10
lines changed

3 files changed

+168
-10
lines changed

frontends/rioterm/src/application.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,14 @@ impl ApplicationHandler<EventPayload> for Application<'_> {
12491249
if route.window.screen.context_manager.current().ime.preedit()
12501250
!= preedit.as_ref()
12511251
{
1252+
route
1253+
.window
1254+
.screen
1255+
.context_manager
1256+
.current_mut()
1257+
.renderable_content
1258+
.pending_update
1259+
.set_ui_damage(rio_backend::event::TerminalDamage::Full);
12521260
route
12531261
.window
12541262
.screen

frontends/rioterm/src/ime.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,13 @@ pub struct Preedit {
5656

5757
impl Preedit {
5858
pub fn new(text: String, cursor_byte_offset: Option<usize>) -> Self {
59-
let cursor_end_offset = if let Some(byte_offset) = cursor_byte_offset {
60-
// Convert byte offset into char offset.
61-
let cursor_end_offset = text[byte_offset..]
59+
let cursor_byte_offset =
60+
cursor_byte_offset.filter(|&byte_offset| text.is_char_boundary(byte_offset));
61+
let cursor_end_offset = cursor_byte_offset.map(|byte_offset| {
62+
text[byte_offset..]
6263
.chars()
63-
.fold(0, |acc, ch| acc + ch.width().unwrap_or(1));
64-
65-
Some(cursor_end_offset)
66-
} else {
67-
None
68-
};
64+
.fold(0, |acc, ch| acc + ch.width().unwrap_or(1))
65+
});
6966

7067
Self {
7168
text,
@@ -74,3 +71,22 @@ impl Preedit {
7471
}
7572
}
7673
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::*;
78+
79+
#[test]
80+
fn preedit_new_rejects_invalid_byte_offset() {
81+
let preedit = Preedit::new("啊a".to_string(), Some(1));
82+
assert!(preedit.cursor_byte_offset.is_none());
83+
assert!(preedit.cursor_end_offset.is_none());
84+
}
85+
86+
#[test]
87+
fn preedit_new_computes_cursor_end_offset() {
88+
let preedit = Preedit::new("啊a".to_string(), Some(0));
89+
assert_eq!(preedit.cursor_byte_offset, Some(0));
90+
assert_eq!(preedit.cursor_end_offset, Some(3));
91+
}
92+
}

frontends/rioterm/src/renderer/mod.rs

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,86 @@ use rio_backend::sugarloaf::{
3131
use std::collections::{BTreeSet, HashMap};
3232
use std::ops::RangeInclusive;
3333

34+
use crate::ime::Preedit;
3435
use unicode_width::UnicodeWidthChar;
3536

37+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38+
enum PreeditCell {
39+
Char(char),
40+
Spacer,
41+
}
42+
43+
struct PreeditOverlay {
44+
columns: usize,
45+
cells: Vec<Option<PreeditCell>>,
46+
}
47+
48+
impl PreeditOverlay {
49+
fn new(
50+
preedit: &Preedit,
51+
start_row: usize,
52+
start_col: usize,
53+
columns: usize,
54+
rows: usize,
55+
) -> Option<Self> {
56+
if preedit.text.is_empty() || columns == 0 || rows == 0 {
57+
return None;
58+
}
59+
60+
let mut cells = vec![None; rows.saturating_mul(columns)];
61+
let mut row = start_row;
62+
let mut col = start_col;
63+
64+
for ch in preedit.text.chars() {
65+
if row >= rows {
66+
break;
67+
}
68+
69+
if col >= columns {
70+
row += 1;
71+
col = 0;
72+
}
73+
if row >= rows {
74+
break;
75+
}
76+
77+
let width = ch.width().unwrap_or(1).max(1);
78+
if width > 1 && col + 1 >= columns {
79+
row += 1;
80+
col = 0;
81+
if row >= rows {
82+
break;
83+
}
84+
}
85+
86+
let idx = row * columns + col;
87+
if let Some(cell) = cells.get_mut(idx) {
88+
*cell = Some(PreeditCell::Char(ch));
89+
}
90+
91+
if width > 1 && col + 1 < columns {
92+
let spacer_idx = idx + 1;
93+
if let Some(cell) = cells.get_mut(spacer_idx) {
94+
*cell = Some(PreeditCell::Spacer);
95+
}
96+
}
97+
98+
col = col.saturating_add(width);
99+
if col >= columns {
100+
row += 1;
101+
col = 0;
102+
}
103+
}
104+
105+
Some(Self { columns, cells })
106+
}
107+
108+
fn get(&self, row: usize, col: usize) -> Option<PreeditCell> {
109+
let idx = row.checked_mul(self.columns)?.saturating_add(col);
110+
self.cells.get(idx).copied().flatten()
111+
}
112+
}
113+
36114
#[derive(Default)]
37115
pub struct Search {
38116
rich_text_id: Option<usize>,
@@ -256,9 +334,11 @@ impl Renderer {
256334
builder: &mut Content,
257335
row: &Row<Square>,
258336
has_cursor: bool,
337+
visible_row_index: usize,
259338
line_opt: Option<usize>,
260339
line: Line,
261340
renderable_content: &RenderableContent,
341+
ime_preedit: Option<&PreeditOverlay>,
262342
hint_matches: Option<&[rio_backend::crosswords::search::Match]>,
263343
focused_match: &Option<RangeInclusive<Pos>>,
264344
term_colors: &TermColors,
@@ -279,8 +359,10 @@ impl Renderer {
279359
// First pass: collect all styles and identify font cache misses
280360
for column in 0..columns {
281361
let square = &row.inner[column];
362+
let preedit_cell =
363+
ime_preedit.and_then(|preedit| preedit.get(visible_row_index, column));
282364

283-
if square.flags.contains(Flags::WIDE_CHAR_SPACER) {
365+
if square.flags.contains(Flags::WIDE_CHAR_SPACER) && preedit_cell.is_none() {
284366
continue;
285367
}
286368

@@ -374,10 +456,28 @@ impl Renderer {
374456
);
375457
}
376458

459+
if let Some(cell) = preedit_cell {
460+
match cell {
461+
PreeditCell::Char(ch) => {
462+
square_content = ch;
463+
}
464+
PreeditCell::Spacer => {
465+
square_content = ' ';
466+
}
467+
}
468+
if !(has_cursor && column == cursor.state.pos.col) {
469+
style.color =
470+
self.color(NamedColor::DimForeground as usize, term_colors);
471+
style.decoration = None;
472+
style.decoration_color = None;
473+
}
474+
}
475+
377476
if !is_active {
378477
style.color[3] = self.unfocused_split_opacity;
379478
if let Some(mut background_color) = style.background_color {
380479
background_color[3] = self.unfocused_split_opacity;
480+
style.background_color = Some(background_color);
381481
}
382482
}
383483

@@ -940,6 +1040,15 @@ impl Renderer {
9401040

9411041
// Update cursor state from snapshot
9421042
context.renderable_content.cursor.state = terminal_snapshot.cursor;
1043+
let preedit_overlay = context.ime.preedit().and_then(|preedit| {
1044+
PreeditOverlay::new(
1045+
preedit,
1046+
context.renderable_content.cursor.state.pos.row.0.max(0) as usize,
1047+
context.renderable_content.cursor.state.pos.col.0,
1048+
terminal_snapshot.columns,
1049+
terminal_snapshot.visible_rows.len(),
1050+
)
1051+
});
9431052

9441053
let mut specific_lines: Option<BTreeSet<LineDamage>> = None;
9451054

@@ -1037,9 +1146,11 @@ impl Renderer {
10371146
content,
10381147
row,
10391148
has_cursor,
1149+
i,
10401150
None,
10411151
Line((i as i32) - terminal_snapshot.display_offset as i32),
10421152
&context.renderable_content,
1153+
preedit_overlay.as_ref(),
10431154
hint_matches,
10441155
focused_match,
10451156
&terminal_snapshot.colors,
@@ -1063,12 +1174,14 @@ impl Renderer {
10631174
content,
10641175
visible_row,
10651176
has_cursor,
1177+
line,
10661178
Some(line),
10671179
Line(
10681180
(line as i32)
10691181
- terminal_snapshot.display_offset as i32,
10701182
),
10711183
&context.renderable_content,
1184+
preedit_overlay.as_ref(),
10721185
hint_matches,
10731186
focused_match,
10741187
&terminal_snapshot.colors,
@@ -1314,4 +1427,25 @@ mod tests {
13141427
Pos::new(Line(2), Column(12))
13151428
));
13161429
}
1430+
1431+
#[test]
1432+
fn preedit_overlay_places_wide_chars_and_spacers() {
1433+
let preedit = Preedit::new("啊a".to_string(), None);
1434+
let overlay = PreeditOverlay::new(&preedit, 0, 0, 4, 1).unwrap();
1435+
1436+
assert_eq!(overlay.get(0, 0), Some(PreeditCell::Char('啊')));
1437+
assert_eq!(overlay.get(0, 1), Some(PreeditCell::Spacer));
1438+
assert_eq!(overlay.get(0, 2), Some(PreeditCell::Char('a')));
1439+
assert_eq!(overlay.get(0, 3), None);
1440+
}
1441+
1442+
#[test]
1443+
fn preedit_overlay_wraps_wide_chars() {
1444+
let preedit = Preedit::new("啊".to_string(), None);
1445+
let overlay = PreeditOverlay::new(&preedit, 0, 2, 3, 2).unwrap();
1446+
1447+
assert_eq!(overlay.get(0, 2), None);
1448+
assert_eq!(overlay.get(1, 0), Some(PreeditCell::Char('啊')));
1449+
assert_eq!(overlay.get(1, 1), Some(PreeditCell::Spacer));
1450+
}
13171451
}

0 commit comments

Comments
 (0)