diff --git a/src/editor.rs b/src/editor.rs index cec967a..9646e8c 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -151,16 +151,34 @@ impl Editor { let line_number_width = self.get_line_number_width(); let line = self.code.char_to_line(self.cursor); - let col = self.cursor - self.code.line_to_char(line); + let line_start_char = self.code.line_to_char(line); + let line_len = self.code.line_len(line); + let col = self.cursor - line_start_char; let visible_width = width.saturating_sub(line_number_width); let visible_height = height; let step_size = 10; - if col < self.offset_x { - self.offset_x = col.saturating_sub(step_size); - } else if col >= self.offset_x + visible_width { - self.offset_x = col.saturating_sub(visible_width.saturating_sub(step_size)); + + let visual_col_of = |char_col: usize| -> usize { + let slice = self + .code + .char_slice(line_start_char, line_start_char + char_col.min(line_len)); + RopeGraphemes::new(&slice).map(grapheme_width).sum() + }; + + let cursor_visual_col = visual_col_of(col); + let offset_visual_col = visual_col_of(self.offset_x); + + if cursor_visual_col < offset_visual_col { + let target_visual_col = cursor_visual_col.saturating_sub(step_size); + self.offset_x = + Self::char_idx_for_visual_col(&self.code, line_start_char, line_len, target_visual_col); + } else if cursor_visual_col >= offset_visual_col + visible_width { + let target_visual_col = + cursor_visual_col.saturating_sub(visible_width.saturating_sub(step_size)); + self.offset_x = + Self::char_idx_for_visual_col(&self.code, line_start_char, line_len, target_visual_col); } let visual_line = self.visual_line_idx(line); @@ -175,6 +193,28 @@ impl Editor { } } + /// Finds the char index (relative to `line_start_char`) whose cumulative grapheme + /// display width first reaches `target_visual_col`, so it can be assigned to `offset_x`. + fn char_idx_for_visual_col( + code: &Code, + line_start_char: usize, + line_len: usize, + target_visual_col: usize, + ) -> usize { + let slice = code.char_slice(line_start_char, line_start_char + line_len); + let mut visual_col = 0; + let mut char_idx = 0; + for g in RopeGraphemes::new(&slice) { + if visual_col >= target_visual_col { + break; + } + let (g_width, g_chars) = grapheme_width_and_chars_len(g); + visual_col += g_width; + char_idx += g_chars; + } + char_idx + } + /// Handles a mouse button press at the given cursor position, updating selection and click state. pub fn handle_mouse_down(&mut self, cursor: usize) { let kind = self.clicks.register(cursor); diff --git a/tests/input.rs b/tests/input.rs index 7e13a06..e73ee1b 100644 --- a/tests/input.rs +++ b/tests/input.rs @@ -45,3 +45,16 @@ fn toggle_fold_at_cursor_toggles_cursor_line_fold() { editor.set_cursor(source.find("value").unwrap()); assert!(editor.get_visible_cursor(&area).is_none()); } + +#[test] +fn focus_scrolls_horizontally_for_wide_glyph_lines() { + let line: String = std::iter::repeat('界').take(20).collect(); + let mut editor = Editor::new("text", &line, vec![]).unwrap(); + let area = Rect::new(0, 0, 40, 10); + + editor.set_cursor(line.chars().count()); + editor.focus(&area); + + assert!(editor.get_offset_x() > 0); + assert!(editor.get_visible_cursor(&area).is_some()); +}