mirror of
https://github.com/enso-org/enso.git
synced 2024-12-28 14:42:25 +03:00
Cursor navigation (https://github.com/enso-org/ide/pull/106)
Original commit: 9d1b18d681
This commit is contained in:
parent
65a687b911
commit
1b6094a126
@ -157,7 +157,7 @@ pub fn set_buffer_data(gl_context:&Context, buffer:&WebGlBuffer, data:&[f32]) {
|
||||
/// wasm-bindgen examples
|
||||
/// (https://rustwasm.github.io/wasm-bindgen/examples/webgl.html)
|
||||
fn set_bound_buffer_data(gl_context:&Context, target:u32, data:&[f32]) {
|
||||
let usage = Context::STATIC_DRAW;
|
||||
let usage = Context::STATIC_DRAW;
|
||||
unsafe { // Note [unsafe buffer_data]
|
||||
let float_array = Float32Array::view(&data);
|
||||
gl_context.buffer_data_with_array_buffer_view(target,&float_array,usage);
|
||||
|
@ -20,6 +20,8 @@ use crate::display::render::webgl::Context;
|
||||
use crate::display::shape::text::buffer::TextComponentBuffers;
|
||||
use crate::display::shape::text::content::TextComponentContent;
|
||||
use crate::display::shape::text::cursor::Cursors;
|
||||
use crate::display::shape::text::cursor::Step;
|
||||
use crate::display::shape::text::cursor::CursorNavigation;
|
||||
use crate::display::shape::text::font::FontId;
|
||||
use crate::display::shape::text::font::Fonts;
|
||||
use crate::display::shape::text::msdf::MsdfTexture;
|
||||
@ -134,6 +136,12 @@ impl TextComponent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn navigate_cursors(&mut self, step:Step, selecting:bool, fonts:&mut Fonts) {
|
||||
let content = &mut self.content;
|
||||
let mut navigation = CursorNavigation {content,fonts,selecting};
|
||||
self.cursors.navigate_all_cursors(&mut navigation,&step);
|
||||
}
|
||||
|
||||
fn refresh_content_buffers(&mut self, fonts:&mut Fonts) {
|
||||
let refresh_info = self.content.refresh_info(fonts);
|
||||
self.buffers.refresh(&self.gl_context,refresh_info);
|
||||
@ -165,8 +173,7 @@ impl TextComponent {
|
||||
}
|
||||
|
||||
fn refresh_cursors(&mut self, fonts:&mut Fonts) {
|
||||
let cursors_changed = !self.cursors.dirty_cursors.is_empty();
|
||||
if cursors_changed {
|
||||
if self.cursors.dirty {
|
||||
let gl_context = &self.gl_context;
|
||||
let content = &mut self.content;
|
||||
self.cursors.update_buffer_data(gl_context,content,fonts);
|
||||
@ -209,6 +216,7 @@ impl TextComponent {
|
||||
}
|
||||
|
||||
fn display_cursors(&self) {
|
||||
let buffer = self.cursors.buffer.as_ref().unwrap();
|
||||
let gl_context = &self.gl_context;
|
||||
let to_scene_matrix = self.to_scene_matrix();
|
||||
let program = &self.cursors_program;
|
||||
@ -216,7 +224,7 @@ impl TextComponent {
|
||||
|
||||
gl_context.use_program(Some(&program.gl_program));
|
||||
self.cursors_program.set_to_scene_transformation(&to_scene_matrix);
|
||||
self.cursors_program.bind_buffer_to_attribute("position",&self.cursors.buffer);
|
||||
self.cursors_program.bind_buffer_to_attribute("position",buffer);
|
||||
gl_context.line_width(2.0);
|
||||
gl_context.draw_arrays(WebGl2RenderingContext::LINES,0,vertices_count);
|
||||
gl_context.line_width(1.0);
|
||||
|
@ -82,21 +82,21 @@ pub enum ChangeType {
|
||||
|
||||
/// A structure describing a text operation in one place.
|
||||
pub struct TextChange {
|
||||
replaced : Range<CharPosition>,
|
||||
replaced : Range<TextLocation>,
|
||||
lines : Vec<Vec<char>>,
|
||||
}
|
||||
|
||||
impl TextChange {
|
||||
/// Creates operation which inserts text at given position.
|
||||
pub fn insert(position:CharPosition, text:&str) -> Self {
|
||||
pub fn insert(at:TextLocation, text:&str) -> Self {
|
||||
TextChange {
|
||||
replaced : position..position,
|
||||
lines : TextComponentContent::split_to_lines(text).map(|s| s.chars().collect_vec()).collect()
|
||||
replaced : at..at,
|
||||
lines : Self::mk_lines_as_char_vector(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates operation which deletes text at given range.
|
||||
pub fn delete(range:Range<CharPosition>) -> Self {
|
||||
pub fn delete(range:Range<TextLocation>) -> Self {
|
||||
TextChange {
|
||||
replaced : range,
|
||||
lines : vec![vec![]],
|
||||
@ -104,9 +104,9 @@ impl TextChange {
|
||||
}
|
||||
|
||||
/// Creates operation which replaces text at given range with given string.
|
||||
pub fn replace(replaced:Range<CharPosition>, text:&str) -> Self {
|
||||
pub fn replace(replaced:Range<TextLocation>, text:&str) -> Self {
|
||||
TextChange {replaced,
|
||||
lines : TextComponentContent::split_to_lines(text).map(|s| s.chars().collect_vec()).collect()
|
||||
lines : Self::mk_lines_as_char_vector(text)
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,8 +123,45 @@ impl TextChange {
|
||||
ChangeType::Multiline
|
||||
}
|
||||
}
|
||||
|
||||
fn mk_lines_as_char_vector(text:&str) -> Vec<Vec<char>> {
|
||||
TextComponentContent::split_to_lines(text).map(|s| s.chars().collect_vec()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === TextLocation ===
|
||||
// ====================
|
||||
|
||||
/// A position of character in a multiline text.
|
||||
#[derive(Copy,Clone,Debug,PartialEq,Eq,PartialOrd,Ord)]
|
||||
pub struct TextLocation {
|
||||
pub line : usize,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================
|
||||
// === TextComponentContent ===
|
||||
// ============================
|
||||
@ -137,13 +174,6 @@ pub struct TextComponentContent {
|
||||
pub font : FontId,
|
||||
}
|
||||
|
||||
/// A position of character in multiline text.
|
||||
#[derive(Copy,Clone,Debug,PartialEq,Eq,PartialOrd,Ord)]
|
||||
pub struct CharPosition {
|
||||
pub line : usize,
|
||||
pub column : usize,
|
||||
}
|
||||
|
||||
/// References to all needed stuff for generating buffer's data.
|
||||
pub struct RefreshInfo<'a, 'b> {
|
||||
pub lines : &'a mut [Line],
|
||||
@ -177,7 +207,7 @@ impl TextComponentContent {
|
||||
}
|
||||
|
||||
/// LineRef structure for line at given index.
|
||||
pub fn line(& mut self, index:usize) -> LineRef {
|
||||
pub fn line(&mut self, index:usize) -> LineRef {
|
||||
LineRef {
|
||||
line : &mut self.lines[index],
|
||||
line_id : index,
|
||||
@ -310,10 +340,10 @@ mod test {
|
||||
#[test]
|
||||
fn edit_single_line() {
|
||||
let text = "Line a\nLine b\nLine c";
|
||||
let delete_from = CharPosition{line:1, column:0};
|
||||
let delete_to = CharPosition{line:1, column:4};
|
||||
let delete_from = TextLocation {line:1, column:0};
|
||||
let delete_to = TextLocation {line:1, column:4};
|
||||
let deleted_range = delete_from..delete_to;
|
||||
let insert = TextChange::insert(CharPosition{line:1, column:1}, "ab");
|
||||
let insert = TextChange::insert(TextLocation {line:1, column:1}, "ab");
|
||||
let delete = TextChange::delete(deleted_range.clone());
|
||||
let replace = TextChange::replace(deleted_range, "text");
|
||||
|
||||
@ -340,12 +370,12 @@ mod test {
|
||||
fn insert_multiple_lines() {
|
||||
let text = "Line a\nLine b\nLine c";
|
||||
let inserted = "Ins a\nIns b";
|
||||
let begin_position = CharPosition{line:0, column:0};
|
||||
let middle_position = CharPosition{line:1, column:2};
|
||||
let end_position = CharPosition{line:2, column:6};
|
||||
let insert_at_begin = TextChange::insert(begin_position , inserted);
|
||||
let insert_in_middle = TextChange::insert(middle_position, inserted);
|
||||
let insert_at_end = TextChange::insert(end_position , inserted);
|
||||
let begin_loc = TextLocation {line:0, column:0};
|
||||
let middle_loc = TextLocation {line:1, column:2};
|
||||
let end_loc = TextLocation {line:2, column:6};
|
||||
let insert_at_begin = TextChange::insert(begin_loc ,inserted);
|
||||
let insert_in_middle = TextChange::insert(middle_loc,inserted);
|
||||
let insert_at_end = TextChange::insert(end_loc ,inserted);
|
||||
|
||||
let mut content = TextComponentContent::new(0,text);
|
||||
|
||||
@ -376,8 +406,8 @@ mod test {
|
||||
#[test]
|
||||
fn delete_multiple_lines() {
|
||||
let text = "Line a\nLine b\nLine c";
|
||||
let delete_from = CharPosition{line:0, column:2};
|
||||
let delete_to = CharPosition{line:2, column:3};
|
||||
let delete_from = TextLocation {line:0, column:2};
|
||||
let delete_to = TextLocation {line:2, column:3};
|
||||
let deleted_range = delete_from..delete_to;
|
||||
let delete = TextChange::delete(deleted_range);
|
||||
|
||||
|
@ -4,7 +4,7 @@ use crate::prelude::*;
|
||||
|
||||
use crate::display::render::webgl::Context;
|
||||
use crate::display::render::webgl::set_buffer_data;
|
||||
use crate::display::shape::text::content::CharPosition;
|
||||
use crate::display::shape::text::content::TextLocation;
|
||||
use crate::display::shape::text::content::TextComponentContent;
|
||||
use crate::display::shape::text::content::line::LineRef;
|
||||
use crate::display::shape::text::buffer::glyph_square::point_to_iterable;
|
||||
@ -12,34 +12,33 @@ use crate::display::shape::text::font::Fonts;
|
||||
|
||||
use nalgebra::Point2;
|
||||
use nalgebra::Translation2;
|
||||
use std::collections::HashSet;
|
||||
use std::cmp::Ordering;
|
||||
use std::iter::once;
|
||||
use std::ops::Range;
|
||||
use web_sys::WebGlBuffer;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Cursor ===
|
||||
// ==============
|
||||
|
||||
/// Cursor in TextComponent with its selection
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone,Debug,Eq,PartialEq)]
|
||||
pub struct Cursor {
|
||||
pub position : CharPosition,
|
||||
pub selected_to : CharPosition,
|
||||
pub position : TextLocation,
|
||||
pub selected_to : TextLocation,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
/// Create a new cursor at given position and without any selection.
|
||||
pub fn new(position:CharPosition) -> Self {
|
||||
pub fn new(position:TextLocation) -> Self {
|
||||
Cursor {position,
|
||||
selected_to : position
|
||||
}
|
||||
}
|
||||
|
||||
/// Get range of selected text by this cursor.
|
||||
pub fn selection_range(&self) -> Range<CharPosition> {
|
||||
pub fn selection_range(&self) -> Range<TextLocation> {
|
||||
match self.position.cmp(&self.selected_to) {
|
||||
Ordering::Equal => self.position..self.position,
|
||||
Ordering::Greater => self.selected_to..self.position,
|
||||
@ -47,8 +46,19 @@ impl Cursor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the selection to cover the given range. Cursor itself may be moved, and will be
|
||||
/// on the same side of selection as before.
|
||||
pub fn extend_selection(&mut self, range:&Range<TextLocation>) {
|
||||
let new_start = range.start.min(self.position).min(self.selected_to);
|
||||
let new_end = range.end.max(self.position).max(self.selected_to);
|
||||
*self = match self.position.cmp(&self.selected_to) {
|
||||
Ordering::Less => Cursor{position:new_start, selected_to:new_end },
|
||||
_ => Cursor{position:new_end , selected_to:new_start},
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if char at given position is selected.
|
||||
pub fn is_char_selected(&self, position:CharPosition) -> bool {
|
||||
pub fn is_char_selected(&self, position:TextLocation) -> bool {
|
||||
self.selection_range().contains(&position)
|
||||
}
|
||||
|
||||
@ -65,20 +75,156 @@ impl Cursor {
|
||||
// (https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1).
|
||||
pub fn render_position(&self, content:&mut TextComponentContent, fonts:&mut Fonts)
|
||||
-> Point2<f64>{
|
||||
let x = Self::x_position_of_cursor_at(&self.position,content,fonts);
|
||||
let y = self.current_line(content).start_point().y;
|
||||
Point2::new(x,y)
|
||||
}
|
||||
|
||||
fn x_position_of_cursor_at
|
||||
(at:&TextLocation, content:&mut TextComponentContent, fonts:&mut Fonts)
|
||||
-> f64 {
|
||||
let font = fonts.get_render_info(content.font);
|
||||
let mut line = self.current_line(content);
|
||||
if self.position.column > 0 {
|
||||
let char_index = self.position.column - 1;
|
||||
let x = line.get_char_x_range(char_index,font).end;
|
||||
let y = line.start_point().y;
|
||||
Point2::new(x.into(),y)
|
||||
let mut line = content.line(at.line);
|
||||
if at.column > 0 {
|
||||
let char_index = at.column - 1;
|
||||
line.get_char_x_range(char_index,font).end.into()
|
||||
} else {
|
||||
line.start_point()
|
||||
line.start_point().x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === Navigation ===
|
||||
// ==================
|
||||
|
||||
/// An enum representing cursor moving step. The steps are based of possible keystrokes (arrows,
|
||||
/// Home, End, Ctrl+Home, etc.)
|
||||
#[derive(Debug,Eq,Hash,PartialEq)]
|
||||
pub enum Step {Left,Right,Up,Down,LineBegin,LineEnd,DocBegin,DocEnd}
|
||||
|
||||
/// A struct for cursor navigation process
|
||||
pub struct CursorNavigation<'a,'b> {
|
||||
pub content : &'a mut TextComponentContent,
|
||||
pub fonts : &'b mut Fonts,
|
||||
pub selecting : bool
|
||||
}
|
||||
|
||||
impl<'a,'b> CursorNavigation<'a,'b> {
|
||||
/// Jump cursor directly to given position.
|
||||
pub fn move_cursor_to_position(&self, cursor:&mut Cursor, to:TextLocation) {
|
||||
cursor.position = to;
|
||||
if !self.selecting {
|
||||
cursor.selected_to = to;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor by given step.
|
||||
pub fn move_cursor(&mut self, cursor:&mut Cursor, step:&Step) {
|
||||
let new_position = self.new_position(cursor.position,&step);
|
||||
self.move_cursor_to_position(cursor,new_position);
|
||||
}
|
||||
|
||||
/// Get cursor position at end of given line.
|
||||
pub fn line_end_position(&self, line_index:usize) -> TextLocation {
|
||||
TextLocation {
|
||||
line : line_index,
|
||||
column : self.content.lines[line_index].len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cursor position at end of whole content
|
||||
pub fn content_end_position(&self) -> TextLocation {
|
||||
TextLocation {
|
||||
column : self.content.lines.last().unwrap().len(),
|
||||
line : self.content.lines.len() - 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cursor position for the next char from given position. Returns none if at end of
|
||||
/// whole document.
|
||||
pub fn next_char_position(&self, position:&TextLocation) -> Option<TextLocation> {
|
||||
let current_line = &self.content.lines[position.line];
|
||||
let next_column = Some(position.column + 1).filter(|c| *c <= current_line.len());
|
||||
let next_line = Some(position.line + 1) .filter(|l| *l < self.content.lines.len());
|
||||
match (next_column,next_line) {
|
||||
(None , None ) => None,
|
||||
(None , Some(line)) => Some(TextLocation::at_line_begin(line)),
|
||||
(Some(column) , _ ) => Some(TextLocation {column, ..*position})
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cursor position for the previous char from given position. Returns none if at begin of
|
||||
/// whole document.
|
||||
pub fn prev_char_position(&self, position:&TextLocation) -> Option<TextLocation> {
|
||||
let prev_column = position.column.checked_sub(1);
|
||||
let prev_line = position.line.checked_sub(1);
|
||||
match (prev_column,prev_line) {
|
||||
(None , None ) => None,
|
||||
(None , Some(line)) => Some(self.line_end_position(line)),
|
||||
(Some(column) , _ ) => Some(TextLocation {column, ..*position})
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cursor position one line above the given position, such the new x coordinate of
|
||||
/// displayed cursor on the screen will be nearest the current value.
|
||||
pub fn line_up_position(&mut self, position:&TextLocation) -> Option<TextLocation> {
|
||||
let prev_line = position.line.checked_sub(1);
|
||||
prev_line.map(|line| self.near_same_x_in_another_line(position,line))
|
||||
}
|
||||
|
||||
/// Get cursor position one line behind the given position, such the new x coordinate of
|
||||
/// displayed cursor on the screen will be nearest the current value.
|
||||
pub fn line_down_position(&mut self, position:&TextLocation) -> Option<TextLocation> {
|
||||
let next_line = Some(position.line + 1).filter(|l| *l < self.content.lines.len());
|
||||
next_line.map(|line| self.near_same_x_in_another_line(position,line))
|
||||
}
|
||||
|
||||
/// New position of cursor at `position` after applying `step`.
|
||||
fn new_position(&mut self, position: TextLocation, step:&Step) -> TextLocation {
|
||||
match step {
|
||||
Step::Left => self.prev_char_position(&position).unwrap_or(position),
|
||||
Step::Right => self.next_char_position(&position).unwrap_or(position),
|
||||
Step::Up => self.line_up_position(&position).unwrap_or(position),
|
||||
Step::Down => self.line_down_position(&position).unwrap_or(position),
|
||||
Step::LineBegin => TextLocation::at_line_begin(position.line),
|
||||
Step::LineEnd => self.line_end_position(position.line),
|
||||
Step::DocBegin => TextLocation::at_document_begin(),
|
||||
Step::DocEnd => self.content_end_position(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cursor position on another line, such that the new x coordinate of
|
||||
/// displayed cursor on the screen will be nearest the current value.
|
||||
fn near_same_x_in_another_line(&mut self, position:&TextLocation, line:usize)
|
||||
-> TextLocation {
|
||||
let x_position = Cursor::x_position_of_cursor_at(position,self.content,self.fonts);
|
||||
let column = self.column_near_x(line,x_position);
|
||||
TextLocation {line,column}
|
||||
}
|
||||
|
||||
/// Get the column number in given line, so the cursor will be as near as possible the
|
||||
/// `x_position` in _text space_. See `display::shape::text::content::line::Line`
|
||||
/// documentation for details about _text space_.
|
||||
fn column_near_x(&mut self, line_index:usize, x_position:f64) -> usize {
|
||||
let font = self.fonts.get_render_info(self.content.font);
|
||||
let mut line = self.content.line(line_index);
|
||||
let x = x_position as f32;
|
||||
let char_at_x = line.find_char_at_x_position(x,font);
|
||||
let nearer_to_end = |range:Range<f32>| range.end - x < x - range.start;
|
||||
let mut nearer_to_chars_end = |index| nearer_to_end(line.get_char_x_range(index,font));
|
||||
match char_at_x {
|
||||
Some(index) if nearer_to_chars_end(index) => index + 1,
|
||||
Some(index) => index,
|
||||
None => line.len()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============
|
||||
// === Cursors ===
|
||||
// ===============
|
||||
@ -102,9 +248,9 @@ lazy_static! {
|
||||
/// a WebGL buffer with vertex positions of all cursors.
|
||||
#[derive(Debug)]
|
||||
pub struct Cursors {
|
||||
pub cursors : Vec<Cursor>,
|
||||
pub dirty_cursors : HashSet<usize>,
|
||||
pub buffer : WebGlBuffer,
|
||||
pub cursors : Vec<Cursor>,
|
||||
pub dirty : bool,
|
||||
pub buffer : Option<WebGlBuffer>,
|
||||
}
|
||||
|
||||
impl Cursors {
|
||||
@ -112,33 +258,92 @@ impl Cursors {
|
||||
/// Create empty `Cursors` structure.
|
||||
pub fn new(gl_context:&Context) -> Self {
|
||||
Cursors {
|
||||
cursors : Vec::new(),
|
||||
dirty_cursors : HashSet::new(),
|
||||
buffer : gl_context.create_buffer().unwrap()
|
||||
cursors : Vec::new(),
|
||||
dirty : false,
|
||||
buffer : gl_context.create_buffer()
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all current cursors and replace them with single cursor without any selection.
|
||||
pub fn set_cursor(&mut self, position:CharPosition) {
|
||||
self.cursors = vec![Cursor::new(position)];
|
||||
self.dirty_cursors = once(0).collect();
|
||||
}
|
||||
|
||||
/// Add new cursor without selection.
|
||||
pub fn add_cursor(&mut self, position:CharPosition) {
|
||||
let new_index = self.cursors.len();
|
||||
self.cursors.push(Cursor::new(position));
|
||||
self.dirty_cursors.insert(new_index);
|
||||
}
|
||||
|
||||
/// Update the cursors' buffer data.
|
||||
pub fn update_buffer_data
|
||||
(&mut self, gl_context:&Context, content:&mut TextComponentContent, fonts:&mut Fonts) {
|
||||
let cursors = self.cursors.iter();
|
||||
let cursors_vertices = cursors.map(|cursor| Self::cursor_vertices(cursor,content,fonts));
|
||||
let buffer_data = cursors_vertices.flatten().collect_vec();
|
||||
set_buffer_data(gl_context,&self.buffer,buffer_data.as_slice());
|
||||
self.dirty_cursors.clear();
|
||||
set_buffer_data(gl_context,self.buffer.as_ref().unwrap(),buffer_data.as_slice());
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
/// Removes all current cursors and replace them with single cursor without any selection.
|
||||
pub fn set_cursor(&mut self, position: TextLocation) {
|
||||
self.cursors = vec![Cursor::new(position)];
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Add new cursor without selection.
|
||||
pub fn add_cursor(&mut self, position: TextLocation) {
|
||||
self.cursors.push(Cursor::new(position));
|
||||
self.merge_overlapping_cursors();
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Do the navigation step of all cursors.
|
||||
///
|
||||
/// If after this operation some of the cursors occupies the same position, or their selected
|
||||
/// area overlap, they are irreversibly merged.
|
||||
pub fn navigate_all_cursors(&mut self, navigaton:&mut CursorNavigation, step:&Step) {
|
||||
self.cursors.iter_mut().for_each(|cursor| navigaton.move_cursor(cursor,&step));
|
||||
self.merge_overlapping_cursors();
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Number of vertices in cursors' buffer.
|
||||
pub fn vertices_count(&self) -> usize {
|
||||
self.cursors.len() * CURSOR_BASE_LAYOUT_SIZE
|
||||
}
|
||||
|
||||
/// Merge overlapping cursors
|
||||
///
|
||||
/// This function checks all cursors, and merge each pair where cursors are at the same position
|
||||
/// or their selection overlap.
|
||||
///
|
||||
/// The merged pair will be replaced with one cursor with selection being a sum of selections of
|
||||
/// 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);
|
||||
match merged {
|
||||
Some(merged_range) => {
|
||||
self.cursors[i-1].extend_selection(&merged_range);
|
||||
self.cursors.remove(i);
|
||||
},
|
||||
None => {
|
||||
i += 1
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if two cursors should be merged and returns new selection range after merging if they
|
||||
/// shoukd, and `None` otherwise.
|
||||
fn merged_selection_range(&self, left_cursor_index:usize, right_cursor_index:usize)
|
||||
-> Option<Range<TextLocation>> {
|
||||
let left_cursor_position = self.cursors[left_cursor_index].position;
|
||||
let left_cursor_range = self.cursors[left_cursor_index].selection_range();
|
||||
let right_cursor_position = self.cursors[right_cursor_index].position;
|
||||
let right_cursor_range = self.cursors[right_cursor_index].selection_range();
|
||||
let are_cursor_at_same_position = left_cursor_position == right_cursor_position;
|
||||
let are_ranges_overlapping = right_cursor_range.start < left_cursor_range.end;
|
||||
let are_cursors_merged = are_cursor_at_same_position || are_ranges_overlapping;
|
||||
are_cursors_merged.and_option_from(|| {
|
||||
let new_start = left_cursor_range.start.min(right_cursor_range.start);
|
||||
let new_end = left_cursor_range.end .max(right_cursor_range.end );
|
||||
Some(new_start..new_end)
|
||||
})
|
||||
}
|
||||
|
||||
fn cursor_vertices(cursor:&Cursor, content:&mut TextComponentContent, fonts:&mut Fonts)
|
||||
@ -150,8 +355,146 @@ impl Cursors {
|
||||
on_position.map(point_to_iterable).flatten().collect()
|
||||
}
|
||||
|
||||
/// Number of vertices in cursors' buffer.
|
||||
pub fn vertices_count(&self) -> usize {
|
||||
self.cursors.len() * CURSOR_BASE_LAYOUT_SIZE
|
||||
#[cfg(test)]
|
||||
fn mock(cursors:Vec<Cursor>) -> Self {
|
||||
Cursors{cursors,
|
||||
dirty : false,
|
||||
buffer : None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use Step::*;
|
||||
|
||||
use basegl_core_msdf_sys::test_utils::TestAfterInit;
|
||||
use std::future::Future;
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
use wasm_bindgen_test::wasm_bindgen_test_configure;
|
||||
use crate::display::shape::text::cursor::Step::{LineBegin, DocBegin, LineEnd};
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test(async)]
|
||||
fn moving_cursors() -> impl Future<Output=()> {
|
||||
TestAfterInit::schedule(||{
|
||||
let text = "FirstLine.\nSecondLine\nThirdLine";
|
||||
let initial_cursors = vec!
|
||||
[ Cursor::new(TextLocation {line:0, column:0 })
|
||||
, Cursor::new(TextLocation {line:1, column:0 })
|
||||
, Cursor::new(TextLocation {line:1, column:6 })
|
||||
, Cursor::new(TextLocation {line:1, column:10})
|
||||
, Cursor::new(TextLocation {line:2, column:9 })
|
||||
];
|
||||
let mut expected_positions = HashMap::<Step,Vec<(usize,usize)>>::new();
|
||||
expected_positions.insert(Left, vec![(0,0),(0,10),(1,5),(1,9),(2,8)]);
|
||||
expected_positions.insert(Right, vec![(0,1),(1,1),(1,7),(2,0),(2,9)]);
|
||||
expected_positions.insert(Up, vec![(0,0),(0,6),(0,10),(1,9)]);
|
||||
expected_positions.insert(Down, vec![(1,0),(2,0),(2,6),(2,9)]);
|
||||
expected_positions.insert(LineBegin, vec![(0,0),(1,0),(2,0)]);
|
||||
expected_positions.insert(LineEnd, vec![(0,10),(1,10),(2,9)]);
|
||||
expected_positions.insert(DocBegin, vec![(0,0)]);
|
||||
expected_positions.insert(DocEnd, vec![(2,9)]);
|
||||
|
||||
let mut fonts = Fonts::new();
|
||||
let font = fonts.load_embedded_font("DejaVuSansMono").unwrap();
|
||||
let mut content = TextComponentContent::new(font,text);
|
||||
let mut navigation = CursorNavigation {
|
||||
content: &mut content,
|
||||
fonts: &mut fonts,
|
||||
selecting: false
|
||||
};
|
||||
|
||||
for step in &[Left,Right,Up,Down,LineBegin,LineEnd,DocBegin,DocEnd] {
|
||||
let mut cursors = Cursors::mock(initial_cursors.clone());
|
||||
|
||||
cursors.navigate_all_cursors(&mut navigation,step);
|
||||
let expected = expected_positions.get(step).unwrap();
|
||||
let current = cursors.cursors.iter().map(|c| (c.position.line, c.position.column));
|
||||
assert_eq!(expected,¤t.collect_vec(), "Error for step {:?}", step);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test(async)]
|
||||
fn moving_without_select() -> impl Future<Output=()> {
|
||||
TestAfterInit::schedule(||{
|
||||
let text = "FirstLine\nSecondLine";
|
||||
let initial_cursor = Cursor {
|
||||
position : TextLocation {line:1, column:0},
|
||||
selected_to : TextLocation {line:0, column:1}
|
||||
};
|
||||
let initial_cursors = vec![initial_cursor];
|
||||
let new_position = TextLocation {line:1,column:10};
|
||||
|
||||
let mut fonts = Fonts::new();
|
||||
let font = fonts.load_embedded_font("DejaVuSansMono").unwrap();
|
||||
let mut content = TextComponentContent::new(font,text);
|
||||
let mut navigation = CursorNavigation {
|
||||
content: &mut content,
|
||||
fonts: &mut fonts,
|
||||
selecting: false
|
||||
};
|
||||
let mut cursors = Cursors::mock(initial_cursors.clone());
|
||||
cursors.navigate_all_cursors(&mut navigation,&LineEnd);
|
||||
assert_eq!(new_position, cursors.cursors.first().unwrap().position);
|
||||
assert_eq!(new_position, cursors.cursors.first().unwrap().selected_to);
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test(async)]
|
||||
fn moving_with_select() -> impl Future<Output=()> {
|
||||
TestAfterInit::schedule(||{
|
||||
let text = "FirstLine\nSecondLine";
|
||||
let initial_loc = TextLocation {line:0,column:1};
|
||||
let initial_cursors = vec![Cursor::new(initial_loc)];
|
||||
let new_loc = TextLocation {line:0,column:9};
|
||||
|
||||
let mut fonts = Fonts::new();
|
||||
let font = fonts.load_embedded_font("DejaVuSansMono").unwrap();
|
||||
let mut content = TextComponentContent::new(font,text);
|
||||
let mut navigation = CursorNavigation {
|
||||
content: &mut content,
|
||||
fonts: &mut fonts,
|
||||
selecting: true
|
||||
};
|
||||
let mut cursors = Cursors::mock(initial_cursors.clone());
|
||||
cursors.navigate_all_cursors(&mut navigation,&LineEnd);
|
||||
assert_eq!(new_loc , cursors.cursors.first().unwrap().position);
|
||||
assert_eq!(initial_loc, cursors.cursors.first().unwrap().selected_to);
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test(async)]
|
||||
fn merging_selection_after_moving() -> impl Future<Output=()> {
|
||||
TestAfterInit::schedule(||{
|
||||
let make_char_loc = |(line,column):(usize,usize)| TextLocation {line,column};
|
||||
let cursor_on_left = |range:&Range<(usize,usize)>| Cursor {
|
||||
position : make_char_loc(range.start),
|
||||
selected_to : make_char_loc(range.end)
|
||||
};
|
||||
let cursor_on_right = |range:&Range<(usize,usize)>| Cursor {
|
||||
position : make_char_loc(range.end),
|
||||
selected_to : make_char_loc(range.start)
|
||||
};
|
||||
merging_selection_after_moving_case(cursor_on_left);
|
||||
merging_selection_after_moving_case(cursor_on_right);
|
||||
})
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
cursors.merge_overlapping_cursors();
|
||||
|
||||
assert_eq!(expected_cursors, cursors.cursors);
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ use crate::data::dirty::traits::*;
|
||||
|
||||
use nalgebra::Point2;
|
||||
use nalgebra::Vector2;
|
||||
use crate::display::shape::text::content::CharPosition;
|
||||
use crate::display::shape::text::content::TextLocation;
|
||||
use crate::display::shape::text::content::TextChange;
|
||||
use crate::display::shape::text::{TextComponentBuilder, Color};
|
||||
use crate::display::shape::text::TextComponentBuilder;
|
||||
use crate::display::shape::text::Color;
|
||||
use crate::display::shape::text::TextComponentProperties;
|
||||
use crate::system::web::forward_panic_hook_to_console;
|
||||
use crate::display::shape::text::cursor::Step::Right;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(dead_code)]
|
||||
@ -38,7 +40,7 @@ pub fn run_example_text() {
|
||||
color: Color { r: 0.0, g: 0.8, b: 0.0, a: 1.0 },
|
||||
}
|
||||
}.build();
|
||||
text_component.cursors.add_cursor(CharPosition { line: 0, column: 0 });
|
||||
text_component.cursors.add_cursor(TextLocation { line: 0, column: 0 });
|
||||
workspace.text_components.push(text_component);
|
||||
world.workspace_dirty.set();
|
||||
}
|
||||
@ -71,10 +73,11 @@ fn animate_text_component
|
||||
( world:&World
|
||||
, typed_chars:&mut Vec<CharToPush>
|
||||
, start_scrolling:f64) {
|
||||
let mut world = world.rc.borrow_mut();
|
||||
let workspace = &mut world.workspace;
|
||||
let editor = workspace.text_components.first_mut().unwrap();
|
||||
let now = js_sys::Date::now();
|
||||
let world : &mut WorldData = &mut world.rc.borrow_mut();
|
||||
let workspace = &mut world.workspace;
|
||||
let editor = workspace.text_components.first_mut().unwrap();
|
||||
let fonts = &mut world.fonts;
|
||||
let now = js_sys::Date::now();
|
||||
|
||||
let to_type_now = typed_chars.drain_filter(|ch| ch.time <= now);
|
||||
for ch in to_type_now {
|
||||
@ -82,12 +85,7 @@ fn animate_text_component
|
||||
let string = ch.a_char.to_string();
|
||||
let change = TextChange::insert(cursor.position, string.as_str());
|
||||
editor.content.make_change(change);
|
||||
let new_cursor_position = CharPosition {
|
||||
line: editor.content.lines.len()-1,
|
||||
column : editor.content.lines.last().unwrap().len(),
|
||||
};
|
||||
cursor.position = new_cursor_position;
|
||||
editor.cursors.dirty_cursors.insert(0);
|
||||
editor.navigate_cursors(Right,false,fonts);
|
||||
}
|
||||
if start_scrolling <= js_sys::Date::now() {
|
||||
editor.scroll(Vector2::new(0.0, -0.01));
|
||||
|
@ -76,7 +76,7 @@ mod tests {
|
||||
use super::WorldTest;
|
||||
use basegl::display::shape::text::Color;
|
||||
use basegl::display::shape::text::content::TextChange;
|
||||
use basegl::display::shape::text::content::CharPosition;
|
||||
use basegl::display::shape::text::content::TextLocation;
|
||||
use basegl::display::shape::text::TextComponentBuilder;
|
||||
use basegl::display::world::WorldData;
|
||||
use basegl::display::shape::text::TextComponentProperties;
|
||||
@ -185,8 +185,8 @@ mod tests {
|
||||
for _ in 0..20 {
|
||||
let workspace = &mut world.workspace;
|
||||
let text_component = &mut workspace.text_components[0];
|
||||
let replace_from = CharPosition{line:1, column:2};
|
||||
let replace_to = CharPosition{line:1, column:3};
|
||||
let replace_from = TextLocation {line:1, column:2};
|
||||
let replace_to = TextLocation {line:1, column:3};
|
||||
let replaced_range = replace_from..replace_to;
|
||||
let change = TextChange::replace(replaced_range, "abc");
|
||||
text_component.content.make_change(change);
|
||||
@ -208,8 +208,8 @@ mod tests {
|
||||
let world : &mut WorldData = &mut world_test.world_ptr.borrow_mut();
|
||||
let workspace = &mut world.workspace;
|
||||
let text_component = &mut workspace.text_components[0];
|
||||
let position = CharPosition{line:1, column:0};
|
||||
let change = TextChange::insert(position, TEST_TEXT);
|
||||
let location = TextLocation {line:1, column:0};
|
||||
let change = TextChange::insert(location,TEST_TEXT);
|
||||
text_component.content.make_change(change);
|
||||
world.workspace_dirty.set();
|
||||
world.update();
|
||||
@ -256,10 +256,10 @@ mod tests {
|
||||
text: text.clone()
|
||||
};
|
||||
let mut text_component = builder.build();
|
||||
let cursor_position_1 = CharPosition{line:0, column: 10};
|
||||
let cursor_position_2 = CharPosition{line:1, column: 6};
|
||||
text_component.cursors.add_cursor(cursor_position_1);
|
||||
text_component.cursors.add_cursor(cursor_position_2);
|
||||
let cursor_loc_1 = TextLocation {line:0, column: 10};
|
||||
let cursor_loc_2 = TextLocation {line:1, column: 6};
|
||||
text_component.cursors.add_cursor(cursor_loc_1);
|
||||
text_component.cursors.add_cursor(cursor_loc_2);
|
||||
workspace.text_components.push(text_component);
|
||||
}
|
||||
world.workspace_dirty.set();
|
||||
|
Loading…
Reference in New Issue
Block a user