Before, all edits did not adjust the cursor positions, but they remained with their selection in the same location. 

Original commit: 57cdbbf588
This commit is contained in:
Adam Obuchowicz 2020-02-10 18:20:47 +01:00 committed by GitHub
parent 81d0bb555b
commit d6d59d224d
6 changed files with 259 additions and 53 deletions

View File

@ -2,6 +2,7 @@
pub mod content;
pub mod cursor;
pub mod location;
pub mod render;
use crate::prelude::*;
@ -9,9 +10,12 @@ use crate::prelude::*;
use crate::display::object::DisplayObjectData;
use crate::display::shape::text::text_field::content::TextFieldContent;
use crate::display::shape::text::text_field::content::TextChange;
use crate::display::shape::text::text_field::cursor::{Cursors, Cursor};
use crate::display::shape::text::text_field::cursor::Cursors;
use crate::display::shape::text::text_field::cursor::Cursor;
use crate::display::shape::text::text_field::cursor::Step;
use crate::display::shape::text::text_field::cursor::CursorNavigation;
use crate::display::shape::text::text_field::location::TextLocation;
use crate::display::shape::text::text_field::location::TextLocationChange;
use crate::display::shape::text::glyph::font::FontId;
use crate::display::shape::text::glyph::font::FontRegistry;
use crate::display::shape::text::text_field::render::TextFieldSprites;
@ -112,6 +116,12 @@ shared! { TextField
self.rendered.display_object.position().xy()
}
/// Add cursor.
pub fn add_cursor(&mut self, position:TextLocation, fonts:&mut FontRegistry) {
self.cursors.add_cursor(position);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
}
/// Move all cursors by given step.
pub fn navigate_cursors(&mut self, step:Step, selecting:bool, fonts:&mut FontRegistry) {
let content = self.content.full_info(fonts);
@ -153,19 +163,18 @@ shared! { TextField
pub fn edit(&mut self, insertion:&str, fonts:&mut FontRegistry) {
let trimmed = insertion.trim_end_matches('\n');
let is_line_per_cursor_edit = trimmed.contains('\n') && self.cursors.cursors.len() > 1;
let selection = self.cursors.cursors.iter().map(Cursor::selection_range);
let cursor_ids = self.cursors.sorted_cursor_indices();
if is_line_per_cursor_edit {
let range_with_line = selection.zip(trimmed.split('\n'));
let changes = range_with_line.map(|(r,l)| TextChange::replace(r,l));
self.content.apply_changes(changes);
let cursor_with_line = cursor_ids.iter().cloned().zip(trimmed.split('\n'));
self.edit_per_cursor(cursor_with_line);
} else {
let changes = selection.map(|range| TextChange::replace(range,insertion));
self.content.apply_changes(changes);
let cursor_with_line = cursor_ids.iter().map(|cursor_id| (*cursor_id,insertion));
self.edit_per_cursor(cursor_with_line);
};
// TODO[ao]: fix cursor positions after applying changes.
self.assignment_update(fonts).update_after_text_edit();
self.rendered.update_glyphs(&mut self.content,fonts);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
}
/// Update underlying Display Object.
@ -194,6 +203,19 @@ impl TextFieldData {
view_size : self.properties.size,
}
}
fn edit_per_cursor<'a,It>(&mut self, cursor_id_with_text_to_insert:It)
where It : Iterator<Item=(usize,&'a str)> {
let mut location_change = TextLocationChange::default();
for (cursor_id,to_insert) in cursor_id_with_text_to_insert {
let cursor = &mut self.cursors.cursors[cursor_id];
let replaced = location_change.apply_to_range(cursor.selection_range());
let change = TextChange::replace(replaced,to_insert);
location_change.add_change(&change);
*cursor = Cursor::new(change.inserted_text_range().end);
self.content.apply_change(change);
}
}
}

View File

