Skip to content

Commit

Permalink
Use Unicode column width when calculating terminal cursor offset (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
simnalamburt authored Oct 1, 2024
1 parent ead4546 commit 45855da
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 22 deletions.
17 changes: 12 additions & 5 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/modalkit-ratatui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ modalkit = { workspace = true }
ratatui = { version = "0.28.1" }
regex = { workspace = true }
serde = { version = "^1.0", features = ["derive"] }
unicode-width = "0.2.0"

[dev-dependencies]
rand = { workspace = true }
Expand Down
93 changes: 76 additions & 17 deletions crates/modalkit-ratatui/src/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ use modalkit::editing::{
use modalkit::errors::{EditError, EditResult, UIResult};
use modalkit::prelude::*;

use unicode_width::UnicodeWidthStr;

use super::{ScrollActions, TerminalCursor, WindowOps};

/// Line annotation shown in the left gutter.
Expand Down Expand Up @@ -859,13 +861,19 @@ where
}
}

let _ = buf.set_stringn(x, y, s, width, self.style);

if cursor_line {
let coff = (cursor.x - start) as u16;
let coff = s[..s
.char_indices()
.map(|(i, _)| i)
.nth(cursor.x.saturating_sub(start))
.unwrap_or(s.len())]
.width_cjk() as u16;

state.term_cursor = (x + coff, y);
}

let _ = buf.set_stringn(x, y, s, width, self.style);

self._highlight_followers(line, start, end, (x, y), &finfo, buf);
self._highlight_line(line, start, end, (x, y), &hinfo, buf);

Expand Down Expand Up @@ -1002,13 +1010,20 @@ where

let s = s.to_string();
let w = (right - x) as usize;
let (xres, _) = buf.set_stringn(x, y, s, w, self.style);

if cursor_line {
let coff = cursor.x.saturating_sub(start) as u16;
let coff = s[..s
.char_indices()
.map(|(i, _)| i)
.nth(cursor.x.saturating_sub(start))
.unwrap_or(s.len())]
.width_cjk() as u16;

state.term_cursor = (x + coff, y);
}

let (xres, _) = buf.set_stringn(x, y, s, w, self.style);

self._highlight_followers(line, start, end, (x, y), &finfo, buf);
self._highlight_line(line, start, end, (x, y), &hinfo, buf);

Expand Down Expand Up @@ -1057,26 +1072,28 @@ where
lgi.render(lga, buf);
}

let s = s.slice(CharOff::from(start)..CharOff::from(end)).to_string();

if line == cursor.y && (start..=end).contains(&cursor.x) {
let coff = s[..s
.char_indices()
.map(|(i, _)| i)
.nth(cursor.x.saturating_sub(start))
.unwrap_or(s.len())]
.width_cjk() as u16;

state.term_cursor = (x + coff, y);
}

if cbx < slen {
let _ = buf.set_stringn(
x,
y,
s.slice(CharOff::from(start)..CharOff::from(end)).to_string(),
width,
self.style,
);
let _ = buf.set_stringn(x, y, s, width, self.style);
}

if let Some(rgi) = rgutter {
let rga = Rect::new(gutters.1.x, y, gutters.1.width, 0);
rgi.render(rga, buf);
}

if line == cursor.y && (start..=end).contains(&cursor.x) {
let coff = (cursor.x - start) as u16;
state.term_cursor = (x + coff, y);
}

self._highlight_followers(line, start, end, (x, y), &finfo, buf);
self._highlight_line(line, start, end, (x, y), &hinfo, buf);

Expand Down Expand Up @@ -1521,4 +1538,46 @@ mod tests {
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.get_term_cursor(), (2, 8).into());
}

#[test]
fn test_wide_char_cursor() {
let (mut tbox, ctx, mut store) = mkboxstr("세계를 향한 대화\n");

let area = Rect::new(0, 0, 20, 20);
let mut buffer = Buffer::empty(area);

// Prompt should push everything right by 2 characters.
TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);

assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.get_term_cursor(), (2, 0).into());

// Move the cursor to be over "대", just before the last character.
let mov = mv!(MoveType::Column(MoveDir1D::Next, false), 7);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();

// Draw again to update our terminal cursor using oneline().
TextBox::new().prompt("> ").oneline().render(area, &mut buffer, &mut tbox);

assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 7));
assert_eq!(tbox.get_term_cursor(), (14, 0).into());
// Draw again to update our terminal cursor using set_wrap(true).
tbox.set_wrap(true);
TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);

assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 7));
assert_eq!(tbox.get_term_cursor(), (14, 0).into());

// Draw again to update our terminal cursor using set_wrap(false).
tbox.set_wrap(true);
TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);

assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 7));
assert_eq!(tbox.get_term_cursor(), (14, 0).into());
}
}

0 comments on commit 45855da

Please sign in to comment.