Original commit: 9d1b18d681
This commit is contained in:
Adam Obuchowicz 2020-01-10 16:17:40 +01:00 committed by GitHub
parent 65a687b911
commit 1b6094a126
6 changed files with 471 additions and 92 deletions

View File

@ -157,7 +157,7 @@ pub fn set_buffer_data(gl_context:&Context, buffer:&WebGlBuffer, data:&[f32]) {
/// wasm-bindgen examples /// wasm-bindgen examples
/// (https://rustwasm.github.io/wasm-bindgen/examples/webgl.html) /// (https://rustwasm.github.io/wasm-bindgen/examples/webgl.html)
fn set_bound_buffer_data(gl_context:&Context, target:u32, data:&[f32]) { 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] unsafe { // Note [unsafe buffer_data]
let float_array = Float32Array::view(&data); let float_array = Float32Array::view(&data);
gl_context.buffer_data_with_array_buffer_view(target,&float_array,usage); gl_context.buffer_data_with_array_buffer_view(target,&float_array,usage);

View File

@ -20,6 +20,8 @@ use crate::display::render::webgl::Context;
use crate::display::shape::text::buffer::TextComponentBuffers; use crate::display::shape::text::buffer::TextComponentBuffers;
use crate::display::shape::text::content::TextComponentContent; use crate::display::shape::text::content::TextComponentContent;
use crate::display::shape::text::cursor::Cursors; 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::FontId;
use crate::display::shape::text::font::Fonts; use crate::display::shape::text::font::Fonts;
use crate::display::shape::text::msdf::MsdfTexture; 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) { fn refresh_content_buffers(&mut self, fonts:&mut Fonts) {
let refresh_info = self.content.refresh_info(fonts); let refresh_info = self.content.refresh_info(fonts);
self.buffers.refresh(&self.gl_context,refresh_info); self.buffers.refresh(&self.gl_context,refresh_info);
@ -165,8 +173,7 @@ impl TextComponent {
} }
fn refresh_cursors(&mut self, fonts:&mut Fonts) { fn refresh_cursors(&mut self, fonts:&mut Fonts) {
let cursors_changed = !self.cursors.dirty_cursors.is_empty(); if self.cursors.dirty {
if cursors_changed {
let gl_context = &self.gl_context; let gl_context = &self.gl_context;
let content = &mut self.content; let content = &mut self.content;
self.cursors.update_buffer_data(gl_context,content,fonts); self.cursors.update_buffer_data(gl_context,content,fonts);
@ -209,6 +216,7 @@ impl TextComponent {
} }
fn display_cursors(&self) { fn display_cursors(&self) {
let buffer = self.cursors.buffer.as_ref().unwrap();
let gl_context = &self.gl_context; let gl_context = &self.gl_context;
let to_scene_matrix = self.to_scene_matrix(); let to_scene_matrix = self.to_scene_matrix();
let program = &self.cursors_program; let program = &self.cursors_program;
@ -216,7 +224,7 @@ impl TextComponent {
gl_context.use_program(Some(&program.gl_program)); gl_context.use_program(Some(&program.gl_program));
self.cursors_program.set_to_scene_transformation(&to_scene_matrix); 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.line_width(2.0);
gl_context.draw_arrays(WebGl2RenderingContext::LINES,0,vertices_count); gl_context.draw_arrays(WebGl2RenderingContext::LINES,0,vertices_count);
gl_context.line_width(1.0); gl_context.line_width(1.0);

View File

@ -82,21 +82,21 @@ pub enum ChangeType {
/// A structure describing a text operation in one place. /// A structure describing a text operation in one place.
pub struct TextChange { pub struct TextChange {
replaced : Range<CharPosition>, replaced : Range<TextLocation>,
lines : Vec<Vec<char>>, lines : Vec<Vec<char>>,
} }
impl TextChange { impl TextChange {
/// Creates operation which inserts text at given position. /// 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 { TextChange {
replaced : position..position, replaced : at..at,
lines : TextComponentContent::split_to_lines(text).map(|s| s.chars().collect_vec()).collect() lines : Self::mk_lines_as_char_vector(text)
} }
} }
/// Creates operation which deletes text at given range. /// Creates operation which deletes text at given range.
pub fn delete(range:Range<CharPosition>) -> Self { pub fn delete(range:Range<TextLocation>) -> Self {
TextChange { TextChange {
replaced : range, replaced : range,
lines : vec![vec![]], lines : vec![vec![]],
@ -104,9 +104,9 @@ impl TextChange {
} }
/// Creates operation which replaces text at given range with given string. /// 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, 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 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 === // === TextComponentContent ===
// ============================ // ============================
@ -137,13 +174,6 @@ pub struct TextComponentContent {
pub font : FontId, 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. /// References to all needed stuff for generating buffer's data.
pub struct RefreshInfo<'a, 'b> { pub struct RefreshInfo<'a, 'b> {
pub lines : &'a mut [Line], pub lines : &'a mut [Line],
@ -177,7 +207,7 @@ impl TextComponentContent {
} }
/// LineRef structure for line at given index. /// LineRef structure for line at given index.
pub fn line(& mut self, index:usize) -> LineRef { pub fn line(&mut self, index:usize) -> LineRef {
LineRef { LineRef {
line : &mut self.lines[index], line : &mut self.lines[index],
line_id : index, line_id : index,
@ -310,10 +340,10 @@ mod test {
#[test] #[test]
fn edit_single_line() { fn edit_single_line() {
let text = "Line a\nLine b\nLine c"; let text = "Line a\nLine b\nLine c";
let delete_from = CharPosition{line:1, column:0}; let delete_from = TextLocation {line:1, column:0};
let delete_to = CharPosition{line:1, column:4}; let delete_to = TextLocation {line:1, column:4};
let deleted_range = delete_from..delete_to; 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 delete = TextChange::delete(deleted_range.clone());
let replace = TextChange::replace(deleted_range, "text"); let replace = TextChange::replace(deleted_range, "text");
@ -340,12 +370,12 @@ mod test {
fn insert_multiple_lines() { fn insert_multiple_lines() {
let text = "Line a\nLine b\nLine c"; let text = "Line a\nLine b\nLine c";
let inserted = "Ins a\nIns b"; let inserted = "Ins a\nIns b";
let begin_position = CharPosition{line:0, column:0}; let begin_loc = TextLocation {line:0, column:0};
let middle_position = CharPosition{line:1, column:2}; let middle_loc = TextLocation {line:1, column:2};
let end_position = CharPosition{line:2, column:6}; let end_loc = TextLocation {line:2, column:6};
let insert_at_begin = TextChange::insert(begin_position , inserted); let insert_at_begin = TextChange::insert(begin_loc ,inserted);
let insert_in_middle = TextChange::insert(middle_position, inserted); let insert_in_middle = TextChange::insert(middle_loc,inserted);
let insert_at_end = TextChange::insert(end_position , inserted); let insert_at_end = TextChange::insert(end_loc ,inserted);
let mut content = TextComponentContent::new(0,text); let mut content = TextComponentContent::new(0,text);
@ -376,8 +406,8 @@ mod test {
#[test] #[test]
fn delete_multiple_lines() { fn delete_multiple_lines() {
let text = "Line a\nLine b\nLine c"; let text = "Line a\nLine b\nLine c";
let delete_from = CharPosition{line:0, column:2}; let delete_from = TextLocation {line:0, column:2};
let delete_to = CharPosition{line:2, column:3}; let delete_to = TextLocation {line:2, column:3};
let deleted_range = delete_from..delete_to; let deleted_range = delete_from..delete_to;
let delete = TextChange::delete(deleted_range); let delete = TextChange::delete(deleted_range);

View File

@ -4,7 +4,7 @@ use crate::prelude::*;
use crate::display::render::webgl::Context; use crate::display::render::webgl::Context;
use crate::display::render::webgl::set_buffer_data; 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::TextComponentContent;
use crate::display::shape::text::content::line::LineRef; use crate::display::shape::text::content::line::LineRef;
use crate::display::shape::text::buffer::glyph_square::point_to_iterable; 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::Point2;
use nalgebra::Translation2; use nalgebra::Translation2;
use std::collections::HashSet;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::iter::once;
use std::ops::Range; use std::ops::Range;
use web_sys::WebGlBuffer; use web_sys::WebGlBuffer;
// ============== // ==============
// === Cursor === // === Cursor ===
// ============== // ==============
/// Cursor in TextComponent with its selection /// Cursor in TextComponent with its selection
#[derive(Debug)] #[derive(Clone,Debug,Eq,PartialEq)]
pub struct Cursor { pub struct Cursor {
pub position : CharPosition, pub position : TextLocation,
pub selected_to : CharPosition, pub selected_to : TextLocation,
} }
impl Cursor { impl Cursor {
/// Create a new cursor at given position and without any selection. /// 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, Cursor {position,
selected_to : position selected_to : position
} }
} }
/// Get range of selected text by this cursor. /// 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) { match self.position.cmp(&self.selected_to) {
Ordering::Equal => self.position..self.position, Ordering::Equal => self.position..self.position,
Ordering::Greater => self.selected_to..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. /// 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) self.selection_range().contains(&position)
} }
@ -65,20 +75,156 @@ impl Cursor {
// (https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1). // (https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1).
pub fn render_position(&self, content:&mut TextComponentContent, fonts:&mut Fonts) pub fn render_position(&self, content:&mut TextComponentContent, fonts:&mut Fonts)
-> Point2<f64>{ -> 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 font = fonts.get_render_info(content.font);
let mut line = self.current_line(content); let mut line = content.line(at.line);
if self.position.column > 0 { if at.column > 0 {
let char_index = self.position.column - 1; let char_index = at.column - 1;
let x = line.get_char_x_range(char_index,font).end; line.get_char_x_range(char_index,font).end.into()
let y = line.start_point().y;
Point2::new(x.into(),y)
} else { } 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 === // === Cursors ===
// =============== // ===============
@ -102,9 +248,9 @@ lazy_static! {
/// a WebGL buffer with vertex positions of all cursors. /// a WebGL buffer with vertex positions of all cursors.
#[derive(Debug)] #[derive(Debug)]
pub struct Cursors { pub struct Cursors {
pub cursors : Vec<Cursor>, pub cursors : Vec<Cursor>,
pub dirty_cursors : HashSet<usize>, pub dirty : bool,
pub buffer : WebGlBuffer, pub buffer : Option<WebGlBuffer>,
} }
impl Cursors { impl Cursors {
@ -112,33 +258,92 @@ impl Cursors {
/// Create empty `Cursors` structure. /// Create empty `Cursors` structure.
pub fn new(gl_context:&Context) -> Self { pub fn new(gl_context:&Context) -> Self {
Cursors { Cursors {
cursors : Vec::new(), cursors : Vec::new(),
dirty_cursors : HashSet::new(), dirty : false,
buffer : gl_context.create_buffer().unwrap() 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. /// Update the cursors' buffer data.
pub fn update_buffer_data pub fn update_buffer_data
(&mut self, gl_context:&Context, content:&mut TextComponentContent, fonts:&mut Fonts) { (&mut self, gl_context:&Context, content:&mut TextComponentContent, fonts:&mut Fonts) {
let cursors = self.cursors.iter(); let cursors = self.cursors.iter();
let cursors_vertices = cursors.map(|cursor| Self::cursor_vertices(cursor,content,fonts)); let cursors_vertices = cursors.map(|cursor| Self::cursor_vertices(cursor,content,fonts));
let buffer_data = cursors_vertices.flatten().collect_vec(); let buffer_data = cursors_vertices.flatten().collect_vec();
set_buffer_data(gl_context,&self.buffer,buffer_data.as_slice()); set_buffer_data(gl_context,self.buffer.as_ref().unwrap(),buffer_data.as_slice());
self.dirty_cursors.clear(); 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) 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() on_position.map(point_to_iterable).flatten().collect()
} }
/// Number of vertices in cursors' buffer. #[cfg(test)]
pub fn vertices_count(&self) -> usize { fn mock(cursors:Vec<Cursor>) -> Self {
self.cursors.len() * CURSOR_BASE_LAYOUT_SIZE 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,&current.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);
} }
} }

View File

@ -8,11 +8,13 @@ use crate::data::dirty::traits::*;
use nalgebra::Point2; use nalgebra::Point2;
use nalgebra::Vector2; 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::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::display::shape::text::TextComponentProperties;
use crate::system::web::forward_panic_hook_to_console; use crate::system::web::forward_panic_hook_to_console;
use crate::display::shape::text::cursor::Step::Right;
#[wasm_bindgen] #[wasm_bindgen]
#[allow(dead_code)] #[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 }, color: Color { r: 0.0, g: 0.8, b: 0.0, a: 1.0 },
} }
}.build(); }.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); workspace.text_components.push(text_component);
world.workspace_dirty.set(); world.workspace_dirty.set();
} }
@ -71,10 +73,11 @@ fn animate_text_component
( world:&World ( world:&World
, typed_chars:&mut Vec<CharToPush> , typed_chars:&mut Vec<CharToPush>
, start_scrolling:f64) { , start_scrolling:f64) {
let mut world = world.rc.borrow_mut(); let world : &mut WorldData = &mut world.rc.borrow_mut();
let workspace = &mut world.workspace; let workspace = &mut world.workspace;
let editor = workspace.text_components.first_mut().unwrap(); let editor = workspace.text_components.first_mut().unwrap();
let now = js_sys::Date::now(); let fonts = &mut world.fonts;
let now = js_sys::Date::now();
let to_type_now = typed_chars.drain_filter(|ch| ch.time <= now); let to_type_now = typed_chars.drain_filter(|ch| ch.time <= now);
for ch in to_type_now { for ch in to_type_now {
@ -82,12 +85,7 @@ fn animate_text_component
let string = ch.a_char.to_string(); let string = ch.a_char.to_string();
let change = TextChange::insert(cursor.position, string.as_str()); let change = TextChange::insert(cursor.position, string.as_str());
editor.content.make_change(change); editor.content.make_change(change);
let new_cursor_position = CharPosition { editor.navigate_cursors(Right,false,fonts);
line: editor.content.lines.len()-1,
column : editor.content.lines.last().unwrap().len(),
};
cursor.position = new_cursor_position;
editor.cursors.dirty_cursors.insert(0);
} }
if start_scrolling <= js_sys::Date::now() { if start_scrolling <= js_sys::Date::now() {
editor.scroll(Vector2::new(0.0, -0.01)); editor.scroll(Vector2::new(0.0, -0.01));

View File

@ -76,7 +76,7 @@ mod tests {
use super::WorldTest; use super::WorldTest;
use basegl::display::shape::text::Color; use basegl::display::shape::text::Color;
use basegl::display::shape::text::content::TextChange; 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::shape::text::TextComponentBuilder;
use basegl::display::world::WorldData; use basegl::display::world::WorldData;
use basegl::display::shape::text::TextComponentProperties; use basegl::display::shape::text::TextComponentProperties;
@ -185,8 +185,8 @@ mod tests {
for _ in 0..20 { for _ in 0..20 {
let workspace = &mut world.workspace; let workspace = &mut world.workspace;
let text_component = &mut workspace.text_components[0]; let text_component = &mut workspace.text_components[0];
let replace_from = CharPosition{line:1, column:2}; let replace_from = TextLocation {line:1, column:2};
let replace_to = CharPosition{line:1, column:3}; let replace_to = TextLocation {line:1, column:3};
let replaced_range = replace_from..replace_to; let replaced_range = replace_from..replace_to;
let change = TextChange::replace(replaced_range, "abc"); let change = TextChange::replace(replaced_range, "abc");
text_component.content.make_change(change); text_component.content.make_change(change);
@ -208,8 +208,8 @@ mod tests {
let world : &mut WorldData = &mut world_test.world_ptr.borrow_mut(); let world : &mut WorldData = &mut world_test.world_ptr.borrow_mut();
let workspace = &mut world.workspace; let workspace = &mut world.workspace;
let text_component = &mut workspace.text_components[0]; let text_component = &mut workspace.text_components[0];
let position = CharPosition{line:1, column:0}; let location = TextLocation {line:1, column:0};
let change = TextChange::insert(position, TEST_TEXT); let change = TextChange::insert(location,TEST_TEXT);
text_component.content.make_change(change); text_component.content.make_change(change);
world.workspace_dirty.set(); world.workspace_dirty.set();
world.update(); world.update();
@ -256,10 +256,10 @@ mod tests {
text: text.clone() text: text.clone()
}; };
let mut text_component = builder.build(); let mut text_component = builder.build();
let cursor_position_1 = CharPosition{line:0, column: 10}; let cursor_loc_1 = TextLocation {line:0, column: 10};
let cursor_position_2 = CharPosition{line:1, column: 6}; let cursor_loc_2 = TextLocation {line:1, column: 6};
text_component.cursors.add_cursor(cursor_position_1); text_component.cursors.add_cursor(cursor_loc_1);
text_component.cursors.add_cursor(cursor_position_2); text_component.cursors.add_cursor(cursor_loc_2);
workspace.text_components.push(text_component); workspace.text_components.push(text_component);
} }
world.workspace_dirty.set(); world.workspace_dirty.set();