mirror of
https://github.com/enso-org/enso.git
synced 2024-12-27 23:15:01 +03:00
Implemented newlines (https://github.com/enso-org/ide/pull/67)
TextComponent will properly display text containing newline characters.
Code of the TextComponent was somewhat prepared for optimal clipping text and scrolling; each line of text have an own fixed-size buffer fragment, so the lines should be easily extended and fragments reused.
Original commit: 647430f1d8
This commit is contained in:
parent
d6df7dd156
commit
ebe9db1b74
@ -109,13 +109,10 @@ mod example_03 {
|
||||
[ "DejaVuSans"
|
||||
, "DejaVuSansMono"
|
||||
, "DejaVuSansMono-Bold"
|
||||
, "DejaVuSansMono-Oblique"
|
||||
, "DejaVuSansCondensed"
|
||||
, "DejaVuSerif"
|
||||
, "DejaVuSerifCondensed"
|
||||
];
|
||||
|
||||
const SIZES : &[f64] = &[0.016, 0.024, 0.032, 0.048, 0.064];
|
||||
const SIZES : &[f64] = &[0.024, 0.032, 0.048];
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(dead_code)]
|
||||
@ -131,21 +128,23 @@ mod example_03 {
|
||||
let fonts_iter = FONT_NAMES.iter().map(font_creator);
|
||||
let mut fonts = fonts_iter.collect::<Box<[FontRenderInfo]>>();
|
||||
|
||||
let all_cases = iproduct!(0..fonts.len(), SIZES.iter());
|
||||
let all_cases = iproduct!(0..fonts.len(), 0..SIZES.len());
|
||||
|
||||
for (i, (font, size)) in all_cases.enumerate() {
|
||||
for (font, size) in all_cases {
|
||||
|
||||
let line_position = nalgebra::Vector2::new(-0.95, 0.9 - 0.064*(i as f64));
|
||||
let x = -0.95 + 0.6 * (size as f64);
|
||||
let y = 0.90 - 0.45 * (font as f64);
|
||||
let line_position = nalgebra::Vector2::new(x,y);
|
||||
let text_compnent = crate::text::TextComponentBuilder {
|
||||
text : "To be, or not to be, that is the question: \
|
||||
Whether 'tis nobler in the mind to suffer \
|
||||
The slings and arrows of outrageous fortune, \
|
||||
Or to take arms against a sea of troubles \
|
||||
text : "To be, or not to be, that is the question:\n\
|
||||
Whether 'tis nobler in the mind to suffer\n\
|
||||
The slings and arrows of outrageous fortune,\n\
|
||||
Or to take arms against a sea of troubles\n\
|
||||
And by opposing end them."
|
||||
.to_string(),
|
||||
font : &mut fonts[font],
|
||||
position : line_position,
|
||||
size : *size,
|
||||
size : SIZES[size],
|
||||
color : Color {r: 1.0, g: 1.0, b: 1.0, a: 1.0},
|
||||
}.build(workspace);
|
||||
workspace.text_components.push(text_compnent);
|
||||
|
@ -1,65 +1,151 @@
|
||||
pub mod font;
|
||||
pub mod glyph_render;
|
||||
pub mod buffer;
|
||||
pub mod msdf;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::Color;
|
||||
use crate::display::world::Workspace;
|
||||
use crate::text::glyph_render::{GylphSquareVerticesBuilder, GlyphSquareTextureCoordinatesBuilder};
|
||||
use crate::text::buffer::TextComponentBuffers;
|
||||
use crate::text::msdf::MsdfTexture;
|
||||
|
||||
use font::FontRenderInfo;
|
||||
use basegl_backend_webgl::{Context,compile_shader,link_program,Program,Shader};
|
||||
use js_sys::Float32Array;
|
||||
use nalgebra::{Vector2,Similarity2,Transform2};
|
||||
use web_sys::{WebGlRenderingContext,WebGlBuffer,WebGlTexture};
|
||||
|
||||
pub struct TextComponentBuilder<'a> {
|
||||
pub text : String,
|
||||
|
||||
// =====================
|
||||
// === TextComponent ===
|
||||
// =====================
|
||||
|
||||
/// Component rendering text
|
||||
///
|
||||
/// This component is under heavy construction, so the api may easily changed in few future
|
||||
/// commits
|
||||
#[derive(Debug)]
|
||||
pub struct TextComponent {
|
||||
gl_context : WebGlRenderingContext,
|
||||
gl_program : Program,
|
||||
gl_msdf_texture : WebGlTexture,
|
||||
lines : Vec<Line>,
|
||||
buffers : TextComponentBuffers,
|
||||
to_window : Transform2<f64>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Line {
|
||||
pub content : String,
|
||||
}
|
||||
|
||||
impl TextComponent {
|
||||
|
||||
/// Render text
|
||||
pub fn display(&self) {
|
||||
let gl_context = &self.gl_context;
|
||||
let vertices_count = self.buffers.vertices_count() as i32;
|
||||
|
||||
gl_context.use_program(Some(&self.gl_program));
|
||||
self.bind_buffer_to_attribute("position",&self.buffers.vertex_position);
|
||||
self.bind_buffer_to_attribute("texCoord",&self.buffers.texture_coords);
|
||||
self.setup_blending();
|
||||
gl_context.bind_texture(Context::TEXTURE_2D, Some(&self.gl_msdf_texture));
|
||||
gl_context.draw_arrays(WebGlRenderingContext::TRIANGLES,0,vertices_count);
|
||||
}
|
||||
|
||||
fn bind_buffer_to_attribute(&self, attribute_name:&str, buffer:&WebGlBuffer) {
|
||||
let gl_context = &self.gl_context;
|
||||
let gl_program = &self.gl_program;
|
||||
let location = gl_context.get_attrib_location(gl_program,attribute_name) as u32;
|
||||
let target = WebGlRenderingContext::ARRAY_BUFFER;
|
||||
let item_size = 2;
|
||||
let item_type = WebGlRenderingContext::FLOAT;
|
||||
let normalized = false;
|
||||
let stride = 0;
|
||||
let offset = 0;
|
||||
|
||||
gl_context.enable_vertex_attrib_array(location);
|
||||
gl_context.bind_buffer(target,Some(buffer));
|
||||
gl_context.vertex_attrib_pointer_with_i32
|
||||
( location
|
||||
, item_size
|
||||
, item_type
|
||||
, normalized
|
||||
, stride
|
||||
, offset
|
||||
);
|
||||
}
|
||||
|
||||
fn setup_blending(&self) {
|
||||
let gl_context = &self.gl_context;
|
||||
let rgb_source = Context::SRC_ALPHA;
|
||||
let alpha_source = Context::ZERO;
|
||||
let rgb_destination = Context::ONE_MINUS_SRC_ALPHA;
|
||||
let alhpa_destination = Context::ONE;
|
||||
|
||||
gl_context.enable(Context::BLEND);
|
||||
gl_context.blend_func_separate(rgb_source,rgb_destination,alpha_source,alhpa_destination);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================
|
||||
// === TextComponentBuilder ===
|
||||
// ============================
|
||||
|
||||
/// Text component builder
|
||||
pub struct TextComponentBuilder<'a, Str:AsRef<str>> {
|
||||
pub text : Str,
|
||||
pub font : &'a mut FontRenderInfo,
|
||||
pub position : Vector2<f64>,
|
||||
pub size : f64,
|
||||
pub color : Color<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextComponent {
|
||||
gl_context : WebGlRenderingContext,
|
||||
gl_program : Program,
|
||||
gl_vertex_buffer : WebGlBuffer,
|
||||
gl_texture_coordinates_buffer : WebGlBuffer,
|
||||
gl_msdf_texture : WebGlTexture,
|
||||
buffers_size : usize,
|
||||
}
|
||||
impl<'a,Str:AsRef<str>> TextComponentBuilder<'a,Str> {
|
||||
|
||||
impl<'a> TextComponentBuilder<'a> {
|
||||
pub fn build(mut self, workspace : &Workspace) -> TextComponent {
|
||||
/// Build a new text component rendering on given workspace
|
||||
pub fn build(mut self, workspace : &Workspace) -> TextComponent {
|
||||
self.load_all_chars();
|
||||
let gl_context = workspace.context.clone();
|
||||
let gl_program = self.create_program(&gl_context);
|
||||
let gl_vertex_buffer = self.create_vertex_bufffer(&gl_context);
|
||||
let gl_tex_coord_buffer = self.create_texture_coordinates_buffer(&gl_context);
|
||||
let gl_msdf_texture = self.create_msdf_texture(&gl_context);
|
||||
let glyph_vertices_count = glyph_render::GLYPH_SQUARE_VERTICES_BASE_LAYOUT.len();
|
||||
let buffers_size = self.text.len() * glyph_vertices_count;
|
||||
let displayed_lines = 100; // TODO[AO] replace with actual component size
|
||||
let displayed_columns = 100;
|
||||
let gl_context = 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 mut buffers = TextComponentBuffers::new(&gl_context,displayed_lines,displayed_columns);
|
||||
let to_window = self.to_window_transform();
|
||||
|
||||
self.setup_uniforms(&gl_context, &gl_program);
|
||||
buffers.refresh_all(&gl_context, &lines, self.font, &to_window);
|
||||
TextComponent {
|
||||
gl_context,
|
||||
gl_program,
|
||||
gl_vertex_buffer,
|
||||
gl_texture_coordinates_buffer : gl_tex_coord_buffer,
|
||||
gl_msdf_texture,
|
||||
buffers_size
|
||||
buffers,
|
||||
lines,
|
||||
to_window,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_all_chars(&mut self) {
|
||||
for ch in self.text.chars() {
|
||||
for ch in self.text.as_ref().chars() {
|
||||
self.font.get_glyph_info(ch);
|
||||
}
|
||||
}
|
||||
|
||||
fn split_lines(&self) -> Vec<Line> {
|
||||
let lines_text = self.text.as_ref().split('\n');
|
||||
let lines_iter = lines_text.map(Self::initialize_line);
|
||||
lines_iter.collect()
|
||||
}
|
||||
|
||||
fn initialize_line(text:&str) -> Line {
|
||||
Line {
|
||||
content : text.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_program(&self, gl_context:&Context) -> Program {
|
||||
gl_context.get_extension("OES_standard_derivatives").unwrap().unwrap();
|
||||
let vert_shader = self.create_vertex_shader(gl_context);
|
||||
@ -81,61 +167,12 @@ impl<'a> TextComponentBuilder<'a> {
|
||||
compile_shader(gl_context,shader_type,body).unwrap()
|
||||
}
|
||||
|
||||
fn create_buffer(gl_context:&Context, vertices:&[f32]) -> WebGlBuffer {
|
||||
let target = WebGlRenderingContext::ARRAY_BUFFER;
|
||||
|
||||
let buffer = gl_context.create_buffer().unwrap();
|
||||
gl_context.bind_buffer(target,Some(&buffer));
|
||||
Self::set_bound_buffer_data(gl_context,target,vertices);
|
||||
buffer
|
||||
}
|
||||
|
||||
fn set_bound_buffer_data(gl_context:&Context, target:u32, data:&[f32]) {
|
||||
let usage = WebGlRenderingContext::STATIC_DRAW;
|
||||
unsafe { // Note [unsafe buffer_data]
|
||||
let float_32_array = Float32Array::view(&data);
|
||||
gl_context.buffer_data_with_array_buffer_view(target,&float_32_array,usage);
|
||||
}
|
||||
}
|
||||
|
||||
/* Note [unsafe buffer_data]
|
||||
*
|
||||
* The Float32Array::view is safe as long there are no allocations done
|
||||
* until it is destroyed. This way of creating buffers were taken from
|
||||
* wasm-bindgen examples
|
||||
* (https://rustwasm.github.io/wasm-bindgen/examples/webgl.html)
|
||||
*/
|
||||
|
||||
fn create_vertex_bufffer(&mut self, gl_context:&Context) -> WebGlBuffer {
|
||||
let to_window = self.to_window_transform();
|
||||
let font = &mut self.font;
|
||||
|
||||
let mut vertices_builder = GylphSquareVerticesBuilder::new(font,to_window);
|
||||
let char_to_vertices = |ch| vertices_builder.build_for_next_glyph(ch);
|
||||
let grouped_vertices = self.text.chars().map(char_to_vertices);
|
||||
let vertices = grouped_vertices.flatten();
|
||||
let buffer_data = vertices.map(|f| f as f32).collect::<SmallVec<[f32;32]>>();
|
||||
Self::create_buffer(gl_context,buffer_data.as_ref())
|
||||
}
|
||||
|
||||
fn to_window_transform(&self) -> Transform2<f64> {
|
||||
const ROTATION : f64 = 0.0;
|
||||
let similarity = Similarity2::new(self.position,ROTATION,self.size);
|
||||
nalgebra::convert(similarity)
|
||||
}
|
||||
|
||||
fn create_texture_coordinates_buffer(&mut self, gl_context:&Context) -> WebGlBuffer {
|
||||
let font = &mut self.font;
|
||||
|
||||
let mut texture_coordinates_builder = GlyphSquareTextureCoordinatesBuilder::new(font);
|
||||
let char_to_texture_coordinates = |ch| texture_coordinates_builder.build_for_next_glyph(ch);
|
||||
let grouped_texture_coordinates = self.text.chars().map(char_to_texture_coordinates);
|
||||
let texture_coordinates = grouped_texture_coordinates.flatten();
|
||||
let converted_data = texture_coordinates.map(|f| f as f32);
|
||||
let buffer_data = converted_data.collect::<SmallVec<[f32;32]>>();
|
||||
Self::create_buffer(gl_context,buffer_data.as_ref())
|
||||
}
|
||||
|
||||
fn create_msdf_texture(&self, gl_ctx:&Context)
|
||||
-> WebGlTexture {
|
||||
let msdf_texture = gl_ctx.create_texture().unwrap();
|
||||
@ -188,51 +225,3 @@ impl<'a> TextComponentBuilder<'a> {
|
||||
gl_context.uniform2f(msdf_size_loc.as_ref(),msdf_width,msdf_height);
|
||||
}
|
||||
}
|
||||
|
||||
impl TextComponent {
|
||||
|
||||
pub fn display(&self) {
|
||||
let gl_context = &self.gl_context;
|
||||
|
||||
gl_context.use_program(Some(&self.gl_program));
|
||||
self.bind_buffer_to_attribute("position",&self.gl_vertex_buffer);
|
||||
self.bind_buffer_to_attribute("texCoord",&self.gl_texture_coordinates_buffer);
|
||||
self.setup_blending();
|
||||
gl_context.bind_texture(Context::TEXTURE_2D, Some(&self.gl_msdf_texture));
|
||||
gl_context.draw_arrays(WebGlRenderingContext::TRIANGLES,0,self.buffers_size as i32);
|
||||
}
|
||||
|
||||
fn bind_buffer_to_attribute(&self, attribute_name:&str, buffer:&WebGlBuffer) {
|
||||
let gl_context = &self.gl_context;
|
||||
let gl_program = &self.gl_program;
|
||||
let location = gl_context.get_attrib_location(gl_program,attribute_name) as u32;
|
||||
let target = WebGlRenderingContext::ARRAY_BUFFER;
|
||||
let item_size = 2;
|
||||
let item_type = WebGlRenderingContext::FLOAT;
|
||||
let normalized = false;
|
||||
let stride = 0;
|
||||
let offset = 0;
|
||||
|
||||
gl_context.enable_vertex_attrib_array(location);
|
||||
gl_context.bind_buffer(target,Some(buffer));
|
||||
gl_context.vertex_attrib_pointer_with_i32
|
||||
( location
|
||||
, item_size
|
||||
, item_type
|
||||
, normalized
|
||||
, stride
|
||||
, offset
|
||||
);
|
||||
}
|
||||
|
||||
fn setup_blending(&self) {
|
||||
let gl_context = &self.gl_context;
|
||||
let rgb_source = Context::SRC_ALPHA;
|
||||
let alpha_source = Context::ZERO;
|
||||
let rgb_destination = Context::ONE_MINUS_SRC_ALPHA;
|
||||
let alhpa_destination = Context::ONE;
|
||||
|
||||
gl_context.enable(Context::BLEND);
|
||||
gl_context.blend_func_separate(rgb_source,rgb_destination,alpha_source,alhpa_destination);
|
||||
}
|
||||
}
|
||||
|
159
gui/lib/core/src/text/buffer.rs
Normal file
159
gui/lib/core/src/text/buffer.rs
Normal file
@ -0,0 +1,159 @@
|
||||
pub mod glyph_square;
|
||||
pub mod line;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::text::buffer::glyph_square::
|
||||
{GlyphVertexPositionBuilder, GlyphTextureCoordsBuilder, GlyphAttributeBuilder, BASE_LAYOUT_SIZE};
|
||||
use crate::text::buffer::line::LineAttributeBuilder;
|
||||
use crate::text::Line;
|
||||
use crate::text::font::FontRenderInfo;
|
||||
|
||||
use basegl_backend_webgl::Context;
|
||||
use js_sys::Float32Array;
|
||||
use nalgebra::Transform2;
|
||||
use web_sys::WebGlBuffer;
|
||||
|
||||
|
||||
// =============================
|
||||
// === TextComponentsBuffers ===
|
||||
// =============================
|
||||
|
||||
/// A structure managing all WebGl buffers used by TextComponent
|
||||
///
|
||||
/// Each attribute buffer is split to equal-sized fragments, and each fragment may is assigned to
|
||||
/// displayed line The fragment keeps the data for this line.
|
||||
#[derive(Debug)]
|
||||
pub struct TextComponentBuffers {
|
||||
pub vertex_position : WebGlBuffer,
|
||||
pub texture_coords: WebGlBuffer,
|
||||
fragments : Vec<BufferFragment>,
|
||||
pub displayed_lines : usize,
|
||||
pub displayed_columns : usize,
|
||||
}
|
||||
|
||||
/// A buffer fragment which may be assigned to some line.
|
||||
#[derive(Debug)]
|
||||
struct BufferFragment {
|
||||
assigned_line : Option<usize>
|
||||
// TODO [AO] maybe some new fields appear in future. if not, remove this and use Option<usize>
|
||||
// directly
|
||||
}
|
||||
|
||||
impl TextComponentBuffers {
|
||||
/// Create and initialize buffers.
|
||||
pub fn new(gl_context:&Context, displayed_lines:usize, displayed_columns:usize)
|
||||
-> TextComponentBuffers {
|
||||
TextComponentBuffers {
|
||||
vertex_position : gl_context.create_buffer().unwrap(),
|
||||
texture_coords : gl_context.create_buffer().unwrap(),
|
||||
fragments : Self::build_fragments(displayed_lines),
|
||||
displayed_lines,
|
||||
displayed_columns,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_fragments(displayed_lines:usize) -> Vec<BufferFragment> {
|
||||
let indexes = 0..displayed_lines;
|
||||
let fragments = indexes.map(|_| BufferFragment { assigned_line : None });
|
||||
fragments.collect()
|
||||
}
|
||||
|
||||
/// Refresh the whole buffers making them display the given lines.
|
||||
pub fn refresh_all<'a>
|
||||
( &mut self
|
||||
, gl_context:&Context
|
||||
, lines:&[Line]
|
||||
, font:&'a mut FontRenderInfo
|
||||
, to_window:&Transform2<f64>
|
||||
) {
|
||||
self.assign_fragments(lines.len());
|
||||
|
||||
let content_from_index = |index : Option<usize>| index.map_or("", |i| lines[i].content.as_str());
|
||||
|
||||
let assigned_lines = self.fragments.iter().map(|fragment| fragment.assigned_line);
|
||||
let content = assigned_lines.clone().map(content_from_index);
|
||||
let line_indexes = assigned_lines.map(|index| index.unwrap_or(0));
|
||||
let content_with_index = content.clone().zip(line_indexes);
|
||||
|
||||
self.refresh_vertex_position_buffer(gl_context,content_with_index,font,to_window);
|
||||
self.refresh_texture_coords_buffer (gl_context,content ,font);
|
||||
}
|
||||
|
||||
fn refresh_vertex_position_buffer<'a, Iter>
|
||||
( &self
|
||||
, gl_context:&Context
|
||||
, content_with_index:Iter
|
||||
, font:&mut FontRenderInfo
|
||||
, to_window:&Transform2<f64>
|
||||
) where Iter : ExactSizeIterator<Item=(&'a str,usize)> {
|
||||
let one_glyph_data_size = GlyphVertexPositionBuilder::OUTPUT_SIZE;
|
||||
let mut data = Vec::new();
|
||||
let lines_count = content_with_index.len();
|
||||
let line_length = self.displayed_columns;
|
||||
let data_size = one_glyph_data_size * line_length * lines_count;
|
||||
|
||||
data.reserve(data_size);
|
||||
for (content, index) in content_with_index {
|
||||
let glyph_buider = GlyphVertexPositionBuilder::new(font,*to_window,index);
|
||||
let builder = LineAttributeBuilder::new(content,glyph_buider,line_length);
|
||||
data.extend(builder.flatten().map(|f| f as f32));
|
||||
}
|
||||
|
||||
self.set_buffer_data(gl_context,&self.vertex_position,data.as_ref());
|
||||
}
|
||||
|
||||
fn refresh_texture_coords_buffer<'a, Iter>
|
||||
(&self, gl_context:&Context, content:Iter, font:&mut FontRenderInfo)
|
||||
where Iter : ExactSizeIterator<Item=&'a str>
|
||||
{
|
||||
let one_glyph_data_size = GlyphTextureCoordsBuilder::OUTPUT_SIZE;
|
||||
let mut data = Vec::new();
|
||||
let lines_count = content.len();
|
||||
let line_length = self.displayed_columns;
|
||||
let data_size = one_glyph_data_size * line_length * lines_count;
|
||||
|
||||
data.reserve(data_size);
|
||||
for line in content {
|
||||
let glyph_buider = GlyphTextureCoordsBuilder::new(font);
|
||||
let builder = LineAttributeBuilder::new(line,glyph_buider,line_length);
|
||||
data.extend(builder.flatten().map(|f| f as f32));
|
||||
}
|
||||
|
||||
self.set_buffer_data(gl_context,&self.texture_coords,data.as_ref());
|
||||
}
|
||||
|
||||
fn assign_fragments(&mut self, lines_count:usize) {
|
||||
for (i, fragment) in self.fragments.iter_mut().enumerate() {
|
||||
fragment.assigned_line = (i < lines_count).and_option(Some(i))
|
||||
}
|
||||
}
|
||||
|
||||
fn set_buffer_data(&self, gl_context:&Context, buffer:&WebGlBuffer, vertices:&[f32]) {
|
||||
let target = Context::ARRAY_BUFFER;
|
||||
|
||||
gl_context.bind_buffer(target,Some(&buffer));
|
||||
Self::set_bound_buffer_data(gl_context,target,vertices);
|
||||
}
|
||||
|
||||
fn set_bound_buffer_data(gl_context:&Context, target:u32, data:&[f32]) {
|
||||
let usage = Context::STATIC_DRAW;
|
||||
|
||||
unsafe { // Note [unsafe buffer_data]
|
||||
let float_32_array = Float32Array::view(&data);
|
||||
gl_context.buffer_data_with_array_buffer_view(target,&float_32_array,usage);
|
||||
}
|
||||
}
|
||||
|
||||
/* Note [unsafe buffer_data]
|
||||
*
|
||||
* The Float32Array::view is safe as long there are no allocations done
|
||||
* until it is destroyed. This way of creating buffers were taken from
|
||||
* wasm-bindgen examples
|
||||
* (https://rustwasm.github.io/wasm-bindgen/examples/webgl.html)
|
||||
*/
|
||||
|
||||
pub fn vertices_count(&self) -> usize {
|
||||
BASE_LAYOUT_SIZE * self.displayed_lines * self.displayed_columns
|
||||
}
|
||||
}
|
@ -9,73 +9,74 @@ use nalgebra::{Point2,Transform2,Translation2,Affine2,Matrix3,Scalar};
|
||||
// === Base vertices layout ===
|
||||
// ============================
|
||||
|
||||
pub const GLYPH_SQUARE_VERTICES_BASE_LAYOUT: &[(f64, f64)] = &
|
||||
[ (0.0, 0.0)
|
||||
, (0.0, 1.0)
|
||||
, (1.0, 0.0)
|
||||
, (1.0, 0.0)
|
||||
, (0.0, 1.0)
|
||||
, (1.0, 1.0)
|
||||
];
|
||||
pub const BASE_LAYOUT_SIZE : usize = 6;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref GLYPH_SQUARE_VERTICES_BASE_LAYOUT : [Point2<f64>;BASE_LAYOUT_SIZE] =
|
||||
[ Point2::new(0.0, 0.0)
|
||||
, Point2::new(0.0, 1.0)
|
||||
, Point2::new(1.0, 0.0)
|
||||
, Point2::new(1.0, 0.0)
|
||||
, Point2::new(0.0, 1.0)
|
||||
, Point2::new(1.0, 1.0)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
fn point_to_iterable<T:Scalar>(p:Point2<T>) -> SmallVec<[T;2]> {
|
||||
p.iter().cloned().collect()
|
||||
}
|
||||
|
||||
// ==================================
|
||||
// === GylphSquareVerticesBuilder ===
|
||||
// ==================================
|
||||
// =========================================
|
||||
// === GlyphSquareVertexAttributeBuilder ===
|
||||
// =========================================
|
||||
|
||||
const GLYPH_SQUARE_VERTICES_SIZE : usize = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.len() * 2;
|
||||
|
||||
pub type GlyphSquareVertices = SmallVec<[f64;GLYPH_SQUARE_VERTICES_SIZE]>;
|
||||
|
||||
/// Builder for glyph vertices
|
||||
/// Builder for specific attribute of glyph square's vertices
|
||||
///
|
||||
/// Once created, each `build_for_next_glyph` gives vertices of the next glyph's square
|
||||
pub struct GylphSquareVerticesBuilder<'a> {
|
||||
/// Builder is meant to be used for producing attribute data for squares of glyphs placed in one
|
||||
/// line of text. The attribute may be vertices position, texture coordinates, etc.
|
||||
pub trait GlyphAttributeBuilder {
|
||||
const OUTPUT_SIZE : usize;
|
||||
type Output;
|
||||
|
||||
/// Build attribute data for next glyph in line.
|
||||
fn build_for_next_glyph(&mut self, ch:char) -> Self::Output;
|
||||
|
||||
/// Create empty attribute data
|
||||
///
|
||||
/// The empty data are used for squares that are not actually rendered, but instead reserved
|
||||
/// for future use (due to optimisation).
|
||||
fn empty() -> Self::Output;
|
||||
}
|
||||
|
||||
|
||||
// ==================================
|
||||
// === GlyphVertexPositionBuilder ===
|
||||
// ==================================
|
||||
|
||||
/// Builder for glyph square vertex positions
|
||||
pub struct GlyphVertexPositionBuilder<'a> {
|
||||
pub previous_char : Option<char>,
|
||||
pub font : &'a mut FontRenderInfo,
|
||||
pub pen_position : Point2<f64>,
|
||||
pub to_window : Transform2<f64>,
|
||||
}
|
||||
|
||||
impl<'a> GylphSquareVerticesBuilder<'a> {
|
||||
|
||||
/// New GylphSquareVerticesBuilder
|
||||
impl<'a> GlyphVertexPositionBuilder<'a> {
|
||||
/// New GlyphVertexPositionBuilder
|
||||
///
|
||||
/// The newly created builder start to place glyphs with pen located at `to_window` * (0.0, 0.0)
|
||||
pub fn new(font:&'a mut FontRenderInfo, to_window:Transform2<f64>)
|
||||
-> GylphSquareVerticesBuilder<'a> {
|
||||
GylphSquareVerticesBuilder {
|
||||
/// The newly created builder start to place glyphs with pen located at the beginning of given
|
||||
/// line.
|
||||
pub fn new(font:&'a mut FontRenderInfo, to_window:Transform2<f64>, line_number:usize)
|
||||
-> GlyphVertexPositionBuilder<'a> {
|
||||
GlyphVertexPositionBuilder {
|
||||
previous_char : None,
|
||||
font,
|
||||
pen_position : Point2::new(0.0, 0.0),
|
||||
to_window
|
||||
pen_position : Point2::new(0.0, -1.0*line_number as f64),
|
||||
to_window,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute vertices for one glyph and move pen for next position
|
||||
pub fn build_for_next_glyph(&mut self, ch:char) -> GlyphSquareVertices {
|
||||
let apply_kerning = self.translation_by_kerning_value(ch);
|
||||
let to_pen_position = self.translation_by_pen_position();
|
||||
let glyph_info = self.font.get_glyph_info(ch);
|
||||
let glyph_specific_transform = &glyph_info.from_base_layout;
|
||||
let to_window = &self.to_window;
|
||||
let advance_pen = Translation2::new(glyph_info.advance, 0.0);
|
||||
let pen_transformation = apply_kerning * advance_pen;
|
||||
|
||||
self.pen_position = pen_transformation * self.pen_position;
|
||||
self.previous_char = Some(ch);
|
||||
let plain_base = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.iter();
|
||||
let base = plain_base .map(|(x, y)| Point2::new(*x, *y));
|
||||
let glyph_fixed = base .map(|p| glyph_specific_transform * p);
|
||||
let moved_to_pen_position = glyph_fixed .map(|p| to_pen_position * p);
|
||||
let kerning_applied = moved_to_pen_position.map(|p| apply_kerning * p);
|
||||
let mapped_to_window = kerning_applied .map(|p| to_window * p);
|
||||
mapped_to_window.map(point_to_iterable).flatten().collect()
|
||||
}
|
||||
|
||||
fn translation_by_kerning_value(&mut self, ch:char) -> Translation2<f64> {
|
||||
let prev_char = self.previous_char;
|
||||
let opt_value = prev_char.map(|lc| self.font.get_kerning(lc, ch));
|
||||
@ -88,41 +89,55 @@ impl<'a> GylphSquareVerticesBuilder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// =================================
|
||||
// === TextureCoordinatesBuilder ===
|
||||
// =================================
|
||||
impl<'a> GlyphAttributeBuilder for GlyphVertexPositionBuilder<'a> {
|
||||
const OUTPUT_SIZE : usize = BASE_LAYOUT_SIZE * 2;
|
||||
type Output = SmallVec<[f64;12]>; // Note[Output size]
|
||||
|
||||
const GLYPH_SQUARE_TEXTURE_COORDINATES_SIZE : usize = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.len() * 2;
|
||||
/// Compute vertices for one glyph and move pen for next position.
|
||||
fn build_for_next_glyph(&mut self, ch:char) -> Self::Output {
|
||||
let apply_kerning = self.translation_by_kerning_value(ch);
|
||||
let to_pen_position = self.translation_by_pen_position();
|
||||
let glyph_info = self.font.get_glyph_info(ch);
|
||||
let glyph_specific_transform = &glyph_info.from_base_layout;
|
||||
let to_window = &self.to_window;
|
||||
let advance_pen = Translation2::new(glyph_info.advance, 0.0);
|
||||
let pen_transformation = apply_kerning * advance_pen;
|
||||
|
||||
pub type GlyphTextureCoordinates = SmallVec<[f64;GLYPH_SQUARE_TEXTURE_COORDINATES_SIZE]>;
|
||||
self.pen_position = pen_transformation * self.pen_position;
|
||||
self.previous_char = Some(ch);
|
||||
let base = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.iter();
|
||||
let glyph_fixed = base .map(|p| glyph_specific_transform * p);
|
||||
let moved_to_pen_position = glyph_fixed .map(|p| to_pen_position * p);
|
||||
let kerning_applied = moved_to_pen_position.map(|p| apply_kerning * p);
|
||||
let mapped_to_window = kerning_applied .map(|p| to_window * p);
|
||||
mapped_to_window.map(point_to_iterable).flatten().collect()
|
||||
}
|
||||
|
||||
fn empty() -> Self::Output {
|
||||
SmallVec::from_buf([0.0;12]) // Note[Output size]
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// === GlyphTextureCoordinatesBuilder ===
|
||||
// ======================================
|
||||
|
||||
/// Builder for glyph MSDF texture coordinates
|
||||
pub struct GlyphSquareTextureCoordinatesBuilder<'a> {
|
||||
pub struct GlyphTextureCoordsBuilder<'a> {
|
||||
pub font : &'a mut FontRenderInfo
|
||||
}
|
||||
|
||||
impl<'a> GlyphSquareTextureCoordinatesBuilder<'a> {
|
||||
/// Create new builder using given font
|
||||
pub fn new(font:&'a mut FontRenderInfo) -> GlyphSquareTextureCoordinatesBuilder<'a> {
|
||||
GlyphSquareTextureCoordinatesBuilder {font}
|
||||
}
|
||||
impl<'a> GlyphTextureCoordsBuilder<'a> {
|
||||
|
||||
/// Compute texture coordinates for `ch`
|
||||
pub fn build_for_next_glyph(&mut self, ch:char) -> GlyphTextureCoordinates {
|
||||
let border_align = self.align_borders_to_msdf_cell_center_transform();
|
||||
let to_proper_fragment = self.glyph_texture_fragment_transform(ch);
|
||||
|
||||
let plain_base = GLYPH_SQUARE_VERTICES_BASE_LAYOUT.iter();
|
||||
let base = plain_base .map(|(x,y)| Point2::new(*x, *y));
|
||||
let aligned_to_border = base .map(|p| border_align * p);
|
||||
let transformed = aligned_to_border.map(|p| to_proper_fragment * p);
|
||||
transformed.map(point_to_iterable).flatten().collect()
|
||||
/// Create new builder using given font.
|
||||
pub fn new(font:&'a mut FontRenderInfo) -> GlyphTextureCoordsBuilder<'a> {
|
||||
GlyphTextureCoordsBuilder {font}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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.
|
||||
@ -166,6 +181,33 @@ impl<'a> GlyphSquareTextureCoordinatesBuilder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GlyphAttributeBuilder for GlyphTextureCoordsBuilder<'a> {
|
||||
|
||||
const OUTPUT_SIZE : usize = BASE_LAYOUT_SIZE * 2;
|
||||
|
||||
type Output = SmallVec<[f64; 12]>; // Note[Output size]
|
||||
|
||||
/// 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_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 transformed = aligned_to_border.map(|p| to_proper_fragment * p);
|
||||
transformed.map(point_to_iterable).flatten().collect()
|
||||
}
|
||||
|
||||
fn empty() -> Self::Output {
|
||||
SmallVec::from_buf([0.0;12]) // Note[Output size]
|
||||
}
|
||||
}
|
||||
|
||||
/* Note [Output size]
|
||||
*
|
||||
* We can use `Self::OUTPUT_SIZE` instead of current hardcode 12 once the rustc bug will be fixed:
|
||||
* https://github.com/rust-lang/rust/issues/62708
|
||||
*/
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@ -193,7 +235,7 @@ mod tests {
|
||||
nalgebra::Transform2::from_matrix_unchecked(to_window_mtx)
|
||||
};
|
||||
|
||||
let mut builder = GylphSquareVerticesBuilder::new(&mut font,to_window_transformation);
|
||||
let mut builder = GlyphVertexPositionBuilder::new(&mut font,to_window_transformation,0);
|
||||
let a_vertices = builder.build_for_next_glyph('A');
|
||||
assert_eq!(Some('A'), builder.previous_char);
|
||||
assert_eq!(0.56 , builder.pen_position.x);
|
||||
@ -226,17 +268,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test(async)]
|
||||
fn build_texture_coordinates_for_glyph_square() -> impl Future<Output=()> {
|
||||
fn build_texture_coords_for_glyph_square() -> impl Future<Output=()> {
|
||||
TestAfterInit::schedule(|| {
|
||||
let mut font = FontRenderInfo::mock_font("Test font".to_string());
|
||||
font.mock_char_info('A');
|
||||
font.mock_char_info('W');
|
||||
|
||||
let mut builder = GlyphSquareTextureCoordinatesBuilder::new(&mut font);
|
||||
let a_texture_coordinates = builder.build_for_next_glyph('A');
|
||||
let w_texture_coordinates = builder.build_for_next_glyph('W');
|
||||
let mut builder = GlyphTextureCoordsBuilder::new(&mut font);
|
||||
let a_texture_coords = builder.build_for_next_glyph('A');
|
||||
let w_texture_coords = builder.build_for_next_glyph('W');
|
||||
|
||||
let expected_a_coordinates = &
|
||||
let expected_a_coords = &
|
||||
[ 1./64. , 1./128.
|
||||
, 1./64. , 63./128.
|
||||
, 63./64. , 1./128.
|
||||
@ -244,7 +286,7 @@ mod tests {
|
||||
, 1./64. , 63./128.
|
||||
, 63./64. , 63./128.
|
||||
];
|
||||
let expected_w_coordinates = &
|
||||
let expected_w_coords = &
|
||||
[ 1./64. , 65./128.
|
||||
, 1./64. , 127./128.
|
||||
, 63./64. , 65./128.
|
||||
@ -253,8 +295,8 @@ mod tests {
|
||||
, 63./64. , 127./128.
|
||||
];
|
||||
|
||||
assert_eq!(expected_a_coordinates, a_texture_coordinates.as_ref());
|
||||
assert_eq!(expected_w_coordinates, w_texture_coordinates.as_ref());
|
||||
assert_eq!(expected_a_coords, a_texture_coords.as_ref());
|
||||
assert_eq!(expected_w_coords, w_texture_coords.as_ref());
|
||||
})
|
||||
}
|
||||
|
154
gui/lib/core/src/text/buffer/line.rs
Normal file
154
gui/lib/core/src/text/buffer/line.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::text::buffer::glyph_square::
|
||||
{GlyphAttributeBuilder,GlyphVertexPositionBuilder,GlyphTextureCoordsBuilder};
|
||||
|
||||
|
||||
// ============================
|
||||
// === LineAttributeBuilder ===
|
||||
// ============================
|
||||
|
||||
/// Buffer data for line of text builder
|
||||
///
|
||||
/// This builder makes a fixed-size buffers for a specific attribute (e.g. vertex position or
|
||||
/// texture coordinates) for one line. If line is longer than `max_line_size`, it is
|
||||
/// cut. When line is shorter, the buffer is padded with empty values (obtained from
|
||||
/// `GlyphAttributeBuilder::empty()`).
|
||||
pub struct LineAttributeBuilder<'a,GlyphBuilder:GlyphAttributeBuilder> {
|
||||
max_line_size : usize,
|
||||
squares_produced : usize,
|
||||
glyph_builder : GlyphBuilder,
|
||||
chars_iterator : Option<std::str::Chars<'a>>,
|
||||
}
|
||||
|
||||
pub type LineVerticesBuilder<'a,'b> = LineAttributeBuilder<'a,GlyphVertexPositionBuilder<'b>>;
|
||||
pub type LineTextureCoordsBuilder<'a,'b> = LineAttributeBuilder<'a,GlyphTextureCoordsBuilder<'b>>;
|
||||
|
||||
impl<'a,GlyphBuilder: GlyphAttributeBuilder> LineAttributeBuilder<'a,GlyphBuilder> {
|
||||
/// Create new LineAttributeBuilder based on `glyph_builder`
|
||||
pub fn new(line: &'a str, glyph_builder: GlyphBuilder, max_line_size:usize)
|
||||
-> LineAttributeBuilder<'a, GlyphBuilder> {
|
||||
LineAttributeBuilder {
|
||||
max_line_size,
|
||||
glyph_builder,
|
||||
squares_produced: 0,
|
||||
chars_iterator: Some(line.chars())
|
||||
}
|
||||
}
|
||||
|
||||
fn next_item(&mut self) -> GlyphBuilder::Output {
|
||||
let chars_iterator_output = self.chars_iterator.as_mut().map(|iter| iter.next());
|
||||
match chars_iterator_output {
|
||||
Some(Some(ch)) => self.chars_iterator_returned_value(ch),
|
||||
Some(None) => self.chars_iterator_returned_none(),
|
||||
None => self.chars_iterator_exthaused_before()
|
||||
}
|
||||
}
|
||||
|
||||
fn chars_iterator_returned_value(&mut self, value:char) -> GlyphBuilder::Output {
|
||||
self.glyph_builder.build_for_next_glyph(value)
|
||||
}
|
||||
|
||||
fn chars_iterator_returned_none(&mut self) -> GlyphBuilder::Output {
|
||||
self.chars_iterator = None;
|
||||
GlyphBuilder::empty()
|
||||
}
|
||||
|
||||
fn chars_iterator_exthaused_before(&mut self) -> GlyphBuilder::Output {
|
||||
GlyphBuilder::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a,GlyphBuilder: GlyphAttributeBuilder> Iterator for LineAttributeBuilder<'a,GlyphBuilder> {
|
||||
type Item = GlyphBuilder::Output;
|
||||
|
||||
/// Get buffer data for next glyph
|
||||
///
|
||||
/// If we have reached end of line before, the empty data is returned. Iterator stops when
|
||||
/// number of items produced reach `max_line_size` value.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let values_remain = self.squares_produced < self.max_line_size;
|
||||
values_remain.and_option_from(|| {
|
||||
self.squares_produced += 1;
|
||||
Some(self.next_item())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct GlyphAttributeBuilderMock<'a> {
|
||||
iteration : usize,
|
||||
processed_chars : &'a mut Vec<char>
|
||||
}
|
||||
|
||||
type TestOutput = SmallVec<[usize;2]>;
|
||||
|
||||
impl<'a> GlyphAttributeBuilder for GlyphAttributeBuilderMock<'a> {
|
||||
const OUTPUT_SIZE: usize = 2;
|
||||
type Output = TestOutput;
|
||||
|
||||
fn build_for_next_glyph(&mut self, ch: char) -> Self::Output {
|
||||
self.iteration += 1;
|
||||
self.processed_chars.push(ch);
|
||||
SmallVec::from_buf([self.iteration, self.iteration+1])
|
||||
}
|
||||
|
||||
fn empty() -> Self::Output {
|
||||
SmallVec::from_buf([0;2])
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_attribute_builder_with_short_line() {
|
||||
let line = "SKR";
|
||||
let mut processed_chars = Vec::<char>::new();
|
||||
let max_line_size = 6;
|
||||
let glyph_builder = GlyphAttributeBuilderMock {
|
||||
iteration : 0,
|
||||
processed_chars : &mut processed_chars
|
||||
};
|
||||
let line_builder = LineAttributeBuilder::new(line,glyph_builder,max_line_size);
|
||||
let data = line_builder.collect::<Vec<TestOutput>>();
|
||||
|
||||
let expected_data = vec!
|
||||
[ SmallVec::from_buf([1, 2])
|
||||
, SmallVec::from_buf([2, 3])
|
||||
, SmallVec::from_buf([3, 4])
|
||||
, SmallVec::from_buf([0, 0])
|
||||
, SmallVec::from_buf([0, 0])
|
||||
, SmallVec::from_buf([0, 0])
|
||||
];
|
||||
assert_eq!(expected_data, data);
|
||||
let expected_chars = vec!['S', 'K', 'R'];
|
||||
assert_eq!(expected_chars, processed_chars);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_attribute_builder_with_long_line() {
|
||||
let line = "XIXAXA XOXAXA XUXAXA";
|
||||
let mut processed_chars = Vec::<char>::new();
|
||||
let max_line_size = 6;
|
||||
let glyph_builder = GlyphAttributeBuilderMock {
|
||||
iteration : 0,
|
||||
processed_chars : &mut processed_chars
|
||||
};
|
||||
let line_builder = LineAttributeBuilder::new(line,glyph_builder,max_line_size);
|
||||
let data = line_builder.collect::<Vec<TestOutput>>();
|
||||
|
||||
let expected_data = vec!
|
||||
[ SmallVec::from_buf([1, 2])
|
||||
, SmallVec::from_buf([2, 3])
|
||||
, SmallVec::from_buf([3, 4])
|
||||
, SmallVec::from_buf([4, 5])
|
||||
, SmallVec::from_buf([5, 6])
|
||||
, SmallVec::from_buf([6, 7])
|
||||
];
|
||||
assert_eq!(expected_data, data);
|
||||
let expected_chars = vec!['X', 'I', 'X', 'A', 'X', 'A'];
|
||||
assert_eq!(expected_chars, processed_chars);
|
||||
}
|
||||
}
|
@ -7,12 +7,13 @@ edition = "2018"
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
failure = "0.1.5"
|
||||
derive_more = "0.15.0"
|
||||
shrinkwraprs = "0.2.1"
|
||||
itertools = "0.8"
|
||||
derivative = "1.0.3"
|
||||
num = "0.2.0"
|
||||
boolinator = "2.4.0"
|
||||
derivative = "1.0.3"
|
||||
derive_more = "0.15.0"
|
||||
failure = "0.1.5"
|
||||
itertools = "0.8"
|
||||
lazy_static = "1.3.0"
|
||||
num = "0.2.0"
|
||||
paste = "0.1"
|
||||
shrinkwraprs = "0.2.1"
|
||||
smallvec = "1.0.0"
|
||||
|
@ -12,6 +12,7 @@ pub use derivative::Derivative;
|
||||
pub use derive_more::*;
|
||||
pub use failure::Fail;
|
||||
pub use itertools::Itertools;
|
||||
pub use lazy_static::lazy_static;
|
||||
pub use num::Num;
|
||||
pub use paste;
|
||||
pub use shrinkwraprs::Shrinkwrap;
|
||||
|
Loading…
Reference in New Issue
Block a user