Browse Source

Fix some edge cases for the cursor movement

pull/43/head
Emil Ernerfeldt 4 years ago
parent
commit
7494026139
  1. 6
      egui/src/paint/font.rs
  2. 209
      egui/src/paint/galley.rs
  3. 19
      egui/src/widgets/text_edit.rs

6
egui/src/paint/font.rs

@ -164,7 +164,7 @@ impl Font {
}
pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley {
let line_spacing = self.row_height();
let row_height = self.row_height();
let mut cursor_y = 0.0;
let mut rows = Vec::new();
@ -188,7 +188,7 @@ impl Font {
row.y_max += cursor_y;
}
cursor_y = paragraph_rows.last().unwrap().y_max;
cursor_y += line_spacing * 0.4; // Extra spacing between paragraphs. TODO: less hacky
cursor_y += row_height * 0.4; // Extra spacing between paragraphs. TODO: less hacky
rows.append(&mut paragraph_rows);
@ -199,7 +199,7 @@ impl Font {
rows.push(Row {
x_offsets: vec![0.0],
y_min: cursor_y,
y_max: cursor_y + line_spacing,
y_max: cursor_y + row_height,
ends_with_newline: false,
});
}

209
egui/src/paint/galley.rs

@ -27,6 +27,8 @@ pub struct CCursor {
/// If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
/// do we prefer the next row?
/// This is *almost* always what you want, *except* for when
/// explicitly clicking the end of a row or pressing the end key.
pub prefer_next_row: bool,
}
@ -98,6 +100,8 @@ pub struct PCursor {
/// If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
/// do we prefer the next row?
/// This is *almost* always what you want, *except* for when
/// explicitly clicking the end of a row or pressing the end key.
pub prefer_next_row: bool,
}
@ -236,15 +240,16 @@ impl Galley {
// Right paragraph, but is it the right row in the paragraph?
if it.offset <= pcursor.offset
&& pcursor.offset <= it.offset + row.char_count_excluding_newline()
&& (pcursor.offset <= it.offset + row.char_count_excluding_newline()
|| row.ends_with_newline)
{
let column = pcursor.offset - it.offset;
let column = column.at_most(row.char_count_excluding_newline());
let select_next_line_instead = pcursor.prefer_next_row
let select_next_row_instead = pcursor.prefer_next_row
&& !row.ends_with_newline
&& column == row.char_count_excluding_newline();
if !select_next_line_instead {
&& column >= row.char_count_excluding_newline();
if !select_next_row_instead {
return vec2(row.x_offsets[column], row.y_min);
}
}
@ -267,31 +272,32 @@ impl Galley {
}
/// Cursor at the given position within the galley
pub fn cursor_at(&self, pos: Vec2) -> Cursor {
pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor {
let mut best_y_dist = f32::INFINITY;
let mut cursor = Cursor::default();
let mut ccursor_index = 0;
let mut pcursor_it = PCursor::default();
for (line_nr, row) in self.rows.iter().enumerate() {
for (row_nr, row) in self.rows.iter().enumerate() {
let y_dist = (row.y_min - pos.y).abs().min((row.y_max - pos.y).abs());
if y_dist < best_y_dist {
best_y_dist = y_dist;
let column = row.char_at(pos.x);
let prefer_next_row = column < row.char_count_excluding_newline();
cursor = Cursor {
ccursor: CCursor {
index: ccursor_index + column,
prefer_next_row: column == 0,
prefer_next_row,
},
rcursor: RCursor {
row: line_nr,
row: row_nr,
column,
},
pcursor: PCursor {
paragraph: pcursor_it.paragraph,
offset: pcursor_it.offset + column,
prefer_next_row: column == 0,
prefer_next_row,
},
}
}
@ -324,13 +330,13 @@ impl Galley {
prefer_next_row: true,
};
for row in &self.rows {
let line_char_count = row.char_count_including_newline();
ccursor.index += line_char_count;
let row_char_count = row.char_count_including_newline();
ccursor.index += row_char_count;
if row.ends_with_newline {
pcursor.paragraph += 1;
pcursor.offset = 0;
} else {
pcursor.offset += line_char_count;
pcursor.offset += row_char_count;
}
}
Cursor {
@ -355,36 +361,36 @@ impl Galley {
/// ## Cursor conversions
impl Galley {
// TODO: return identical cursor, or clamp?
// The returned cursor is clamped.
pub fn from_ccursor(&self, ccursor: CCursor) -> Cursor {
let prefer_next_line = ccursor.prefer_next_row;
let prefer_next_row = ccursor.prefer_next_row;
let mut ccursor_it = CCursor {
index: 0,
prefer_next_row: prefer_next_line,
prefer_next_row,
};
let mut pcursor_it = PCursor {
paragraph: 0,
offset: 0,
prefer_next_row: prefer_next_line,
prefer_next_row,
};
for (line_nr, row) in self.rows.iter().enumerate() {
let line_char_count = row.char_count_excluding_newline();
for (row_nr, row) in self.rows.iter().enumerate() {
let row_char_count = row.char_count_excluding_newline();
if ccursor_it.index <= ccursor.index
&& ccursor.index <= ccursor_it.index + line_char_count
&& ccursor.index <= ccursor_it.index + row_char_count
{
let column = ccursor.index - ccursor_it.index;
let select_next_line_instead = prefer_next_line
let select_next_row_instead = prefer_next_row
&& !row.ends_with_newline
&& column == row.char_count_excluding_newline();
if !select_next_line_instead {
&& column >= row.char_count_excluding_newline();
if !select_next_row_instead {
pcursor_it.offset += column;
return Cursor {
ccursor,
rcursor: RCursor {
row: line_nr,
row: row_nr,
column,
},
pcursor: pcursor_it,
@ -407,40 +413,38 @@ impl Galley {
}
}
// TODO: return identical cursor, or clamp?
pub fn from_rcursor(&self, rcursor: RCursor) -> Cursor {
if rcursor.row >= self.rows.len() {
return self.end();
}
let prefer_next_line = rcursor.column == 0;
let prefer_next_row =
rcursor.column < self.rows[rcursor.row].char_count_excluding_newline();
let mut ccursor_it = CCursor {
index: 0,
prefer_next_row: prefer_next_line,
prefer_next_row,
};
let mut pcursor_it = PCursor {
paragraph: 0,
offset: 0,
prefer_next_row: prefer_next_line,
prefer_next_row,
};
for (line_nr, row) in self.rows.iter().enumerate() {
if line_nr == rcursor.row {
let column = rcursor.column.at_most(row.char_count_excluding_newline());
let select_next_line_instead = prefer_next_line
&& !row.ends_with_newline
&& column == row.char_count_excluding_newline();
for (row_nr, row) in self.rows.iter().enumerate() {
if row_nr == rcursor.row {
ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline());
if !select_next_line_instead {
ccursor_it.index += column;
pcursor_it.offset += column;
return Cursor {
ccursor: ccursor_it,
rcursor,
pcursor: pcursor_it,
};
if row.ends_with_newline {
// Allow offset to go beyond the end of the paragraph
pcursor_it.offset += rcursor.column;
} else {
pcursor_it.offset += rcursor.column.at_most(row.char_count_excluding_newline());
}
return Cursor {
ccursor: ccursor_it,
rcursor,
pcursor: pcursor_it,
};
}
ccursor_it.index += row.char_count_including_newline();
if row.ends_with_newline {
@ -459,36 +463,38 @@ impl Galley {
// TODO: return identical cursor, or clamp?
pub fn from_pcursor(&self, pcursor: PCursor) -> Cursor {
let prefer_next_line = pcursor.prefer_next_row;
let prefer_next_row = pcursor.prefer_next_row;
let mut ccursor_it = CCursor {
index: 0,
prefer_next_row: prefer_next_line,
prefer_next_row,
};
let mut pcursor_it = PCursor {
paragraph: 0,
offset: 0,
prefer_next_row: prefer_next_line,
prefer_next_row,
};
for (line_nr, row) in self.rows.iter().enumerate() {
for (row_nr, row) in self.rows.iter().enumerate() {
if pcursor_it.paragraph == pcursor.paragraph {
// Right paragraph, but is it the right row in the paragraph?
if pcursor_it.offset <= pcursor.offset
&& pcursor.offset <= pcursor_it.offset + row.char_count_excluding_newline()
&& (pcursor.offset <= pcursor_it.offset + row.char_count_excluding_newline()
|| row.ends_with_newline)
{
let column = pcursor.offset - pcursor_it.offset;
let column = column.at_most(row.char_count_excluding_newline());
let select_next_line_instead = pcursor.prefer_next_row
let select_next_row_instead = pcursor.prefer_next_row
&& !row.ends_with_newline
&& column == row.char_count_excluding_newline();
if !select_next_line_instead {
ccursor_it.index += column;
&& column >= row.char_count_excluding_newline();
if !select_next_row_instead {
ccursor_it.index += column.at_most(row.char_count_excluding_newline());
return Cursor {
ccursor: ccursor_it,
rcursor: RCursor {
row: line_nr,
row: row_nr,
column,
},
pcursor,
@ -519,44 +525,97 @@ impl Galley {
if cursor.ccursor.index == 0 {
Default::default()
} else {
self.from_ccursor(cursor.ccursor - 1)
let ccursor = CCursor {
index: cursor.ccursor.index,
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
};
self.from_ccursor(ccursor - 1)
}
}
pub fn cursor_right_one_character(&self, cursor: &Cursor) -> Cursor {
self.from_ccursor(cursor.ccursor + 1)
let ccursor = CCursor {
index: cursor.ccursor.index,
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
};
self.from_ccursor(ccursor + 1)
}
pub fn cursor_up_one_line(&self, cursor: &Cursor) -> Cursor {
pub fn cursor_up_one_row(&self, cursor: &Cursor) -> Cursor {
if cursor.rcursor.row == 0 {
Cursor::default()
} else {
let x = self.pos_from_cursor(cursor).x;
let row = cursor.rcursor.row - 1;
let column = self.rows[row].char_at(x).max(cursor.rcursor.column);
self.from_rcursor(RCursor { row, column })
let new_row = cursor.rcursor.row - 1;
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
let new_rcursor = if cursor_is_beyond_end_of_current_row {
// keep same column
RCursor {
row: new_row,
column: cursor.rcursor.column,
}
} else {
// keep same X coord
let x = self.pos_from_cursor(cursor).x;
let column = if x > self.rows[new_row].max_x() {
// beyond the end of this row - keep same colum
cursor.rcursor.column
} else {
self.rows[new_row].char_at(x)
};
RCursor {
row: new_row,
column,
}
};
self.from_rcursor(new_rcursor)
}
}
pub fn cursor_down_one_line(&self, cursor: &Cursor) -> Cursor {
pub fn cursor_down_one_row(&self, cursor: &Cursor) -> Cursor {
if cursor.rcursor.row + 1 < self.rows.len() {
let x = self.pos_from_cursor(cursor).x;
let row = cursor.rcursor.row + 1;
let column = self.rows[row].char_at(x).max(cursor.rcursor.column);
self.from_rcursor(RCursor { row, column })
let new_row = cursor.rcursor.row + 1;
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
let new_rcursor = if cursor_is_beyond_end_of_current_row {
// keep same column
RCursor {
row: new_row,
column: cursor.rcursor.column,
}
} else {
// keep same X coord
let x = self.pos_from_cursor(cursor).x;
let column = if x > self.rows[new_row].max_x() {
// beyond the end of the next row - keep same column
cursor.rcursor.column
} else {
self.rows[new_row].char_at(x)
};
RCursor {
row: new_row,
column,
}
};
self.from_rcursor(new_rcursor)
} else {
self.end()
}
}
pub fn cursor_begin_of_line(&self, cursor: &Cursor) -> Cursor {
pub fn cursor_begin_of_row(&self, cursor: &Cursor) -> Cursor {
self.from_rcursor(RCursor {
row: cursor.rcursor.row,
column: 0,
})
}
pub fn cursor_end_of_line(&self, cursor: &Cursor) -> Cursor {
pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor {
self.from_rcursor(RCursor {
row: cursor.rcursor.row,
column: self.rows[cursor.rcursor.row].char_count_excluding_newline(),
@ -735,11 +794,11 @@ fn test_text_layout() {
let cursor = Cursor::default();
assert_eq!(galley.cursor_up_one_line(&cursor), cursor);
assert_eq!(galley.cursor_begin_of_line(&cursor), cursor);
assert_eq!(galley.cursor_up_one_row(&cursor), cursor);
assert_eq!(galley.cursor_begin_of_row(&cursor), cursor);
assert_eq!(
galley.cursor_end_of_line(&cursor),
galley.cursor_end_of_row(&cursor),
Cursor {
ccursor: CCursor::new(5),
rcursor: RCursor { row: 0, column: 5 },
@ -752,7 +811,7 @@ fn test_text_layout() {
);
assert_eq!(
galley.cursor_down_one_line(&cursor),
galley.cursor_down_one_row(&cursor),
Cursor {
ccursor: CCursor::new(5),
rcursor: RCursor { row: 1, column: 0 },
@ -766,7 +825,7 @@ fn test_text_layout() {
let cursor = Cursor::default();
assert_eq!(
galley.cursor_down_one_line(&galley.cursor_down_one_line(&cursor)),
galley.cursor_down_one_row(&galley.cursor_down_one_row(&cursor)),
Cursor {
ccursor: CCursor::new(11),
rcursor: RCursor { row: 2, column: 0 },
@ -779,13 +838,13 @@ fn test_text_layout() {
);
let cursor = galley.end();
assert_eq!(galley.cursor_down_one_line(&cursor), cursor);
assert_eq!(galley.cursor_down_one_row(&cursor), cursor);
let cursor = galley.end();
assert!(galley.cursor_up_one_line(&galley.end()) != cursor);
assert!(galley.cursor_up_one_row(&galley.end()) != cursor);
assert_eq!(
galley.cursor_up_one_line(&galley.end()),
galley.cursor_up_one_row(&galley.end()),
Cursor {
ccursor: CCursor::new(15),
rcursor: RCursor { row: 2, column: 10 },

19
egui/src/widgets/text_edit.rs

@ -181,7 +181,11 @@ impl<'t> Widget for TextEdit<'t> {
if response.clicked && enabled {
ui.memory().request_kb_focus(id);
if let Some(mouse_pos) = ui.input().mouse.pos {
state.pcursor = Some(galley.cursor_at(mouse_pos - response.rect.min).pcursor);
state.pcursor = Some(
galley
.cursor_from_pos(mouse_pos - response.rect.min)
.pcursor,
);
}
} else if ui.input().mouse.click || (ui.input().mouse.pressed && !response.hovered) {
// User clicked somewhere else
@ -331,8 +335,6 @@ fn on_key_press(
galley: &Galley,
key: Key,
) -> Option<CCursor> {
// eprintln!("on_key_press before: '{}', cursor at {}", text, cursor);
match key {
Key::Backspace if cursor.ccursor.index > 0 => {
*cursor = galley.from_ccursor(cursor.ccursor - 1);
@ -358,12 +360,11 @@ fn on_key_press(
Key::Enter => unreachable!("Should have been handled earlier"),
Key::Home => {
// To start of line:
*cursor = galley.cursor_begin_of_line(cursor);
*cursor = galley.cursor_begin_of_row(cursor);
None
}
Key::End => {
*cursor = galley.cursor_end_of_line(cursor);
*cursor = galley.cursor_end_of_row(cursor);
None
}
Key::Left => {
@ -375,15 +376,13 @@ fn on_key_press(
None
}
Key::Up => {
*cursor = galley.cursor_up_one_line(cursor);
*cursor = galley.cursor_up_one_row(cursor);
None
}
Key::Down => {
*cursor = galley.cursor_down_one_line(cursor);
*cursor = galley.cursor_down_one_row(cursor);
None
}
_ => None,
}
// eprintln!("on_key_press after: '{}', cursor at {}\n", text, cursor);
}

Loading…
Cancel
Save