Added operation for editing text in text component, which does the minimal 
required buffer refresh.

Original commit: 2ece0ca13b
This commit is contained in:
Adam Obuchowicz 2019-12-20 16:57:52 +01:00 committed by GitHub
parent 99e08bbe75
commit 74ed8b36c8
7 changed files with 583 additions and 149 deletions

View File

@ -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<String>,
pub font : FontId,
pub position : Point2<f64>,
pub size : Vector2<f64>,
pub text_size : f64,
gl_context : WebGl2RenderingContext,
gl_program : Program,
gl_msdf_texture : WebGlTexture,
buffers : TextComponentBuffers,
pub content : TextComponentContent,
pub position : Point2<f64>,
pub size : Vector2<f64>,
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<str>> 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<str>> TextComponentBuilder<'a,'b,Str> {
}
}
fn split_lines(&self) -> Vec<String> {
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<str>> 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<str>> 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<str>> 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);
}
}

View File

@ -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<f64>
}
/// 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<f64>, content:ContentRef)
pub fn new(gl_context:&Context, display_size:Vector2<f64>, 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<f64>, content:&mut ContentRef)
fn create_uninitialized
(gl_context:&Context, display_size:Vector2<f64>, 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<usize>, content:ContentRef) {
let ofsset = *indexes.start();
let mut builder = self.create_fragments_data_builder(content.font);
(&mut self, gl_context:&Context, indexes:RangeInclusive<usize>, 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)

View File

@ -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<RangeInclusive<usize>> {
let fragments = self.fragments.iter().enumerate();
@ -318,6 +330,7 @@ impl BufferFragments {
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -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<f64> {
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<f64> {
/// the bottom-left corner of MSDF, and each cell have size of 1.0.
pub fn align_borders_to_msdf_cell_center_transform() -> Affine2<f64> {
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<f64> {
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<f64> {
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());

View File

@ -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<usize>,
pub range : Option<RangeFrom<usize>>
}
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<usize>) {
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<usize>) {
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<CharPosition>,
lines : Vec<String>,
}
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<CharPosition>) -> Self {
TextChange {
replaced : range,
lines : vec!["".to_string()],
}
}
/// Creates operation which replaces text at given range with given string.
pub fn replace(replaced:Range<CharPosition>, 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<String>,
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<String> {
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);
}
}

View File

@ -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);
}

View File

@ -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();