editor: Add to support move vertical for soft wrapped lines. (#1312)

This commit is contained in:
Jason Lee 2025-10-03 18:19:37 +08:00 committed by GitHub
parent f59bac8c8b
commit 794f4d6397
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 477 additions and 238 deletions

View file

@ -6,6 +6,7 @@ mod element;
mod lsp;
mod mask_pattern;
mod mode;
mod movement;
mod number_input;
mod otp_input;
pub(crate) mod popovers;

View file

@ -0,0 +1,234 @@
use gpui::{Context, Point, Window};
use crate::input::{
InputState, MoveDown, MoveEnd, MoveHome, MoveLeft, MovePageDown, MovePageUp, MoveRight,
MoveToEnd, MoveToNextWord, MoveToPreviousWord, MoveToStart, MoveUp, RopeExt as _,
};
impl InputState {
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
pub(super) fn update_preferred_column(&mut self) {
let Some(last_layout) = &self.last_layout else {
self.preferred_column = None;
return;
};
let point = self.text.offset_to_point(self.cursor());
let row = point.row.saturating_sub(last_layout.visible_range.start);
let Some(line) = last_layout.lines.get(row) else {
self.preferred_column = None;
return;
};
let Some(pos) = line.position_for_index(point.column, last_layout.line_height) else {
self.preferred_column = None;
return;
};
self.preferred_column = Some((pos.x, point.column));
}
/// Move the cursor to the given offset.
///
/// The offset is the UTF-8 offset.
///
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
pub(crate) fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
let offset = offset.clamp(0, self.text.len());
self.selected_range = (offset..offset).into();
self.scroll_to(offset, cx);
self.pause_blink_cursor(cx);
self.update_preferred_column();
self.hide_context_menu(cx);
cx.notify()
}
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
///
/// move_lines: Number of lines to move vertically (positive for down, negative for up).
pub(super) fn move_vertical(
&mut self,
move_lines: isize,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.mode.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let offset = self.cursor();
let was_preferred_column = self.preferred_column;
let mut display_point = self.text_wrapper.offset_to_display_point(offset);
display_point.row = display_point.row.saturating_add_signed(move_lines);
display_point.column = 0;
let mut new_offset = self.text_wrapper.display_point_to_offset(display_point);
if let Some((preferred_x, column)) = was_preferred_column {
// Get display point again to update local_row.
let mut next_display_point = self.text_wrapper.offset_to_display_point(new_offset);
next_display_point.column = 0;
let next_point = self.text_wrapper.display_point_to_point(next_display_point);
let line_start_offset = self.text.line_start_offset(next_point.row);
// If in visible range, prefer to use position to get column.
if let Some(line) = last_layout.line(next_point.row) {
if let Some(x) = line.closest_index_for_position(
Point {
x: preferred_x,
y: next_display_point.local_row * last_layout.line_height,
},
last_layout.line_height,
) {
new_offset = line_start_offset + x;
}
} else {
// Not in visible range, use column directly.
let max_line_len = self.text.slice_line(next_point.row).len();
new_offset = line_start_offset + column.min(max_line_len);
}
}
self.pause_blink_cursor(cx);
self.move_to(new_offset, cx);
// Set back the preferred_column
self.preferred_column = was_preferred_column;
cx.notify();
}
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor()), cx);
} else {
self.move_to(self.selected_range.start, cx)
}
}
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), cx);
} else {
self.move_to(self.selected_range.end, cx)
}
}
pub(super) fn up(&mut self, action: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) {
return;
}
if self.mode.is_single_line() {
return;
}
if !self.selected_range.is_empty() {
self.move_to(
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
cx,
);
}
self.pause_blink_cursor(cx);
self.move_vertical(-1, window, cx);
}
pub(super) fn down(&mut self, action: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) {
return;
}
if self.mode.is_single_line() {
return;
}
if !self.selected_range.is_empty() {
self.move_to(
self.next_boundary(self.selected_range.end.saturating_sub(1)),
cx,
);
}
self.pause_blink_cursor(cx);
self.move_vertical(1, window, cx);
}
pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
self.move_vertical(-display_lines, window, cx);
}
pub(super) fn page_down(
&mut self,
_: &MovePageDown,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.mode.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
self.move_vertical(display_lines, window, cx);
}
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
let offset = self.start_of_line();
self.move_to(offset, cx);
}
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
let offset = self.end_of_line();
self.move_to(offset, cx);
}
pub(super) fn move_to_start(
&mut self,
_: &MoveToStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.move_to(0, cx);
}
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(self.text.len(), cx);
}
pub(super) fn move_to_previous_word(
&mut self,
_: &MoveToPreviousWord,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.previous_start_of_word();
self.move_to(offset, cx);
}
pub(super) fn move_to_next_word(
&mut self,
_: &MoveToNextWord,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.next_end_of_word();
self.move_to(offset, cx);
}
}

