diff --git a/Cargo.lock b/Cargo.lock index 70b104d..7355086 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,7 +498,7 @@ version = "0.0.1" dependencies = [ "textwrap", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -611,7 +611,7 @@ dependencies = [ "temp-dir", "thiserror", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -627,6 +627,7 @@ dependencies = [ "regex", "serde", "serde_json", + "unicode-width 0.2.0", ] [[package]] @@ -863,7 +864,7 @@ dependencies = [ "strum_macros", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1139,7 +1140,7 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1213,7 +1214,7 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1222,6 +1223,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "version_check" version = "0.9.4" diff --git a/crates/modalkit-ratatui/Cargo.toml b/crates/modalkit-ratatui/Cargo.toml index 60cd35d..9a6a015 100644 --- a/crates/modalkit-ratatui/Cargo.toml +++ b/crates/modalkit-ratatui/Cargo.toml @@ -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 } diff --git a/crates/modalkit-ratatui/src/textbox.rs b/crates/modalkit-ratatui/src/textbox.rs index 8a72ccd..b021a20 100644 --- a/crates/modalkit-ratatui/src/textbox.rs +++ b/crates/modalkit-ratatui/src/textbox.rs @@ -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. @@ -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); @@ -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); @@ -1057,14 +1072,21 @@ 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 { @@ -1072,11 +1094,6 @@ where 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); @@ -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()); + } }