From 74ed8b36c8b98207ca3e2265663cda87dc14fc7b Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Fri, 20 Dec 2019 16:57:52 +0100 Subject: [PATCH] Text editing (https://github.com/enso-org/ide/pull/88) Added operation for editing text in text component, which does the minimal required buffer refresh. Original commit: https://github.com/enso-org/ide/commit/2ece0ca13bfa2965c8a2227d1d0c3c14db249e7b --- gui/lib/core/src/text.rs | 133 +++---- gui/lib/core/src/text/buffer.rs | 54 ++- gui/lib/core/src/text/buffer/fragment.rs | 17 +- gui/lib/core/src/text/buffer/glyph_square.rs | 92 ++--- gui/lib/core/src/text/content.rs | 379 +++++++++++++++++++ gui/lib/core/src/text/msdf_vert.glsl | 3 +- gui/lib/core/tests/text.rs | 54 ++- 7 files changed, 583 insertions(+), 149 deletions(-) create mode 100644 gui/lib/core/src/text/content.rs diff --git a/gui/lib/core/src/text.rs b/gui/lib/core/src/text.rs index 0152d8a5503..154a6cc7c67 100644 --- a/gui/lib/core/src/text.rs +++ b/gui/lib/core/src/text.rs @@ -1,13 +1,14 @@ pub mod font; pub mod buffer; +pub mod content; pub mod msdf; use crate::prelude::*; use crate::Color; use crate::display::world::Workspace; -use crate::text::buffer::ContentRef; use crate::text::buffer::TextComponentBuffers; +use crate::text::content::TextComponentContent; use crate::text::font::FontId; use crate::text::font::FontRenderInfo; use crate::text::font::Fonts; @@ -37,15 +38,15 @@ use web_sys::WebGlTexture; /// commits #[derive(Debug)] pub struct TextComponent { - pub lines : Vec, - pub font : FontId, - pub position : Point2, - pub size : Vector2, - pub text_size : f64, - gl_context : WebGl2RenderingContext, - gl_program : Program, - gl_msdf_texture : WebGlTexture, - buffers : TextComponentBuffers, + pub content : TextComponentContent, + pub position : Point2, + pub size : Vector2, + pub text_size : f64, + gl_context : WebGl2RenderingContext, + gl_program : Program, + gl_msdf_texture : WebGlTexture, + msdf_texture_rows : usize, + buffers : TextComponentBuffers, } impl TextComponent { @@ -75,15 +76,17 @@ impl TextComponent { /// Render text pub fn display(&mut self, fonts:&mut Fonts) { + self.buffers.refresh(&self.gl_context,self.content.refresh_info(fonts)); + + if self.msdf_texture_rows != fonts.get_render_info(self.content.font).msdf_texture.rows() { + self.update_msdf_texture(fonts); + } + let gl_context = &self.gl_context; let vertices_count = self.buffers.vertices_count() as i32; - let lines = &mut self.lines; - let font = fonts.get_render_info(self.font); - let content_ref = ContentRef{lines,font}; - self.buffers.refresh(gl_context,content_ref); gl_context.use_program(Some(&self.gl_program)); - self.update_uniforms(); + self.update_uniforms(fonts); self.bind_buffer_to_attribute("position",&self.buffers.vertex_position); self.bind_buffer_to_attribute("tex_coord",&self.buffers.texture_coords); self.setup_blending(); @@ -91,13 +94,39 @@ impl TextComponent { gl_context.draw_arrays(WebGl2RenderingContext::TRIANGLES,0,vertices_count); } - fn update_uniforms(&self) { - let gl_context = &self.gl_context; - let to_scene = self.to_scene_matrix(); - let to_scene_ref = to_scene.as_ref(); - let to_scene_loc = gl_context.get_uniform_location(&self.gl_program,"to_scene"); - let transpose = false; + fn update_uniforms(&self, fonts:&mut Fonts) { + let gl_context = &self.gl_context; + let to_scene = self.to_scene_matrix(); + let to_scene_ref = to_scene.as_ref(); + let msdf_width = MsdfTexture::WIDTH as f32; + let msdf_height = fonts.get_render_info(self.content.font).msdf_texture.rows() as f32; + let to_scene_loc = gl_context.get_uniform_location(&self.gl_program,"to_scene"); + let msdf_size_loc = gl_context.get_uniform_location(&self.gl_program,"msdf_size"); + let transpose = false; gl_context.uniform_matrix3fv_with_f32_array(to_scene_loc.as_ref(),transpose,to_scene_ref); + gl_context.uniform2f(msdf_size_loc.as_ref(),msdf_width,msdf_height); + + } + + fn update_msdf_texture(&mut self, fonts:&mut Fonts) { + let gl_context = &self.gl_context; + let font_msdf = &fonts.get_render_info(self.content.font).msdf_texture; + let target = Context::TEXTURE_2D; + let width = MsdfTexture::WIDTH as i32; + let height = font_msdf.rows() as i32; + let border = 0; + let tex_level = 0; + let format = Context::RGB; + let internal_fmt = Context::RGB as i32; + let tex_type = Context::UNSIGNED_BYTE; + let data = Some(font_msdf.data.as_slice()); + + gl_context.bind_texture(target,Some(&self.gl_msdf_texture)); + let tex_image_result = + gl_context.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array + (target,tex_level,internal_fmt,width,height,border,format,tex_type,data); + tex_image_result.unwrap(); + self.msdf_texture_rows = font_msdf.rows(); } fn to_scene_matrix(&self) -> SmallVec<[f32;9]> { @@ -172,25 +201,19 @@ impl<'a,'b,Str:AsRef> TextComponentBuilder<'a,'b,Str> { /// Build a new text component rendering on given workspace pub fn build(mut self) -> TextComponent { self.load_all_chars(); - let gl_context = self.workspace.context.clone(); - let gl_program = self.create_program(&gl_context); - let gl_msdf_texture = self.create_msdf_texture(&gl_context); - let lines = self.split_lines(); - let font = self.fonts.get_render_info(self.font_id); - let display_size = self.size / self.text_size; - let content_ref = ContentRef{lines:lines.as_ref(),font}; - let buffers = TextComponentBuffers::new(&gl_context,display_size,content_ref); + let gl_context = self.workspace.context.clone(); + let gl_program = self.create_program(&gl_context); + let gl_msdf_texture = self.create_msdf_texture(&gl_context); + let display_size = self.size / self.text_size; + let mut content = TextComponentContent::new(self.font_id,self.text.as_ref()); + let initial_refresh = content.refresh_info(self.fonts); + let buffers = TextComponentBuffers::new(&gl_context,display_size,initial_refresh); self.setup_constant_uniforms(&gl_context,&gl_program); - TextComponent { - lines, - font: self.font_id, - position: self.position, - size: self.size, - text_size: self.text_size, - gl_context, - gl_program, - gl_msdf_texture, - buffers, + TextComponent {content,gl_context,gl_program,gl_msdf_texture,buffers, + position : self.position, + size : self.size, + text_size : self.text_size, + msdf_texture_rows : 0 } } @@ -200,12 +223,6 @@ impl<'a,'b,Str:AsRef> TextComponentBuilder<'a,'b,Str> { } } - fn split_lines(&self) -> Vec { - let lines_text = self.text.as_ref().split('\n'); - let lines_iter = lines_text.map(|line| line.to_string()); - lines_iter.collect() - } - fn create_program(&self, gl_context:&Context) -> Program { let vert_shader = self.create_vertex_shader(gl_context); let frag_shader = self.create_fragment_shader(gl_context); @@ -229,36 +246,14 @@ impl<'a,'b,Str:AsRef> TextComponentBuilder<'a,'b,Str> { fn create_msdf_texture(&mut self, gl_ctx:&Context) -> WebGlTexture { let msdf_texture = gl_ctx.create_texture().unwrap(); - let font_msdf = &self.fonts.get_render_info(self.font_id).msdf_texture; let target = Context::TEXTURE_2D; let wrap = Context::CLAMP_TO_EDGE as i32; let min_filter = Context::LINEAR as i32; - let width = MsdfTexture::WIDTH as i32; - let height = font_msdf.rows() as i32; - let border = 0; - let tex_level = 0; - let format = Context::RGB; - let internal_fmt = Context::RGB as i32; - let tex_type = Context::UNSIGNED_BYTE; - let data = Some(font_msdf.data.as_slice()); gl_ctx.bind_texture(target,Some(&msdf_texture)); gl_ctx.tex_parameteri(target,Context::TEXTURE_WRAP_S,wrap); gl_ctx.tex_parameteri(target,Context::TEXTURE_WRAP_T,wrap); gl_ctx.tex_parameteri(target,Context::TEXTURE_MIN_FILTER,min_filter); - let tex_image_result = - gl_ctx.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array - ( target - , tex_level - , internal_fmt - , width - , height - , border - , format - , tex_type - , data - ); - tex_image_result.unwrap(); msdf_texture } @@ -269,14 +264,11 @@ impl<'a,'b,Str:AsRef> TextComponentBuilder<'a,'b,Str> { let bottom = self.position.y as f32; let color = &self.color; let range = FontRenderInfo::MSDF_PARAMS.range as f32; - let msdf_width = MsdfTexture::WIDTH as f32; - let msdf_height = self.fonts.get_render_info(self.font_id).msdf_texture.rows() as f32; let clip_lower_loc = gl_context.get_uniform_location(gl_program,"clip_lower"); let clip_upper_loc = gl_context.get_uniform_location(gl_program,"clip_upper"); let color_loc = gl_context.get_uniform_location(gl_program,"color"); let range_loc = gl_context.get_uniform_location(gl_program,"range"); let msdf_loc = gl_context.get_uniform_location(gl_program,"msdf"); - let msdf_size_loc = gl_context.get_uniform_location(gl_program,"msdf_size"); gl_context.use_program(Some(gl_program)); gl_context.uniform2f(clip_lower_loc.as_ref(),left,bottom); @@ -284,6 +276,5 @@ impl<'a,'b,Str:AsRef> TextComponentBuilder<'a,'b,Str> { gl_context.uniform4f(color_loc.as_ref(),color.r,color.g,color.b,color.a); gl_context.uniform1f(range_loc.as_ref(),range); gl_context.uniform1i(msdf_loc.as_ref(),0); - gl_context.uniform2f(msdf_size_loc.as_ref(),msdf_width,msdf_height); } } diff --git a/gui/lib/core/src/text/buffer.rs b/gui/lib/core/src/text/buffer.rs index 3f3eb18f547..fb43b8b8156 100644 --- a/gui/lib/core/src/text/buffer.rs +++ b/gui/lib/core/src/text/buffer.rs @@ -10,6 +10,7 @@ use crate::text::buffer::glyph_square::GlyphVertexPositionBuilder; use crate::text::buffer::glyph_square::GlyphTextureCoordsBuilder; use crate::text::buffer::fragment::BufferFragments; use crate::text::buffer::fragment::FragmentsDataBuilder; +use crate::text::content::RefreshInfo; use crate::text::font::FontRenderInfo; use basegl_backend_webgl::Context; @@ -38,19 +39,13 @@ pub struct TextComponentBuffers { scroll_since_last_frame : Vector2 } -/// References to all needed stuff for generating buffer's data. -pub struct ContentRef<'a, 'b> { - pub lines : &'a[String], - pub font : &'b mut FontRenderInfo, -} - impl TextComponentBuffers { /// Create and initialize buffers. - pub fn new(gl_context:&Context, display_size:Vector2, content:ContentRef) + pub fn new(gl_context:&Context, display_size:Vector2, refresh:RefreshInfo) -> TextComponentBuffers { - let mut content_mut = content; - let mut buffers = Self::create_uninitialized(gl_context,display_size,&mut content_mut); - buffers.setup_buffers(gl_context,content_mut); + let mut ref_mut = refresh; + let mut buffers = Self::create_uninitialized(gl_context,display_size,&mut ref_mut); + buffers.setup_buffers(gl_context,ref_mut); buffers } @@ -71,23 +66,24 @@ impl TextComponentBuffers { } /// Refresh the whole buffers data. - pub fn refresh(&mut self, gl_context:&Context, content:ContentRef) { + pub fn refresh(&mut self, gl_context:&Context, info:RefreshInfo) { let scrolled_x = self.scroll_since_last_frame.x != 0.0; let scrolled_y = self.scroll_since_last_frame.y != 0.0; if scrolled_y { - let displayed_lines = self.displayed_lines(content.lines.len()); + let displayed_lines = self.displayed_lines(info.lines.len()); self.fragments.reassign_fragments(displayed_lines); } if scrolled_x { let displayed_x = self.displayed_x_range(); let x_scroll = self.scroll_since_last_frame.x; - let lines = content.lines; + let lines = info.lines; self.fragments.mark_dirty_after_x_scrolling(x_scroll,displayed_x,lines); } - if scrolled_x || scrolled_y { + if scrolled_x || scrolled_y || info.dirty_lines.any_dirty() { + self.fragments.mark_lines_dirty(&info.dirty_lines); let opt_dirty_range = self.fragments.minimum_fragments_range_with_all_dirties(); if let Some(dirty_range) = opt_dirty_range { - self.refresh_fragments(gl_context,dirty_range,content); // Note[refreshing buffers] + self.refresh_fragments(gl_context,dirty_range,info); // Note[refreshing buffers] } self.scroll_since_last_frame = Vector2::new(0.0,0.0); } @@ -100,14 +96,15 @@ impl TextComponentBuffers { * not-dirty fragments. */ - fn create_uninitialized(gl_context:&Context, display_size:Vector2, content:&mut ContentRef) + fn create_uninitialized + (gl_context:&Context, display_size:Vector2, refresh:&mut RefreshInfo) -> TextComponentBuffers { // Display_size.(x/y).floor() makes space for all lines/glyphs that fit in space in // their full size. But we have 2 more lines/glyphs: one clipped from top or left, and one // from bottom or right. const ADDITIONAL: usize = 2; let displayed_lines = display_size.y.floor() as usize + ADDITIONAL; - let space_width = content.font.get_glyph_info(' ').advance; + let space_width = refresh.font.get_glyph_info(' ').advance; let displayed_chars = (display_size.x/space_width).floor(); // This margin is to ensure, that after x scrolling we won't need to refresh all the lines // at once. @@ -144,13 +141,14 @@ impl TextComponentBuffers { is_valid.and_option_from(|| Some(index as usize)) } - fn setup_buffers(&mut self, gl_context:&Context, content:ContentRef) { - let displayed_lines = self.displayed_lines(content.lines.len()); - let all_fragments = 0..self.fragments.fragments.len(); - let mut builder = self.create_fragments_data_builder(content.font); + fn setup_buffers(&mut self, gl_context:&Context, refresh:RefreshInfo) { + let displayed_lines = self.displayed_lines(refresh.lines.len()); + let lines = refresh.lines; + let all_fragments = 0..self.fragments.fragments.len(); + let mut builder = self.create_fragments_data_builder(refresh.font); self.fragments.reassign_fragments(displayed_lines); - self.fragments.build_buffer_data_for_fragments(all_fragments,&mut builder,content.lines); + self.fragments.build_buffer_data_for_fragments(all_fragments,&mut builder,lines); let vertex_position_data = builder.vertex_position_data.as_ref(); let texture_coords_data = builder.texture_coords_data.as_ref(); self.set_buffer_data(gl_context,&self.vertex_position, vertex_position_data); @@ -158,13 +156,13 @@ impl TextComponentBuffers { } fn refresh_fragments - (&mut self, gl_context:&Context, indexes:RangeInclusive, content:ContentRef) { - let ofsset = *indexes.start(); - let mut builder = self.create_fragments_data_builder(content.font); + (&mut self, gl_context:&Context, indexes:RangeInclusive, refresh:RefreshInfo) { + let offset = *indexes.start(); + let mut builder = self.create_fragments_data_builder(refresh.font); - self.fragments.build_buffer_data_for_fragments(indexes,&mut builder,content.lines); - self.set_vertex_position_buffer_subdata(gl_context,ofsset,&builder); - self.set_texture_coords_buffer_subdata (gl_context,ofsset,&builder); + self.fragments.build_buffer_data_for_fragments(indexes,&mut builder,refresh.lines.as_ref()); + self.set_vertex_position_buffer_subdata(gl_context,offset,&builder); + self.set_texture_coords_buffer_subdata (gl_context,offset,&builder); } fn create_fragments_data_builder<'a>(&self, font:&'a mut FontRenderInfo) diff --git a/gui/lib/core/src/text/buffer/fragment.rs b/gui/lib/core/src/text/buffer/fragment.rs index 818a0ce0f18..664a796d7f4 100644 --- a/gui/lib/core/src/text/buffer/fragment.rs +++ b/gui/lib/core/src/text/buffer/fragment.rs @@ -4,6 +4,7 @@ use crate::text::buffer::glyph_square::Pen; use crate::text::buffer::glyph_square::GlyphVertexPositionBuilder; use crate::text::buffer::glyph_square::GlyphTextureCoordsBuilder; use crate::text::buffer::line::LineAttributeBuilder; +use crate::text::content::DirtyLines; use crate::text::font::FontRenderInfo; use nalgebra::geometry::Point2; @@ -121,6 +122,7 @@ impl<'a> FragmentsDataBuilder<'a> { let first_char_ref = first_char.as_ref(); let rendered_text = first_char_ref.map_or(line, |rch| &line[rch.byte_offset..]); let last_char = first_char_ref.map(|fc| self.last_rendered_char(&fc,rendered_text)); + self.build_vertex_positions(&pen,rendered_text); self.build_texture_coords(&rendered_text); match (first_char,last_char.flatten()) { @@ -167,14 +169,16 @@ impl<'a> FragmentsDataBuilder<'a> { pub fn build_vertex_positions(&mut self, pen:&Pen, text:&str) { let rendering_pen = Pen::new(pen.position); let glyph_builder = GlyphVertexPositionBuilder::new(self.font,rendering_pen); - let builder = LineAttributeBuilder::new(text,glyph_builder,self.max_chars_in_fragment); + let max_line_size = self.max_chars_in_fragment; + let builder = LineAttributeBuilder::new(text,glyph_builder,max_line_size); self.vertex_position_data.extend(builder.flatten().map(|f| f as f32)); } /// Extend texture coordinates data with a new line's. pub fn build_texture_coords(&mut self, text:&str) { let glyph_builder = GlyphTextureCoordsBuilder::new(self.font); - let builder = LineAttributeBuilder::new(text,glyph_builder,self.max_chars_in_fragment); + let max_line_size = self.max_chars_in_fragment; + let builder = LineAttributeBuilder::new(text,glyph_builder,max_line_size); self.texture_coords_data.extend(builder.flatten().map(|f| f as f32)); } } @@ -292,6 +296,14 @@ impl BufferFragments { } } + /// Mark as dirty all fragments with dirty assigned line. + pub fn mark_lines_dirty(&mut self, lines:&DirtyLines) { + let not_yet_dirty = self.fragments.iter_mut().filter(|f| !f.dirty); + for fragment in not_yet_dirty { + fragment.dirty = fragment.assigned_line.map_or(false, |l| lines.is_dirty(l)); + } + } + /// Get the minimum fragment id range covering all dirties. pub fn minimum_fragments_range_with_all_dirties(&self) -> Option> { let fragments = self.fragments.iter().enumerate(); @@ -318,6 +330,7 @@ impl BufferFragments { } } + #[cfg(test)] mod tests { use super::*; diff --git a/gui/lib/core/src/text/buffer/glyph_square.rs b/gui/lib/core/src/text/buffer/glyph_square.rs index ea9ecbb3606..0065a6ef83e 100644 --- a/gui/lib/core/src/text/buffer/glyph_square.rs +++ b/gui/lib/core/src/text/buffer/glyph_square.rs @@ -138,6 +138,9 @@ impl<'a> GlyphAttributeBuilder for GlyphVertexPositionBuilder<'a> { type Output = SmallVec<[f64;12]>; // Note[Output size] /// Compute vertices for the next glyph. + /// + /// The vertices position are the final vertices passed to webgl buffer. It takes the previous + /// built glyph into consideration for proper spacing. fn build_for_next_glyph(&mut self, ch:char) -> Self::Output { self.pen.next_char(ch, self.font); let to_pen_position = self.translation_by_pen_position(); @@ -170,50 +173,52 @@ impl<'a> GlyphTextureCoordsBuilder<'a> { GlyphTextureCoordsBuilder {font} } + /// Convert base layout to msdf space. + /// + /// The base layout contains vertices within (0.0, 0.0) - (1.0, 1.0) range. In msdf + /// space we use distances expressed in msdf cells. + pub fn base_layout_to_msdf_space() -> Affine2 { + let scale_x = MsdfTexture::WIDTH as f64; + let scale_y = MsdfTexture::ONE_GLYPH_HEIGHT as f64; + let matrix = Matrix3::new + ( scale_x, 0.0 , 0.0 + , 0.0 , scale_y, 0.0 + , 0.0 , 0.0 , 1.0 + ); + Affine2::from_matrix_unchecked(matrix) + } + /// Transformation aligning borders to MSDF cell center /// /// Each cell in MSFD contains a distance measured from its center, therefore the borders of /// glyph's square should be matched with center of MSDF cells to read distance properly. /// /// The transformation's input should be a point in _single MSDF space_, where (0.0, 0.0) is - /// the bottom-left corner of MSDF, and (1.0, 1.0) is the top-right corner. - pub fn align_borders_to_msdf_cell_center_transform(&self) -> Affine2 { + /// the bottom-left corner of MSDF, and each cell have size of 1.0. + pub fn align_borders_to_msdf_cell_center_transform() -> Affine2 { let columns = MsdfTexture::WIDTH as f64; let rows = MsdfTexture::ONE_GLYPH_HEIGHT as f64; - let column_size = 1.0 / columns; - let row_size = 1.0 / rows; - let translation_x = column_size / 2.0; - let translation_y = row_size / 2.0; - let scale_x = 1.0 - column_size; - let scale_y = 1.0 - row_size; + let translation_x = 0.5; + let translation_y = 0.5; + let scale_x = (columns - 1.0) / columns; + let scale_y = (rows - 1.0) / rows; let matrix = Matrix3::new - ( scale_x, 0.0 , translation_x - , 0.0 , scale_y, translation_y - , 0.0 , 0.0 , 1.0 - ); + ( scale_x, 0.0 , translation_x + , 0.0 , scale_y, translation_y + , 0.0 , 0.0 , 1.0 + ); Affine2::from_matrix_unchecked(matrix) } /// Transformation MSDF texture fragment associated with given glyph /// - /// The MSDF texture contains MSDFs for many glyphs. The returned transform maps the point in - /// a _single MSDF space_ to actual texture space. In other words, a (0.0, 0.0) point will be - /// mapped to bottom-left corner of `ch` texture fragment, and a (1.0, 1.0) will be mapped to - /// upper-right corner. - pub fn glyph_texture_fragment_transform(&mut self, ch:char) -> Affine2 { - let one_glyph_rows = MsdfTexture::ONE_GLYPH_HEIGHT as f64; - let all_rows = self.font.msdf_texture.rows() as f64; - - let fraction = one_glyph_rows / all_rows; + /// The MSDF texture contains MSDFs for many glyphs, so this translation moves points expressed + /// in "single" msdf space to actual texture coordinates. + pub fn glyph_texture_fragment_transform(&mut self, ch:char) -> Translation2 { let glyph_info = self.font.get_glyph_info(ch); - let offset = glyph_info.msdf_texture_rows.start as f64 / all_rows; - let matrix = nalgebra::Matrix3::new - ( 1.0, 0.0 , 0.0 - , 0.0, fraction, offset - , 0.0, 0.0 , 1.0 - ); - Affine2::from_matrix_unchecked(matrix) + let offset_y = glyph_info.msdf_texture_rows.start as f64; + Translation2::new(0.0, offset_y) } } @@ -223,13 +228,14 @@ impl<'a> GlyphAttributeBuilder for GlyphTextureCoordsBuilder<'a> { type Output = SmallVec<[f64; 12]>; // Note[Output size] - /// Compute texture coordinates for `ch` + /// Compute texture coordinates for `ch`. fn build_for_next_glyph(&mut self, ch:char) -> Self::Output { - let border_align = self.align_borders_to_msdf_cell_center_transform(); + let to_msdf = Self::base_layout_to_msdf_space(); + let border_align = Self::align_borders_to_msdf_cell_center_transform(); let to_proper_fragment = self.glyph_texture_fragment_transform(ch); let base = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.iter(); - let aligned_to_border = base .map(|p| border_align * p); + let aligned_to_border = base .map(|p| border_align * to_msdf * p); let transformed = aligned_to_border.map(|p| to_proper_fragment * p); transformed.map(point_to_iterable).flatten().collect() } @@ -330,20 +336,20 @@ mod tests { let w_texture_coords = builder.build_for_next_glyph('W'); let expected_a_coords = & - [ 1./64. , 1./128. - , 1./64. , 63./128. - , 63./64. , 1./128. - , 63./64. , 1./128. - , 1./64. , 63./128. - , 63./64. , 63./128. + [ 0.5 , 0.5 + , 0.5 , 31.5 + , 31.5 , 0.5 + , 31.5 , 0.5 + , 0.5 , 31.5 + , 31.5 , 31.5 ]; let expected_w_coords = & - [ 1./64. , 65./128. - , 1./64. , 127./128. - , 63./64. , 65./128. - , 63./64. , 65./128. - , 1./64. , 127./128. - , 63./64. , 127./128. + [ 0.5 , 32.5 + , 0.5 , 63.5 + , 31.5 , 32.5 + , 31.5 , 32.5 + , 0.5 , 63.5 + , 31.5 , 63.5 ]; assert_eq!(expected_a_coords, a_texture_coords.as_ref()); diff --git a/gui/lib/core/src/text/content.rs b/gui/lib/core/src/text/content.rs new file mode 100644 index 00000000000..3b5de610764 --- /dev/null +++ b/gui/lib/core/src/text/content.rs @@ -0,0 +1,379 @@ +use crate::prelude::*; + +use crate::text::font::FontId; +use crate::text::font::FontRenderInfo; +use crate::text::font::Fonts; + +use std::ops::Range; +use std::ops::RangeFrom; +use failure::_core::ops::RangeInclusive; + + +// ================== +// === DirtyLines === +// ================== + +/// Set of dirty lines' indices +#[derive(Debug)] +pub struct DirtyLines { + pub single_lines : HashSet, + pub range : Option> +} + +impl Default for DirtyLines { + /// Default `DirtyLines` where no line is dirty. + fn default() -> Self { + Self { + single_lines : HashSet::new(), + range : None, + } + } +} + +impl DirtyLines { + /// Mark single line as dirty. + pub fn add_single_line(&mut self, index:usize) { + self.single_lines.insert(index); + } + + /// Mark an open range of lines as dirty. + pub fn add_lines_range_from(&mut self, range:RangeFrom) { + let current_is_wider = self.range.as_ref().map_or(false, |cr| cr.start <= range.start); + if !current_is_wider { + self.range = Some(range); + } + } + + /// Mark an open range of lines as dirty. + pub fn add_lines_range(&mut self, range:RangeInclusive) { + for i in range { + self.add_single_line(i); + } + } + + /// Check if line is marked as dirty. + pub fn is_dirty(&self, index:usize) -> bool { + let range_contains = self.range.as_ref().map_or(false, |r| r.contains(&index)); + range_contains || self.single_lines.contains(&index) + } + + /// Check if there is any dirty line + pub fn any_dirty(&self) -> bool { + self.range.is_some() || !self.single_lines.is_empty() + } +} + +// ============== +// === Change === +// ============== + +/// A change type +/// +/// A change is simple if it's replace a fragment of one line with text without new lines. Otherwise +/// its a multiline change. +pub enum ChangeType { + Simple, Multiline +} + +/// A structure describing a text operation in one place. +pub struct TextChange { + replaced : Range, + lines : Vec, +} + +impl TextChange { + /// Creates operation which inserts text at given position. + pub fn insert(position:CharPosition, text:&str) -> Self { + TextChange { + replaced : position.clone()..position, + lines : TextComponentContent::split_to_lines(text) + } + } + + /// Creates operation which deletes text at given range. + pub fn delete(range:Range) -> Self { + TextChange { + replaced : range, + lines : vec!["".to_string()], + } + } + + /// Creates operation which replaces text at given range with given string. + pub fn replace(replaced:Range, text:&str) -> Self { + TextChange {replaced, + lines : TextComponentContent::split_to_lines(text) + } + } + + /// A type of this change. See `ChangeType` doc for details. + pub fn change_type(&self) -> ChangeType { + if self.lines.is_empty() { + panic!("Invalid change"); + } + let is_one_line_modified = self.replaced.start.line == self.replaced.end.line; + let is_one_line_inserted = self.lines.len() == 1; + if is_one_line_modified && is_one_line_inserted { + ChangeType::Simple + } else { + ChangeType::Multiline + } + } +} + +// ============================ +// === TextComponentContent === +// ============================ + +/// The content of text component - namely lines of text. +#[derive(Debug)] +pub struct TextComponentContent { + pub lines : Vec, + pub dirty_lines : DirtyLines, + pub font : FontId, +} + +/// A position of character in multiline text. +#[derive(Clone,PartialEq,Eq,PartialOrd,Ord)] +pub struct CharPosition { + pub line : usize, + pub byte_offset : usize, +} + +/// References to all needed stuff for generating buffer's data. +pub struct RefreshInfo<'a, 'b> { + pub lines : &'a [String], + pub dirty_lines : DirtyLines, + pub font : &'b mut FontRenderInfo, +} + +impl TextComponentContent { + /// Create a text component containing `text` + /// + /// The text will be split to lines by `'\n'` characters. + pub fn new(font_id:FontId, text:&str) -> Self { + TextComponentContent { + lines : Self::split_to_lines(text), + dirty_lines : DirtyLines::default(), + font : font_id, + } + } + + fn split_to_lines(text:&str) -> Vec { + let split = text.split('\n'); + let without_cr = split.map(Self::cut_cr_at_end_of_line); + without_cr.map(|s| s.to_string()).collect() + } + + /// Cuts carriage return (also known as CR or `'\r'`) from line's end + fn cut_cr_at_end_of_line(from:&str) -> &str { + if from.ends_with('\r') { + &from[..from.len()-1] + } else { + from + } + } + + /// Get RefreshInfo for this content. + /// + /// The dirty flags for lines are moved to returned content, so the `self` dirty flags will be + /// cleared after this call. + pub fn refresh_info<'a,'b>(&'a mut self, fonts:&'b mut Fonts) -> RefreshInfo<'a,'b> { + RefreshInfo { + lines : &mut self.lines, + dirty_lines : std::mem::take(&mut self.dirty_lines), + font : fonts.get_render_info(self.font) + } + } + + /// Apply change to content. + pub fn make_change(&mut self, change:TextChange) { + match change.change_type() { + ChangeType::Simple => self.make_simple_change(change), + ChangeType::Multiline => self.make_multiline_change(change), + } + } + + fn make_simple_change(&mut self, change:TextChange) { + let line_index = change.replaced.start.line; + let new_content = change.lines.first().unwrap(); + let range = change.replaced.start.byte_offset..change.replaced.end.byte_offset; + self.lines[line_index].replace_range(range, new_content); + self.dirty_lines.add_single_line(line_index); + } + + fn make_multiline_change(&mut self, mut change:TextChange) { + let start_line = change.replaced.start.line; + let end_line = change.replaced.end.line; + let replaced_lines_count = end_line - start_line + 1; + let inserted_lines_count = change.lines.len(); + + self.mix_content_into_change(&mut change); + self.lines.splice(start_line..=end_line, change.lines); + if replaced_lines_count != inserted_lines_count { + self.dirty_lines.add_lines_range_from(start_line..); + } else { + self.dirty_lines.add_lines_range(start_line..=end_line); + } + } + + /// Mix the unchanged parts of modified lines into change. + /// + /// This is for convenience of making multiline content changes. After mixing existing content + /// into change we can just operate on whole lines (replace the whole lines of current content + /// with the whole lines-to-insert in change description). + fn mix_content_into_change(&mut self, change:&mut TextChange) { + self.mix_first_edited_line_into_change(change); + self.mix_last_edited_line_into_change(change); + } + + fn mix_first_edited_line_into_change(&self, change:&mut TextChange) { + let first_line = change.replaced.start.line; + let replace_from = change.replaced.start.byte_offset; + let first_edited = &self.lines[first_line]; + let prefix = &first_edited[..replace_from]; + change.lines.first_mut().unwrap().insert_str(0,prefix); + } + + fn mix_last_edited_line_into_change(&mut self, change:&mut TextChange) { + let last_line = change.replaced.end.line; + let replace_to = change.replaced.end.byte_offset; + let last_edited = &self.lines[last_line]; + let suffix = &last_edited[replace_to..]; + change.lines.last_mut().unwrap().push_str(suffix); + } +} + + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn mark_single_line_as_dirty() { + let mut dirty_lines = DirtyLines::default(); + dirty_lines.add_single_line(3); + dirty_lines.add_single_line(5); + assert!( dirty_lines.is_dirty(3)); + assert!(!dirty_lines.is_dirty(4)); + assert!( dirty_lines.is_dirty(5)); + } + + #[test] + fn mark_line_range_as_dirty() { + let mut dirty_lines = DirtyLines::default(); + dirty_lines.add_lines_range(3..=5); + assert!(!dirty_lines.is_dirty(2)); + assert!( dirty_lines.is_dirty(3)); + assert!( dirty_lines.is_dirty(4)); + assert!( dirty_lines.is_dirty(5)); + assert!(!dirty_lines.is_dirty(6)); + } + + #[test] + fn mark_line_range_from_as_dirty() { + let mut dirty_lines = DirtyLines::default(); + dirty_lines.add_lines_range_from(3..); + dirty_lines.add_lines_range_from(5..); + assert!(!dirty_lines.is_dirty(2)); + assert!( dirty_lines.is_dirty(3)); + assert!( dirty_lines.is_dirty(4)); + assert!( dirty_lines.is_dirty(70000)); + } + + #[test] + fn create_content() { + let font_id = 0; + let single_line = "Single line"; + let mutliple_lines = "Multiple\nlines\n"; + + let single_line_content = TextComponentContent::new(font_id,single_line); + let multiline_content = TextComponentContent::new(font_id,mutliple_lines); + assert_eq!(1, single_line_content.lines.len()); + assert_eq!(3, multiline_content .lines.len()); + assert_eq!(single_line, single_line_content.lines[0]); + assert_eq!("Multiple" , multiline_content .lines[0]); + assert_eq!("lines" , multiline_content .lines[1]); + assert_eq!("" , multiline_content .lines[2]); + } + + #[test] + fn edit_single_line() { + let text = "Line a\nLine b\nLine c"; + let delete_from = CharPosition{line:1, byte_offset:0}; + let delete_to = CharPosition{line:1, byte_offset:4}; + let deleted_range = delete_from..delete_to; + let insert = TextChange::insert(CharPosition{line:1, byte_offset:1}, "ab"); + let delete = TextChange::delete(deleted_range.clone()); + let replace = TextChange::replace(deleted_range, "text"); + + let mut content = TextComponentContent::new(0, text); + + content.make_change(insert); + let expected = vec!["Line a", "Labine b", "Line c"]; + assert_eq!(expected, content.lines); + + content.make_change(delete); + let expected = vec!["Line a", "ne b", "Line c"]; + assert_eq!(expected, content.lines); + + content.make_change(replace); + let expected = vec!["Line a", "text", "Line c"]; + assert_eq!(expected, content.lines); + + assert!(!content.dirty_lines.is_dirty(0)); + assert!( content.dirty_lines.is_dirty(1)); + assert!(!content.dirty_lines.is_dirty(2)); + } + + #[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, byte_offset:0}; + let middle_position = CharPosition{line:1, byte_offset:2}; + let end_position = CharPosition{line:2, byte_offset: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 mut content = TextComponentContent::new(0,text); + + content.make_change(insert_at_end); + let expected = vec!["Line a", "Line b", "Line cIns a", "Ins b"]; + assert_eq!(expected, content.lines); + assert!(!content.dirty_lines.is_dirty(0)); + assert!(!content.dirty_lines.is_dirty(1)); + assert!( content.dirty_lines.is_dirty(2)); + content.dirty_lines = default(); + + content.make_change(insert_in_middle); + let expected = vec!["Line a", "LiIns a", "Ins bne b", "Line cIns a", "Ins b"]; + assert_eq!(expected, content.lines); + assert!(!content.dirty_lines.is_dirty(0)); + assert!( content.dirty_lines.is_dirty(1)); + assert!( content.dirty_lines.is_dirty(2)); + content.dirty_lines = default(); + + content.make_change(insert_at_begin); + let expected = vec!["Ins a", "Ins bLine a", "LiIns a", "Ins bne b", "Line cIns a", "Ins b"]; + assert_eq!(expected, content.lines); + assert!( content.dirty_lines.is_dirty(0)); + assert!( content.dirty_lines.is_dirty(1)); + assert!( content.dirty_lines.is_dirty(2)); + } + + #[test] + fn delete_multiple_lines() { + let text = "Line a\nLine b\nLine c"; + let delete_from = CharPosition{line:0, byte_offset:2}; + let delete_to = CharPosition{line:2, byte_offset:3}; + let deleted_range = delete_from..delete_to; + let delete = TextChange::delete(deleted_range); + + let mut content = TextComponentContent::new(0,text); + content.make_change(delete); + + let expected = vec!["Lie c"]; + assert_eq!(expected, content.lines); + } +} diff --git a/gui/lib/core/src/text/msdf_vert.glsl b/gui/lib/core/src/text/msdf_vert.glsl index 6609731d107..d99fccb058e 100644 --- a/gui/lib/core/src/text/msdf_vert.glsl +++ b/gui/lib/core/src/text/msdf_vert.glsl @@ -3,6 +3,7 @@ in vec2 position; in vec2 tex_coord; +uniform highp vec2 msdf_size; uniform highp mat3 to_scene; uniform highp vec2 clip_lower; uniform highp vec2 clip_upper; @@ -17,6 +18,6 @@ void main() { v_clip_distance.z = clip_upper.x - position_on_scene.x; v_clip_distance.w = clip_upper.y - position_on_scene.y; - v_tex_coord = tex_coord; + v_tex_coord = tex_coord / msdf_size; gl_Position = vec4(position_on_scene.xy, 0.0, position_on_scene.z); } diff --git a/gui/lib/core/tests/text.rs b/gui/lib/core/tests/text.rs index 5c1ff095734..ae365f102ab 100644 --- a/gui/lib/core/tests/text.rs +++ b/gui/lib/core/tests/text.rs @@ -82,6 +82,8 @@ mod tests { use super::WorldTest; use basegl::Color; use basegl::display::world::World; + use basegl::text::content::TextChange; + use basegl::text::content::CharPosition; use basegl::text::TextComponentBuilder; use basegl_core_msdf_sys::run_once_initialized; @@ -138,8 +140,8 @@ mod tests { } #[web_bench] - fn scrolling_vertical(bencher:&mut Bencher) { - if let Some(world_test) = WorldTest::new("scrolling_vertical") { + fn scrolling_vertical_30(bencher:&mut Bencher) { + if let Some(world_test) = WorldTest::new("scrolling_vertical_30") { let mut bencher_clone = bencher.clone(); run_once_initialized(move || { create_full_sized_text_component(&world_test,LONG_TEXT.to_string()); @@ -158,8 +160,8 @@ mod tests { } #[web_bench] - fn scrolling_horizontal(bencher:&mut Bencher) { - if let Some(world_test) = WorldTest::new("scrolling_horizontal") { + fn scrolling_horizontal_10(bencher:&mut Bencher) { + if let Some(world_test) = WorldTest::new("scrolling_horizontal_10") { let mut bencher_clone = bencher.clone(); run_once_initialized(move || { create_full_sized_text_component(&world_test,WIDE_TEXT.to_string()); @@ -177,6 +179,50 @@ mod tests { } } + #[web_bench] + fn editing_single_long_line_20(bencher:&mut Bencher) { + if let Some(world_test) = WorldTest::new("editing_single_long_line_20") { + let mut bencher_clone = bencher.clone(); + run_once_initialized(move || { + create_full_sized_text_component(&world_test,WIDE_TEXT.to_string()); + bencher_clone.iter(move || { + let world : &mut World = &mut world_test.world_ptr.borrow_mut(); + for _ in 0..20 { + let workspace = &mut world.workspaces[world_test.workspace_id]; + let text_component = &mut workspace.text_components[0]; + let replace_from = CharPosition{line:1, byte_offset:2}; + let replace_to = CharPosition{line:1, byte_offset:3}; + let replaced_range = replace_from..replace_to; + let change = TextChange::replace(replaced_range, "abc"); + text_component.content.make_change(change); + world.workspace_dirty.set(world_test.workspace_id); + world.update(); + } + }); + }); + } + } + + #[web_bench] + fn inserting_many_lines_in_long_file(bencher:&mut Bencher) { + if let Some(world_test) = WorldTest::new("inserting_many_lines_in_long_file") { + let mut bencher_clone = bencher.clone(); + run_once_initialized(move || { + create_full_sized_text_component(&world_test,LONG_TEXT.to_string()); + bencher_clone.iter(move || { + let world : &mut World = &mut world_test.world_ptr.borrow_mut(); + let workspace = &mut world.workspaces[world_test.workspace_id]; + let text_component = &mut workspace.text_components[0]; + let position = CharPosition{line:1, byte_offset:0}; + let change = TextChange::insert(position, TEST_TEXT); + text_component.content.make_change(change); + world.workspace_dirty.set(world_test.workspace_id); + world.update(); + }); + }); + } + } + fn create_full_sized_text_component(world_test:&WorldTest, text:String) { let workspace_id = world_test.workspace_id; let world : &mut World = &mut world_test.world_ptr.borrow_mut();