View file

@ -249,6 +249,21 @@ pub(super) struct LastLayout {
pub(super) cursor_bounds: Option<Bounds<Pixels>>,
}
impl LastLayout {
/// Get the line layout for the given row (0-based).
///
/// 0 is the viewport first visible line.
///
/// Returns None if the row is out of range.
pub(crate) fn line(&self, row: usize) -> Option<&LineLayout> {
if row < self.visible_range.start || row >= self.visible_range.end {
return None;
}
self.lines.get(row.saturating_sub(self.visible_range.start))
}
}
/// InputState to keep editing state of the [`super::TextInput`].
pub struct InputState {
pub(super) focus_handle: FocusHandle,
@ -315,7 +330,7 @@ pub struct InputState {
///
/// The first element is the x-coordinate (Pixels), preferred to use this.
/// The second element is the column (usize), fallback to use this.
preferred_column: Option<(Pixels, usize)>,
pub(super) preferred_column: Option<(Pixels, usize)>,
_subscriptions: Vec<Subscription>,
pub(super) _context_menu_task: Task<Result<()>>,
@ -573,28 +588,6 @@ impl InputState {
cx.notify();
}
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
fn update_preferred_column(&mut self) {
let Some(last_layout) = &self.last_layout else {
self.preferred_column = None;
return;
};
let point = self.text.offset_to_point(self.cursor());
let row = point.row.saturating_sub(last_layout.visible_range.start);
let Some(line) = last_layout.lines.get(row) else {
self.preferred_column = None;
return;
};
let Some(pos) = line.position_for_index(point.column, last_layout.line_height) else {
self.preferred_column = None;
return;
};
self.preferred_column = Some((pos.x, point.column));
}
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
///
/// Returns:
@ -628,55 +621,6 @@ impl InputState {
(0, 0, None)
}
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
///
/// move_lines: Number of lines to move vertically (positive for down, negative for up).
fn move_vertical(&mut self, move_lines: isize, _: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let offset = self.cursor();
let was_preferred_column = self.preferred_column;
let row = self.text.offset_to_point(offset).row;
let new_row = row.saturating_add_signed(move_lines);
let line_start_offset = self
.text
.point_to_offset(tree_sitter::Point::new(new_row, 0));
let mut new_offset = line_start_offset;
if let Some((preferred_x, column)) = was_preferred_column {
let new_column = column.min(self.text.slice_line(new_row).len());
new_offset = line_start_offset + new_column;
// If in visible range, prefer to use position to get column.
if new_row >= last_layout.visible_range.start {
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
if let Some(line) = last_layout.lines.get(visible_row) {
if let Some(x) = line.closest_index_for_position(
Point {
x: preferred_x,
y: px(0.),
},
last_layout.line_height,
) {
new_offset = line_start_offset + x;
}
}
}
}
self.pause_blink_cursor(cx);
self.move_to(new_offset, cx);
// Set back the preferred_column
self.preferred_column = was_preferred_column;
cx.notify();
}
/// Set the text of the input field.
///
/// And the selection_range will be reset to 0..0.
@ -904,94 +848,6 @@ impl InputState {
});
}
pub(super) fn left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor()), cx);
} else {
self.move_to(self.selected_range.start, cx)
}
}
pub(super) fn right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), cx);
} else {
self.move_to(self.selected_range.end, cx)
}
}
pub(super) fn up(&mut self, action: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) {
return;
}
if self.mode.is_single_line() {
return;
}
if !self.selected_range.is_empty() {
self.move_to(
self.previous_boundary(self.selected_range.start.saturating_sub(1)),
cx,
);
}
self.pause_blink_cursor(cx);
self.move_vertical(-1, window, cx);
}
pub(super) fn down(&mut self, action: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
if self.handle_action_for_context_menu(Box::new(action.clone()), window, cx) {
return;
}
if self.mode.is_single_line() {
return;
}
if !self.selected_range.is_empty() {
self.move_to(
self.next_boundary(self.selected_range.end.saturating_sub(1)),
cx,
);
}
self.pause_blink_cursor(cx);
self.move_vertical(1, window, cx);
}
pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
self.move_vertical(-display_lines, window, cx);
}
pub(super) fn page_down(
&mut self,
_: &MovePageDown,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.mode.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
self.move_vertical(display_lines, window, cx);
}
pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(self.previous_boundary(self.cursor()), cx);
}
@ -1021,51 +877,6 @@ impl InputState {
cx.notify();
}
pub(super) fn home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
let offset = self.start_of_line();
self.move_to(offset, cx);
}
pub(super) fn end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
let offset = self.end_of_line();
self.move_to(offset, cx);
}
pub(super) fn move_to_start(
&mut self,
_: &MoveToStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.move_to(0, cx);
}
pub(super) fn move_to_end(&mut self, _: &MoveToEnd, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(self.text.len(), cx);
}
pub(super) fn move_to_previous_word(
&mut self,
_: &MoveToPreviousWord,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.previous_start_of_word();
self.move_to(offset, cx);
}
pub(super) fn move_to_next_word(
&mut self,
_: &MoveToNextWord,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.next_end_of_word();
self.move_to(offset, cx);
}
pub(super) fn select_to_start(
&mut self,
_: &SelectToStart,
@ -1126,7 +937,7 @@ impl InputState {
}
/// Return the start offset of the previous word.
fn previous_start_of_word(&mut self) -> usize {
pub(super) fn previous_start_of_word(&mut self) -> usize {
let offset = self.selected_range.start;
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
// FIXME: Avoid to_string
@ -1140,7 +951,7 @@ impl InputState {
}
/// Return the next end offset of the next word.
fn next_end_of_word(&mut self) -> usize {
pub(super) fn next_end_of_word(&mut self) -> usize {
let offset = self.cursor();
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
let right_part = self.text.slice(offset..self.text.len()).to_string();
@ -1152,7 +963,7 @@ impl InputState {
}
/// Get start of line byte offset of cursor
fn start_of_line(&self) -> usize {
pub(super) fn start_of_line(&self) -> usize {
if self.mode.is_single_line() {
return 0;
}
@ -1162,7 +973,7 @@ impl InputState {
}
/// Get end of line byte offset of cursor
fn end_of_line(&self) -> usize {
pub(super) fn end_of_line(&self) -> usize {
if self.mode.is_single_line() {
return self.text.len();
}
@ -1787,21 +1598,6 @@ impl InputState {
self.history.ignore = false;
}
/// Move the cursor to the given offset.
///
/// The offset is the UTF-8 offset.
///
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
pub(crate) fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
let offset = offset.clamp(0, self.text.len());
self.selected_range = (offset..offset).into();
self.scroll_to(offset, cx);
self.pause_blink_cursor(cx);
self.update_preferred_column();
self.hide_context_menu(cx);
cx.notify()
}
/// Get byte offset of the cursor.
///
/// The offset is the UTF-8 offset.
@ -2023,7 +1819,7 @@ impl InputState {
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
}
fn previous_boundary(&self, offset: usize) -> usize {
pub(super) fn previous_boundary(&self, offset: usize) -> usize {
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
if let Some(ch) = self.text.char_at(offset) {
if ch == '\r' {
@ -2034,7 +1830,7 @@ impl InputState {
offset
}
fn next_boundary(&self, offset: usize) -> usize {
pub(super) fn next_boundary(&self, offset: usize) -> usize {
let mut offset = self.text.clip_offset(offset + 1, Bias::Right);
if let Some(ch) = self.text.char_at(offset) {
if ch == '\r' {
@ -2078,7 +1874,7 @@ impl InputState {
cx.notify();
}
fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
self.blink_cursor.update(cx, |cursor, cx| {
cursor.pause(cx);
});

View file

@ -9,13 +9,11 @@ use crate::input::RopeExt;
/// A line with soft wrapped lines info.
#[derive(Debug, Clone)]
pub(super) struct LineItem {
/// The original line text.
/// The original line text, without end `\n`.
line: Rope,
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
///
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
/// like the `window.text_system().shape_text`. So, this value may not equal
/// the actual rendered lines.
/// Not contains the line end `\n`.
pub(super) wrapped_lines: Vec<Range<usize>>,
}
@ -233,8 +231,100 @@ impl TextWrapper {
fn update_all(&mut self, text: &Rope, cx: &mut App) {
self.update(text, &(0..text.len()), &text, cx);
}
/// Return display point (with soft wrap) from the given byte offset in the text.
///
/// Panics if the `offset` is out of bounds.
pub(crate) fn offset_to_display_point(&self, offset: usize) -> DisplayPoint {
let row = self.text.offset_to_point(offset).row;
let start = self.text.line_start_offset(row);
let line = &self.lines[row];
let mut wrapped_row = self
.lines
.iter()
.take(row)
.map(|l| l.lines_len())
.sum::<usize>();
let local_offset = offset.saturating_sub(start);
for (ix, range) in line.wrapped_lines.iter().enumerate() {
if range.contains(&local_offset) {
return DisplayPoint::new(
wrapped_row + ix,
ix,
local_offset.saturating_sub(range.start),
);
}
}
// Otherwise return the eof of the line.
let last_range = line.wrapped_lines.last().unwrap_or(&(0..0));
let ix = line.lines_len().saturating_sub(1);
return DisplayPoint::new(wrapped_row + ix, ix, last_range.len());
}
/// Return byte offset in the text from the given display point (with soft wrap).
///
/// Panics if the `point.row` is out of bounds.
pub(crate) fn display_point_to_offset(&self, point: DisplayPoint) -> usize {
let mut wrapped_row = 0;
for (row, line) in self.lines.iter().enumerate() {
if wrapped_row + line.lines_len() > point.row {
let line_start = self.text.line_start_offset(row);
let local_row = point.row.saturating_sub(wrapped_row);
if let Some(range) = line.wrapped_lines.get(local_row) {
return line_start + (range.start + point.column).min(range.end);
} else {
// If not found, return the end of the line.
return line_start + line.len();
}
}
wrapped_row += line.lines_len();
}
return self.text.len();
}
pub(crate) fn display_point_to_point(&self, point: DisplayPoint) -> tree_sitter::Point {
let offset = self.display_point_to_offset(point);
self.text.offset_to_point(offset)
}
pub(crate) fn point_to_display_point(&self, point: tree_sitter::Point) -> DisplayPoint {
let offset = self.text.point_to_offset(point);
self.offset_to_display_point(offset)
}
}
/// The actually display point in the text.
///
/// This is usually used to describe the
/// position in the text with `soft-wrap` mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisplayPoint {
/// The 0-based soft wrapped row index in the text.
pub row: usize,
/// The 0-based row index in local line (include first line).
///
/// This value only valid when return from [`TextWrapper::offset_to_display_point`], otherwise it will be ignored.
pub local_row: usize,
/// The 0-based column byte index in the display line (with soft wrap).
pub column: usize,
}
impl DisplayPoint {
pub fn new(row: usize, local_row: usize, column: usize) -> Self {
Self {
row,
local_row,
column,
}
}
}
/// The layout info of a line with soft wrapped lines.
pub(crate) struct LineLayout {
/// Total bytes length of this line.
len: usize,
@ -285,24 +375,35 @@ impl LineLayout {
let mut acc_len = 0;
let mut offset_y = px(0.);
for line in self.wrapped_lines.iter() {
let range = acc_len..=(acc_len + line.len());
for (i, line) in self.wrapped_lines.iter().enumerate() {
let is_last = i + 1 == self.wrapped_lines.len();
let line_len = if is_last { line.len + 1 } else { line.len };
let range = acc_len..(acc_len + line_len);
if range.contains(&offset) {
let x = line.x_for_index(offset.saturating_sub(acc_len));
return Some(point(x, offset_y));
}
acc_len += line.text.len();
acc_len += line_len;
offset_y += line_height;
}
None
}
/// Get the closest index for the given x in this line layout.
pub(super) fn closest_index_for_x(&self, x: Pixels) -> usize {
let mut acc_len = 0;
for line in self.wrapped_lines.iter() {
for (i, line) in self.wrapped_lines.iter().enumerate() {
let is_last = i + 1 == self.wrapped_lines.len();
if x <= line.width {
let ix = line.closest_index_for_x(x);
let mut ix = line.closest_index_for_x(x);
if !is_last && ix == line.text.len() {
// For soft wrap line, we can't put the cursor at the end of the line.
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
ix = ix.saturating_sub(c_len);
}
return acc_len + ix;
}
acc_len += line.text.len();
@ -322,10 +423,16 @@ impl LineLayout {
) -> Option<usize> {
let mut offset = 0;
let mut line_top = px(0.);
for line in self.wrapped_lines.iter() {
for (i, line) in self.wrapped_lines.iter().enumerate() {
let is_last = i + 1 == self.wrapped_lines.len();
let line_bottom = line_top + line_height;
if pos.y >= line_top && pos.y < line_bottom {
let ix = line.closest_index_for_x(pos.x);
let mut ix = line.closest_index_for_x(pos.x);
if !is_last && ix == line.text.len() {
// For soft wrap line, we can't put the cursor at the end of the line.
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
ix = ix.saturating_sub(c_len);
}
return Some(offset + ix);
}
@ -592,4 +699,105 @@ mod tests {
assert_eq!(line_layout.len(), 150);
assert_eq!(line_layout.wrapped_lines.len(), 2);
}
#[test]
fn test_offset_to_display_point() {
let font = gpui::Font {
family: "Arial".into(),
weight: FontWeight::default(),
style: FontStyle::Normal,
features: FontFeatures::default(),
fallbacks: None,
};
let mut wrapper = TextWrapper::new(font, px(14.), None);
wrapper.text = Rope::from(
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。",
);
wrapper.lines = vec![
// range: 0..15
LineItem {
line: Rope::from("Hello, 世界!\r"),
wrapped_lines: vec![0..15],
},
// range: 16..36
LineItem {
line: Rope::from("This is second line."),
wrapped_lines: vec![0..10, 10..20],
},
// range: 37..56
LineItem {
line: Rope::from("This is third line."),
wrapped_lines: vec![0..9, 9..15, 15..20],
},
// range: 57..79
LineItem {
line: Rope::from("这里是第 4 行。"),
wrapped_lines: vec![0..22],
},
];
assert_eq!(
wrapper.offset_to_display_point(12),
DisplayPoint::new(0, 0, 12)
);
assert_eq!(
wrapper.offset_to_display_point(15),
DisplayPoint::new(0, 0, 15)
);
assert_eq!(
wrapper.offset_to_display_point(16),
DisplayPoint::new(1, 0, 0)
);
assert_eq!(
wrapper.offset_to_display_point(21),
DisplayPoint::new(1, 0, 5)
);
assert_eq!(
wrapper.offset_to_display_point(27),
DisplayPoint::new(2, 1, 1)
);
assert_eq!(
wrapper.offset_to_display_point(37),
DisplayPoint::new(3, 0, 0)
);
assert_eq!(
wrapper.offset_to_display_point(54),
DisplayPoint::new(5, 2, 2)
);
assert_eq!(
wrapper.offset_to_display_point(59),
DisplayPoint::new(6, 0, 2)
);
assert_eq!(
wrapper.display_point_to_offset(DisplayPoint::new(6, 0, 2)),
59
);
assert_eq!(
wrapper.display_point_to_offset(DisplayPoint::new(5, 2, 2)),
54
);
assert_eq!(
wrapper.display_point_to_offset(DisplayPoint::new(3, 0, 0)),
37
);
assert_eq!(
wrapper.display_point_to_offset(DisplayPoint::new(2, 1, 1)),
27
);
assert_eq!(
wrapper.display_point_to_offset(DisplayPoint::new(1, 0, 5)),
21
);
assert_eq!(
wrapper.display_point_to_offset(DisplayPoint::new(1, 0, 0)),
16
);
assert_eq!(
wrapper.display_point_to_offset(DisplayPoint::new(0, 0, 15)),
15
);
}
}