@ -8,6 +8,7 @@ use crate::display::shape::text::glyph::font::FontRegistry;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::text_field::content::line::Line;
use crate::display::shape::text::text_field::content::line::LineFullInfo;
use crate::display::shape::text::text_field::location::TextLocation;
use crate::display::shape::text::text_field::TextFieldProperties;
use nalgebra::Vector2;
@ -93,8 +94,12 @@ pub enum ChangeType {
/// A structure describing a text operation in one place.
#[derive(Debug)]
pub struct TextChange {
replaced : Range<TextLocation>,
lines : Vec<Vec<char>>,
/// Text fragment to be replaced. If we don't mean to remove any text, this should be an empty
/// range with start set at position there `lines` will be inserted (see `TextChange::insert`
/// definition).
pub replaced : Range<TextLocation>,
/// Lines to insert instead of replaced fragment.
pub lines : Vec<Vec<char>>,
}
impl TextChange {
@ -135,6 +140,19 @@ impl TextChange {
}
}
/// Returns text location range where the inserted text will appear after making this change.
pub fn inserted_text_range(&self) -> Range<TextLocation> {
let start = self.replaced.start;
let end_line = start.line + self.lines.len().saturating_sub(1);
let last_line_len = self.lines.last().map_or(0, |l| l.len());
let end_column = if start.line == end_line {
start.column + last_line_len
} else {
last_line_len
};
start..TextLocation{line:end_line, column:end_column}
}
fn mk_lines_as_char_vector(text:&str) -> Vec<Vec<char>> {
TextFieldContent::split_to_lines(text).map(|s| s.chars().collect_vec()).collect()
}
@ -142,39 +160,6 @@ impl TextChange {
// ====================
// === TextLocation ===
// ====================
/// A position of character in a multiline text.
#[derive(Copy,Clone,Debug,PartialEq,Eq,PartialOrd,Ord)]
pub struct TextLocation {
/// Line index.
pub line: usize,
/// Column is a index of char in given line.
pub column: usize,
}
impl TextLocation {
/// Create location at begin of given line.
pub fn at_line_begin(line_index:usize) -> TextLocation {
TextLocation {
line : line_index,
column : 0,
}
}
/// Create location at begin of the whole document.
pub fn at_document_begin() -> TextLocation {
TextLocation {
line : 0,
column : 0,
}
}
}
// ============================
// === TextFieldContent ===
// ============================
@ -523,6 +508,21 @@ mod test {
assert_eq!(text , content.copy_fragment(whole_content));
}
#[test]
fn get_inserted_text_location_of_change() {
let one_line = "One line";
let two_lines = "Two\nlines";
let replaced_range = TextLocation{line:1,column:2}..TextLocation{line:2,column:2};
let one_line_change = TextChange::replace(replaced_range.clone(),one_line);
let two_lines_change = TextChange::replace(replaced_range.clone(),two_lines);
let one_line_expected = replaced_range.start..TextLocation{line:1, column:10};
let two_lines_expected = replaced_range.start..TextLocation{line:2, column:5 };
assert_eq!(one_line_expected , one_line_change .inserted_text_range());
assert_eq!(two_lines_expected, two_lines_change.inserted_text_range());
}
fn get_lines_as_strings(content:&TextFieldContent) -> Vec<String> {
content.lines.iter().map(|l| l.chars().iter().collect()).collect()
}

View File

@ -2,9 +2,9 @@
use crate::prelude::*;
use crate::display::shape::text::text_field::content::TextLocation;
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
use crate::display::shape::text::text_field::content::line::LineFullInfo;
use crate::display::shape::text::text_field::location::TextLocation;
use nalgebra::Vector2;
use std::cmp::Ordering;
@ -298,6 +298,11 @@ impl Cursors {
self.merge_overlapping_cursors();
}
/// Returns cursor indices sorted by cursors' position in text.
pub fn sorted_cursor_indices(&self) -> Vec<usize> {
self.cursors.iter().enumerate().sorted_by_key(|(_,c)| c.position).map(|(i,_)| i).collect()
}
/// Merge overlapping cursors
///
/// This function checks all cursors, and merge each pair where cursors are at the same position
@ -307,20 +312,24 @@ impl Cursors {
/// removed cursors.
fn merge_overlapping_cursors(&mut self) {
if !self.cursors.is_empty() {
self.cursors.sort_by_key(|c| c.position);
let mut i = 1;
while i < self.cursors.len() {
let merged = self.merged_selection_range(i - 1,i);
let sorted = self.sorted_cursor_indices();
let mut to_remove = Vec::new();
let mut last_cursor_id = sorted[0];
for id in sorted.iter().skip(1) {
let merged = self.merged_selection_range(last_cursor_id,*id);
match merged {
Some(merged_range) => {
self.cursors[i-1].extend_selection(&merged_range);
self.cursors.remove(i);
self.cursors[last_cursor_id].extend_selection(&merged_range);
to_remove.push(*id);
},
None => {
i += 1
last_cursor_id = *id;
}
};
}
for id in to_remove.iter().sorted().rev() {
self.cursors.remove(*id);
}
}
}
@ -471,7 +480,7 @@ mod test {
fn merging_selection_after_moving_case<F>(convert:F)
where F : FnMut(&Range<(usize,usize)>) -> Cursor + Clone {
let ranges = vec![(1,4)..(1,5), (0,0)..(0,5), (0,2)..(1,0), (1,5)..(2,0)];
let expected_ranges = vec![(0,0)..(1,0), (1,4)..(1,5), (1,5)..(2,0)];
let expected_ranges = vec![(1,4)..(1,5), (0,0)..(1,0), (1,5)..(2,0)];
let initial_cursors = ranges.iter().map(convert.clone()).collect_vec();
let expected_cursors = expected_ranges.iter().map(convert).collect_vec();
let mut cursors = Cursors::mock(initial_cursors);

View File

@ -0,0 +1,173 @@
//! Module with content regarding location in text (e.g. location of cursors or changes etc.)
use crate::display::shape::text::text_field::content::TextChange;
use std::ops::Range;
// ====================
// === TextLocation ===
// ====================
/// A position of character in a multiline text.
#[derive(Copy,Clone,Debug,PartialEq,Eq,PartialOrd,Ord)]
pub struct TextLocation {
/// Line index.
pub line: usize,
/// Column is a index of char in given line.
pub column: usize,
}
impl TextLocation {
/// Create location at begin of given line.
pub fn at_line_begin(line_index:usize) -> TextLocation {
TextLocation {
line : line_index,
column : 0,
}
}
/// Create location at begin of the whole document.
pub fn at_document_begin() -> TextLocation {
TextLocation {
line : 0,
column : 0,
}
}
}
// ==========================
// === TextLocationChange ===
// ==========================
/// A locations change after some edit.
///
/// After on or many changes to text content, the following locations are altered (e.g. when you
/// remove one line, all the next location are shifted one line up). This structure keeps history
/// of sequence of text changes and can tell how the further TextLocations should change to mark
/// the same place in the text.
#[derive(Copy,Clone,Debug,Default)]
pub struct TextLocationChange {
/// A current line offset for all next locations;
pub line_offset : isize,
/// Last line altered by considered text changes (in the current numeration).
pub last_changed_line : usize,
/// The column offset for all next locations in `last_changed_line`.
pub column_offset : isize,
}
impl TextLocationChange {
/// Return the new location after considering text changes so far.
pub fn apply_to(&self, location:TextLocation) -> TextLocation {
let line = (location.line as isize + self.line_offset) as usize;
let column = if line == self.last_changed_line {
(location.column as isize + self.column_offset) as usize
} else {
location.column
};
TextLocation{line,column}
}
/// Return the new location range after considering text changes so far.
pub fn apply_to_range(&self, range:Range<TextLocation>) -> Range<TextLocation> {
self.apply_to(range.start)..self.apply_to(range.end)
}
/// Add a new text change into consideration.
pub fn add_change(&mut self, change:&TextChange) {
let removed = &change.replaced;
let inserted = change.inserted_text_range();
let lines_removed = (removed.end.line - removed.start.line) as isize;
let lines_added = (inserted.end.line - inserted.start.line) as isize;
let change_in_last_line = removed.start.line == self.last_changed_line;
let drop_column_offset = !change_in_last_line || lines_removed > 0;
if drop_column_offset {
self.column_offset = 0;
}
self.line_offset += lines_added - lines_removed;
self.column_offset += inserted.end.column as isize - removed.end.column as isize;
self.last_changed_line = inserted.end.line;
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn applying_change_to_location() {
// positive offsets
let change = TextLocationChange {
line_offset: 1,
last_changed_line: 5,
column_offset: 3
};
let in_last_changed_line = TextLocation{line:4, column:2};
let not_in_last_changed_line = TextLocation{line:5, column:2};
assert_eq!(TextLocation{line:5, column:5}, change.apply_to(in_last_changed_line));
assert_eq!(TextLocation{line:6, column:2}, change.apply_to(not_in_last_changed_line));
// negative offsets
let change = TextLocationChange {
line_offset: -1,
last_changed_line: 5,
column_offset: -2
};
let in_last_changed_line = TextLocation{line:6, column:2};
let not_in_last_changed_line = TextLocation{line:5, column:2};
assert_eq!(TextLocation{line:5, column:0}, change.apply_to(in_last_changed_line));
assert_eq!(TextLocation{line:4, column:2}, change.apply_to(not_in_last_changed_line));
}
#[test]
fn adding_changes() {
let one_line = "One line";
let two_lines = "Two\nLines";
let mut location_change = TextLocationChange::default();
// initial change
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 2};
location_change.add_change(&TextChange::replace(replaced,one_line));
assert_eq!(0, location_change.line_offset);
assert_eq!(2, location_change.last_changed_line);
assert_eq!(8, location_change.column_offset);
// single line in the same line as previous
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 13};
location_change.add_change(&TextChange::replace(replaced,one_line));
assert_eq!(0, location_change.line_offset);
assert_eq!(2, location_change.last_changed_line);
assert_eq!(5, location_change.column_offset);
// single -> multiple lines in the same line as previous
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 8};
location_change.add_change(&TextChange::replace(replaced,two_lines));
assert_eq!(1, location_change.line_offset);
assert_eq!(3, location_change.last_changed_line);
assert_eq!(2, location_change.column_offset);
// multiple -> multiple lines
let replaced = TextLocation{line:3, column:2}..TextLocation{line:7, column: 3};
location_change.add_change(&TextChange::replace(replaced,two_lines));
assert_eq!(-2, location_change.line_offset);
assert_eq!(4 , location_change.last_changed_line);
assert_eq!(2 , location_change.column_offset);
// multiple -> single lines in the same line as previous
let replaced = TextLocation{line:4, column:2}..TextLocation{line:5, column: 11};
location_change.add_change(&TextChange::replace(replaced,one_line));
assert_eq!(-3, location_change.line_offset);
assert_eq!(4 , location_change.last_changed_line);
assert_eq!(-1, location_change.column_offset);
// single line in other line than previous
let replaced = TextLocation{line:5, column:2}..TextLocation{line:5, column: 12};
location_change.add_change(&TextChange::replace(replaced,one_line));
assert_eq!(-3, location_change.line_offset);
assert_eq!(5 , location_change.last_changed_line);
assert_eq!(-2, location_change.column_offset);
}
}

View File

@ -2,8 +2,8 @@
use crate::display::shape::primitive::system::ShapeSystem;
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
use crate::display::shape::text::text_field::content::TextLocation;
use crate::display::shape::text::text_field::cursor::Cursor;
use crate::display::shape::text::text_field::location::TextLocation;
use crate::display::symbol::geometry::compound::sprite::Sprite;
use nalgebra::Vector2;

View File

@ -21,6 +21,7 @@ use web_sys::MouseEvent;
use failure::_core::cell::RefCell;
use wasm_bindgen::prelude::*;
use crate::system::web::text_input::KeyboardBinding;
use crate::display::shape::text::text_field::location::TextLocation;
const TEXT:&str =
@ -58,6 +59,7 @@ pub fn run_example_text_selecting() {
let text_field = TextField::new(&world,TEXT,properties,&mut fonts);
text_field.set_position(Vector3::new(10.0, 600.0, 0.0));
text_field.jump_cursor(Vector2::new(50.0, -40.0),false,&mut fonts);
text_field.add_cursor(TextLocation{line:1, column:0}, &mut fonts);
world.add_child(&text_field);
text_field.update();