Implemented the reactions of TextField for keyboard events.

It includes change for Fonts - now we don't have to pass
reference to FontRegistry on each text operation.

Original commit: e6e44ad827
This commit is contained in:
Adam Obuchowicz 2020-02-13 10:26:42 +01:00 committed by GitHub
parent dc1ce292b0
commit 0fe5b0fe8e
24 changed files with 1423 additions and 1444 deletions

View File

@ -36,10 +36,9 @@ In particular, you can provide them with `--release`, `--dev`, or `--profile`
flags to switch the compilation profile. If not option is provided, the scripts
default to the `--release` profile.
### Running examples
Please note that in order to run the examples you have to first build the
project. For best experience, it is recommended to use the
`scripts/watch.sh --dev` in a second shell. In order to build the demo scenes,
### Running application and examples
For best experience, it is recommended to use the
`scripts/watch.sh --dev` in a second shell. In order to build the IDE application,
follow the steps below:
```bash
@ -48,7 +47,8 @@ npm install
npm run start
```
You can now navigate to http://localhost:8080 and play with the demo scenes!
You can now navigate to http://localhost:8080 and play with it! The example
scenes will be available at http://localhost:8080/debug.
While Webpack provides handy utilities for development, like live-reloading on
sources change, it also adds some runtime overhead. In order to run the compiled

View File

@ -5,16 +5,22 @@
mod internal;
pub mod emscripten_data;
pub mod test_utils;
pub use enso_prelude as prelude;
use internal::*;
use emscripten_data::ArrayMemoryView;
use js_sys::Uint8Array;
use std::future::Future;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::Closure;
// ======================
// === Initialization ===
// ======================
@ -33,6 +39,29 @@ where F : 'static + FnOnce() {
}
}
/// Returns future which returns once the msdfgen library is initialized.
pub fn initialized() -> impl Future<Output=()> {
MsdfgenJsInitialized{}
}
/// The future for running test after initialization
#[derive(Debug)]
struct MsdfgenJsInitialized {}
impl Future for MsdfgenJsInitialized {
type Output = ();
fn poll(self:Pin<&mut Self>, cx:&mut Context<'_>) -> Poll<Self::Output> {
if is_emscripten_runtime_initialized() {
Poll::Ready(())
} else {
let waker = cx.waker().clone();
run_once_initialized(move || waker.wake());
Poll::Pending
}
}
}
// ============
// === Font ===
// ============
@ -182,47 +211,46 @@ impl Drop for MultichannelSignedDistanceField {
#[cfg(test)]
mod tests {
use crate::*;
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
use super::*;
use basegl_core_embedded_fonts::EmbeddedFonts;
use std::future::Future;
use test_utils::TestAfterInit;
use nalgebra::Vector2;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test(async)]
fn generate_msdf_for_capital_a() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
// given
let font_base = EmbeddedFonts::create_and_fill();
let font = Font::load_from_memory(
font_base.font_data_by_name.get("DejaVuSansMono-Bold").unwrap()
);
let params = MsdfParameters {
width : 32,
height : 32,
edge_coloring_angle_threshold : 3.0,
range : 2.0,
max_scale : 2.0,
edge_threshold : 1.001,
overlap_support : true
};
// when
let msdf = MultichannelSignedDistanceField::generate(
&font,
'A' as u32,
&params,
);
// then
let data : Vec<f32> = msdf.data.iter().collect();
assert_eq!(-0.9408906 , data[0]); // Note [asserts]
assert_eq!(0.2 , data[10]);
assert_eq!(-4.3035655 , data[data.len()-1]);
assert_eq!(Vector2::new(3.03125, 1.0), msdf.translation);
assert_eq!(Vector2::new(1.25, 1.25) , msdf.scale);
assert_eq!(19.265625 , msdf.advance);
})
async fn generate_msdf_for_capital_a() {
initialized().await;
// given
let font_base = EmbeddedFonts::create_and_fill();
let font = Font::load_from_memory(
font_base.font_data_by_name.get("DejaVuSansMono-Bold").unwrap()
);
let params = MsdfParameters {
width : 32,
height : 32,
edge_coloring_angle_threshold : 3.0,
range : 2.0,
max_scale : 2.0,
edge_threshold : 1.001,
overlap_support : true
};
// when
let msdf = MultichannelSignedDistanceField::generate(
&font,
'A' as u32,
&params,
);
// then
let data : Vec<f32> = msdf.data.iter().collect();
assert_eq!(-0.9408906 , data[0]); // Note [asserts]
assert_eq!(0.2 , data[10]);
assert_eq!(-4.3035655 , data[data.len()-1]);
assert_eq!(Vector2::new(3.03125, 1.0), msdf.translation);
assert_eq!(Vector2::new(1.25, 1.25) , msdf.scale);
assert_eq!(19.265625 , msdf.advance);
}
/* Note [asserts]

View File

@ -1,32 +0,0 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use std::future::Future;
use crate::{ is_emscripten_runtime_initialized, run_once_initialized };
/// The future for running test after initialization
#[derive(Debug)]
pub struct TestAfterInit<F:Fn()> {
test : F
}
impl<F:Fn()> TestAfterInit<F> {
pub fn schedule(test:F) -> TestAfterInit<F> {
TestAfterInit{test}
}
}
impl<F:Fn()> Future for TestAfterInit<F> {
type Output = ();
fn poll(self:Pin<&mut Self>, cx:&mut Context<'_>) -> Poll<Self::Output> {
if is_emscripten_runtime_initialized() {
(self.test)();
Poll::Ready(())
} else {
let waker = cx.waker().clone();
run_once_initialized(move || waker.wake());
Poll::Pending
}
}
}

View File

@ -15,6 +15,52 @@ use std::collections::hash_map::Entry::Occupied;
use std::collections::hash_map::Entry::Vacant;
#[derive(Debug)]
struct Cache<K:Eq+Hash, V> {
map: RefCell<HashMap<K,V>>,
}
impl<K:Eq+Hash, V:Copy> Cache<K,V> {
pub fn get_or_create<F>(&self, key:K, constructor:F) -> V
where F : FnOnce() -> V {
let mut map = self.map.borrow_mut();
match map.entry(key) {
Occupied(entry) => *entry.get(),
Vacant(entry) => *entry.insert(constructor()),
}
}
}
impl<K:Eq+Hash, V:Clone> Cache<K,V> {
pub fn get_clone_or_create<F>(&self, key:K, constructor:F) -> V
where F : FnOnce() -> V {
let mut map = self.map.borrow_mut();
match map.entry(key) {
Occupied(entry) => entry.get().clone(),
Vacant(entry) => entry.insert(constructor()).clone(),
}
}
}
impl<K:Eq+Hash, V> Cache<K,V> {
pub fn len(&self) -> usize {
self.map.borrow().len()
}
pub fn is_empty(&self) -> bool {
self.map.borrow().is_empty()
}
pub fn invalidate(&self, key:&K) {
self.map.borrow_mut().remove(key);
}
}
impl<K:Eq+Hash, V> Default for Cache<K,V> {
fn default() -> Self {
Cache { map:default() }
}
}
// ========================
// === Font render info ===
@ -44,28 +90,11 @@ pub struct GlyphRenderInfo {
pub advance: f32
}
/// A single font data used for rendering
///
/// The data for individual characters and kerning are load on demand.
///
/// Each distance and transformation values are expressed in normalized coordinates, where `y` = 0.0
/// is _baseline_ and `y` = 1.0 is _ascender_. For explanation of various font-rendering terms, see
/// [freetype documentation](https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1)
#[derive(Debug)]
pub struct FontRenderInfo {
/// Name of the font.
pub name : String,
msdf_sys_font : msdf_sys::Font,
msdf_texture : MsdfTexture,
glyphs : HashMap<char,GlyphRenderInfo>,
kerning : HashMap<(char,char),f32>
}
impl FontRenderInfo {
impl GlyphRenderInfo {
/// See `MSDF_PARAMS` docs.
pub const MAX_MSDF_SHRINK_FACTOR : f64 = 4.; // Note [Picked MSDF parameters]
pub const MAX_MSDF_SHRINK_FACTOR : f64 = 4.;
/// See `MSDF_PARAMS` docs.
pub const MAX_MSDF_GLYPH_SCALE : f64 = 2.; // Note [Picked MSDF parameters]
pub const MAX_MSDF_GLYPH_SCALE : f64 = 2.;
/// Parameters used for MSDF generation.
///
@ -85,14 +114,51 @@ impl FontRenderInfo {
overlap_support : true
};
/// Load new GlyphRenderInfo from msdf_sys font handle. This also extends the msdf_texture with
/// MSDF generated for this character.
pub fn load(handle:&msdf_sys::Font, ch:char, msdf_texture:&MsdfTexture) -> Self {
let unicode = ch as u32;
let params = Self::MSDF_PARAMS;
let msdf = MultichannelSignedDistanceField::generate(handle,unicode,&params);
let inversed_scale = Vector2::new(1.0/msdf.scale.x, 1.0/msdf.scale.y);
let translation = convert_msdf_translation(&msdf);
let glyph_id = msdf_texture.rows() / MsdfTexture::ONE_GLYPH_HEIGHT;
msdf_texture.extend_f32(msdf.data.iter());
GlyphRenderInfo {
msdf_texture_glyph_id : glyph_id,
offset : nalgebra::convert(-translation),
scale : nalgebra::convert(inversed_scale),
advance : x_distance_from_msdf_value(msdf.advance),
}
}
}
/// A single font data used for rendering
///
/// The data for individual characters and kerning are load on demand.
///
/// Each distance and transformation values are expressed in normalized coordinates, where `y` = 0.0
/// is _baseline_ and `y` = 1.0 is _ascender_. For explanation of various font-rendering terms, see
/// [freetype documentation](https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1)
#[derive(Debug)]
pub struct FontRenderInfo {
/// Name of the font.
pub name : String,
msdf_sys_font : msdf_sys::Font,
msdf_texture : MsdfTexture,
glyphs : Cache<char,GlyphRenderInfo>,
kerning : Cache<(char,char),f32>
}
impl FontRenderInfo {
/// Create render info based on font data in memory
pub fn new(name:String, font_data:&[u8]) -> FontRenderInfo {
FontRenderInfo {
name,
FontRenderInfo {name,
msdf_sys_font : msdf_sys::Font::load_from_memory(font_data),
msdf_texture : MsdfTexture { data : Vec::new() },
glyphs : HashMap::new(),
kerning : HashMap::new()
msdf_texture : default(),
glyphs : default(),
kerning : default(),
}
}
@ -103,80 +169,59 @@ impl FontRenderInfo {
font_data_opt.map(|data| FontRenderInfo::new(name.to_string(),data))
}
/// Load char render info
pub fn load_char(&mut self, ch:char) {
let handle = &self.msdf_sys_font;
let unicode = ch as u32;
let params = Self::MSDF_PARAMS;
let msdf = MultichannelSignedDistanceField::generate(handle,unicode,&params);
let inversed_scale = Vector2::new(1.0/msdf.scale.x, 1.0/msdf.scale.y);
let translation = convert_msdf_translation(&msdf);
let glyph_info = GlyphRenderInfo {
msdf_texture_glyph_id : self.glyphs.len(),
offset : nalgebra::convert(-translation),
scale : nalgebra::convert(inversed_scale),
advance : x_distance_from_msdf_value(msdf.advance),
};
self.msdf_texture.extend(msdf.data.iter());
self.glyphs.insert(ch, glyph_info);
}
/// Get render info for one character, generating one if not found
pub fn get_glyph_info(&mut self, ch:char) -> &GlyphRenderInfo {
if !self.glyphs.contains_key(&ch) {
self.load_char(ch);
}
self.glyphs.get(&ch).unwrap()
pub fn get_glyph_info(&self, ch:char) -> GlyphRenderInfo {
let handle = &self.msdf_sys_font;
self.glyphs.get_or_create(ch, move || GlyphRenderInfo::load(handle,ch,&self.msdf_texture))
}
/// Get kerning between two characters
pub fn get_kerning(&mut self, left : char, right : char) -> f32 {
match self.kerning.entry((left,right)) {
Occupied(entry) => *entry.get(),
Vacant(entry) => {
let msdf_val = self.msdf_sys_font.retrieve_kerning(left, right);
let normalized = x_distance_from_msdf_value(msdf_val);
*entry.insert(normalized)
}
}
pub fn get_kerning(&self, left:char, right:char) -> f32 {
self.kerning.get_or_create((left,right), || {
let msdf_val = self.msdf_sys_font.retrieve_kerning(left, right);
x_distance_from_msdf_value(msdf_val)
})
}
/// A whole msdf texture bound for this font.
pub fn msdf_texture(&self) -> &MsdfTexture {
&self.msdf_texture
pub fn with_borrowed_msdf_texture_data<F,R>(&self, operation:F) -> R
where F : FnOnce(&Vec<u8>) -> R {
self.msdf_texture.with_borrowed_data(operation)
}
/// Get number of rows in msdf texture.
pub fn msdf_texture_rows(&self) -> usize {
self.msdf_texture.rows()
}
#[cfg(test)]
pub fn mock_font(name : String) -> FontRenderInfo {
FontRenderInfo {
name,
FontRenderInfo { name,
msdf_sys_font : msdf_sys::Font::mock_font(),
msdf_texture : MsdfTexture { data : Vec::new() },
glyphs : HashMap::new(),
kerning : HashMap::new()
msdf_texture : default(),
glyphs : default(),
kerning : default(),
}
}
#[cfg(test)]
pub fn mock_char_info(&mut self, ch : char) -> &mut GlyphRenderInfo {
let data_size = MsdfTexture::ONE_GLYPH_SIZE;
let msdf_data = (0..data_size).map(|_| 0.12345);
pub fn mock_char_info
(&self, ch:char, offset:Vector2<f32>, scale:Vector2<f32>, advance:f32) -> GlyphRenderInfo {
self.glyphs.invalidate(&ch);
let data_size = MsdfTexture::ONE_GLYPH_SIZE;
let msdf_data = (0..data_size).map(|_| 0.12345);
let msdf_texture_glyph_id = self.msdf_texture_rows() / MsdfTexture::ONE_GLYPH_HEIGHT;
let char_info = GlyphRenderInfo {
msdf_texture_glyph_id : self.glyphs.len(),
offset : Vector2::new(0.0, 0.0),
scale : Vector2::new(1.0, 1.0),
advance : 0.0
};
self.msdf_texture.extend(msdf_data);
self.glyphs.insert(ch, char_info);
self.glyphs.get_mut(&ch).unwrap()
self.msdf_texture.extend_f32(msdf_data);
self.glyphs.get_or_create(ch, move || {
GlyphRenderInfo {offset,scale,advance,msdf_texture_glyph_id}
})
}
#[cfg(test)]
pub fn mock_kerning_info(&mut self, l : char, r : char, value : f32) {
self.kerning.insert((l,r),value);
pub fn mock_kerning_info(&self, l:char, r:char, value:f32) {
self.kerning.invalidate(&(l,r));
self.kerning.get_or_create((l,r),|| value);
}
}
@ -187,14 +232,13 @@ impl FontRenderInfo {
// ===================
/// A handle for fonts loaded into memory.
pub type FontId = usize;
pub type FontHandle = Rc<FontRenderInfo>;
/// Structure keeping all fonts loaded from different sources.
#[derive(Debug)]
pub struct FontRegistry {
embedded : EmbeddedFonts,
fonts : HashMap<FontId,FontRenderInfo>,
next_id : FontId
fonts : HashMap<String,FontHandle>,
}
impl FontRegistry {
@ -203,27 +247,28 @@ impl FontRegistry {
FontRegistry {
embedded : EmbeddedFonts::create_and_fill(),
fonts : HashMap::new(),
next_id : 1
}
}
/// Load data from one of embedded fonts. Returns None if embedded font not found.
pub fn load_embedded_font(&mut self, name:&str) -> Option<FontId> {
let render_info = FontRenderInfo::from_embedded(&self.embedded,name);
render_info.map(|info| self.put_render_info(info))
/// Get render font info from loaded fonts, and if it does not exists, load data from one of
/// embedded fonts. Returns None if the name is missing in both loaded and embedded font list.
pub fn get_or_load_embedded_font(&mut self, name:&str) -> Option<FontHandle> {
match self.fonts.entry(name.to_string()) {
Occupied(entry) => Some(entry.get().clone()),
Vacant(entry) => {
let font_opt = FontRenderInfo::from_embedded(&self.embedded,name);
font_opt.map(|font| {
let rc = Rc::new(font);
entry.insert(rc.clone_ref());
rc
})
}
}
}
fn put_render_info(&mut self, font:FontRenderInfo) -> FontId {
let id = self.next_id;
self.fonts.insert(id,font);
self.next_id += 1;
id
}
/// Get render info of one of loaded fonts. Panics for unrecognized id - you should only use
/// ids returned from `Fonts`' functions.
pub fn get_render_info(&mut self, id:FontId) -> &mut FontRenderInfo {
self.fonts.get_mut(&id).unwrap()
/// Get handle one of loaded fonts.
pub fn get_render_info(&mut self, name:&str) -> Option<FontHandle> {
self.fonts.get_mut(name).cloned()
}
}
@ -240,10 +285,7 @@ mod tests {
use super::*;
use crate::display::shape::text::glyph::msdf::MsdfTexture;
use basegl_core_msdf_sys as msdf_sys;
use basegl_core_embedded_fonts::EmbeddedFonts;
use msdf_sys::test_utils::TestAfterInit;
use std::future::Future;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
@ -260,61 +302,58 @@ mod tests {
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test(async)]
fn empty_font_render_info() -> impl Future<Output=()> {
TestAfterInit::schedule(||{
let font_render_info = create_test_font_render_info();
async fn empty_font_render_info() {
basegl_core_msdf_sys::initialized().await;
let font_render_info = create_test_font_render_info();
assert_eq!(TEST_FONT_NAME, font_render_info.name);
assert_eq!(0, font_render_info.msdf_texture.data.len());
assert_eq!(0, font_render_info.glyphs.len());
})
assert_eq!(TEST_FONT_NAME, font_render_info.name);
assert_eq!(0, font_render_info.msdf_texture.with_borrowed_data(Vec::len));
assert_eq!(0, font_render_info.glyphs.len());
}
#[wasm_bindgen_test(async)]
fn loading_chars() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font_render_info = create_test_font_render_info();
async fn loading_glyph_info() {
basegl_core_msdf_sys::initialized().await;
let font_render_info = create_test_font_render_info();
font_render_info.load_char('A');
font_render_info.load_char('B');
font_render_info.get_glyph_info('A');
font_render_info.get_glyph_info('B');
let chars = 2;
let tex_width = MsdfTexture::WIDTH;
let tex_height = MsdfTexture::ONE_GLYPH_HEIGHT * chars;
let channels = MultichannelSignedDistanceField::CHANNELS_COUNT;
let tex_size = tex_width * tex_height * channels;
let chars = 2;
let tex_width = MsdfTexture::WIDTH;
let tex_height = MsdfTexture::ONE_GLYPH_HEIGHT * chars;
let channels = MultichannelSignedDistanceField::CHANNELS_COUNT;
let tex_size = tex_width * tex_height * channels;
assert_eq!(tex_height , font_render_info.msdf_texture.rows());
assert_eq!(tex_size , font_render_info.msdf_texture.data.len());
assert_eq!(chars , font_render_info.glyphs.len());
assert_eq!(tex_height , font_render_info.msdf_texture_rows());
assert_eq!(tex_size , font_render_info.msdf_texture.with_borrowed_data(Vec::len));
assert_eq!(chars , font_render_info.glyphs.len());
let first_char = font_render_info.glyphs.get(&'A').unwrap();
let second_char = font_render_info.glyphs.get(&'B').unwrap();
let first_char = font_render_info.glyphs.get_or_create('A', || panic!("Expected value"));
let second_char = font_render_info.glyphs.get_or_create('B', || panic!("Expected value"));
let first_index = 0;
let second_index = 1;
let first_index = 0;
let second_index = 1;
assert_eq!(first_index , first_char.msdf_texture_glyph_id);
assert_eq!(second_index , second_char.msdf_texture_glyph_id);
})
assert_eq!(first_index , first_char.msdf_texture_glyph_id);
assert_eq!(second_index , second_char.msdf_texture_glyph_id);
}
#[wasm_bindgen_test(async)]
fn getting_or_creating_char() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font_render_info = create_test_font_render_info();
async fn getting_or_creating_char() {
basegl_core_msdf_sys::initialized().await;
let font_render_info = create_test_font_render_info();
{
let char_info = font_render_info.get_glyph_info('A');
assert_eq!(0, char_info.msdf_texture_glyph_id);
}
assert_eq!(1, font_render_info.glyphs.len());
{
let char_info = font_render_info.get_glyph_info('A');
assert_eq!(0, char_info.msdf_texture_glyph_id);
}
assert_eq!(1, font_render_info.glyphs.len());
{
let char_info = font_render_info.get_glyph_info('A');
assert_eq!(0, char_info.msdf_texture_glyph_id);
}
assert_eq!(1, font_render_info.glyphs.len());
})
{
let char_info = font_render_info.get_glyph_info('A');
assert_eq!(0, char_info.msdf_texture_glyph_id);
}
assert_eq!(1, font_render_info.glyphs.len());
}
}

View File

@ -1,5 +1,7 @@
//! Multichannel Signed Distance Field handling.
use crate::prelude::*;
use basegl_core_msdf_sys as msdf_sys;
use msdf_sys::MultichannelSignedDistanceField;
use nalgebra::clamp;
@ -13,10 +15,10 @@ use nalgebra::clamp;
/// This structure keeps texture data in 8-bit-per-channel RGB format, which
/// is ready to be passed to webgl texImage2D. The texture contains MSDFs for
/// all loaded glyph, organized in vertical column.
#[derive(Debug)]
#[derive(Debug,Default)]
pub struct MsdfTexture {
/// A plain data of this texture.
pub data : Vec<u8>
data : RefCell<Vec<u8>>
}
impl MsdfTexture {
@ -33,30 +35,38 @@ impl MsdfTexture {
/// Number of rows in texture
pub fn rows(&self) -> usize {
self.data.len() / Self::ROW_SIZE
self.data.borrow().len() / Self::ROW_SIZE
}
/// Do operation on borrowed texture data. Panics, if inside `operation` the texture data will
/// be borrowed again (e.g. by calling `with_borrowed_data`.
pub fn with_borrowed_data<F,R>(&self, operation:F) -> R
where F : FnOnce(&Vec<u8>) -> R {
let data = self.data.borrow();
operation(&data)
}
/// Extends texture with new MSDF data in f32 format
pub fn extend_f32<T:IntoIterator<Item=f32>>(&self, iter:T) {
let f32_iterator = iter.into_iter();
let converted_iterator = f32_iterator.map(Self::convert_cell_from_f32);
self.data.borrow_mut().extend(converted_iterator);
}
fn convert_cell_from_f32(value : f32) -> u8 {
const UNSIGNED_BYTE_MIN : f32 = 0.0;
const UNSIGNED_BYTE_MAX : f32 = 255.0;
let scaled_to_byte = value * UNSIGNED_BYTE_MAX;
let clamped_to_byte = clamp(scaled_to_byte,UNSIGNED_BYTE_MIN,UNSIGNED_BYTE_MAX);
let scaled_to_byte = value * UNSIGNED_BYTE_MAX;
let clamped_to_byte = clamp(scaled_to_byte,UNSIGNED_BYTE_MIN,UNSIGNED_BYTE_MAX);
clamped_to_byte as u8
}
}
impl Extend<f32> for MsdfTexture {
/// Extends texture with new MSDF data in f32 format
fn extend<T:IntoIterator<Item=f32>>(&mut self, iter:T) {
let f32_iterator = iter.into_iter();
let converted_iterator = f32_iterator.map(Self::convert_cell_from_f32);
self.data.extend(converted_iterator);
}
}
// ==================================
// === msdf-sys values converting ===
// === Msdf-sys Values Converting ===
// ==================================
/// Converts x dimension distance obtained from msdf-sys to vertex-space values
@ -98,19 +108,17 @@ pub fn convert_msdf_translation(msdf:&MultichannelSignedDistanceField)
mod test {
use super::*;
use basegl_core_msdf_sys::test_utils::TestAfterInit;
use nalgebra::Vector2;
use std::future::Future;
use wasm_bindgen_test::wasm_bindgen_test;
#[test]
fn extending_msdf_texture() {
let mut texture = MsdfTexture{data : Vec::new()};
let texture = MsdfTexture::default();
let msdf_values: &[f32] = &[-0.5, 0.0, 0.25, 0.5, 0.75, 1.0, 1.25];
texture.extend(msdf_values[..4].iter().cloned());
texture.extend(msdf_values[4..].iter().cloned());
texture.extend_f32(msdf_values[..4].iter().cloned());
texture.extend_f32(msdf_values[4..].iter().cloned());
assert_eq!([0, 0, 63, 127, 191, 255, 255], texture.data.as_slice());
assert_eq!([0, 0, 63, 127, 191, 255, 255], texture.data.borrow().as_slice());
}
#[test]
@ -126,15 +134,14 @@ mod test {
}
#[wasm_bindgen_test(async)]
fn msdf_translation_converting() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut msdf = MultichannelSignedDistanceField::mock_results();
msdf.translation = Vector2::new(16.0, 4.0);
async fn msdf_translation_converting() {
basegl_core_msdf_sys::initialized().await;
let mut msdf = MultichannelSignedDistanceField::mock_results();
msdf.translation = Vector2::new(16.0, 4.0);
let converted = convert_msdf_translation(&msdf);
let expected = nalgebra::Vector2::new(0.5, 1.0/8.0);
let converted = convert_msdf_translation(&msdf);
let expected = nalgebra::Vector2::new(0.5, 1.0/8.0);
assert_eq!(expected, converted);
})
assert_eq!(expected, converted);
}
}

View File

@ -4,7 +4,7 @@
use crate::prelude::*;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::glyph::font::FontHandle;
use nalgebra::Vector2;
@ -20,16 +20,16 @@ use nalgebra::Vector2;
/// [freetype documentation](https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1)
/// for details).
#[derive(Debug)]
pub struct PenIterator<'a,CharIterator> {
pub struct PenIterator<CharIterator> {
position : Vector2<f32>,
line_height : f32,
current_char : Option<char>,
next_chars : CharIterator,
next_advance : f32,
font : &'a mut FontRenderInfo,
font : FontHandle,
}
impl<'a,CharIterator> Iterator for PenIterator<'a,CharIterator>
impl<CharIterator> Iterator for PenIterator<CharIterator>
where CharIterator : Iterator<Item=char> {
type Item = (char,Vector2<f32>);
@ -38,14 +38,14 @@ where CharIterator : Iterator<Item=char> {
}
}
impl<'a,CharIterator> PenIterator<'a,CharIterator>
impl<CharIterator> PenIterator<CharIterator>
where CharIterator : Iterator<Item=char> {
/// Create iterator wrapping `chars`, with pen starting from given position.
pub fn new
( start_from:Vector2<f32>
, line_height:f32
, chars:CharIterator
, font:&'a mut FontRenderInfo
, font:FontHandle
) -> Self {
Self {font,line_height,
position : start_from,
@ -76,47 +76,43 @@ where CharIterator : Iterator<Item=char> {
mod tests {
use super::*;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::glyph::font::GlyphRenderInfo;
use basegl_core_msdf_sys::test_utils::TestAfterInit;
use std::future::Future;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test(async)]
fn moving_pen() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = FontRenderInfo::mock_font("Test font".to_string());
mock_a_glyph_info(&mut font);
mock_w_glyph_info(&mut font);
font.mock_kerning_info('A', 'W', -0.16);
font.mock_kerning_info('W', 'A', 0.0);
async fn moving_pen(){
basegl_core_msdf_sys::initialized().await;
let font = FontHandle::new(FontRenderInfo::mock_font("Test font".to_string()));
mock_a_glyph_info(font.clone_ref());
mock_w_glyph_info(font.clone_ref());
font.mock_kerning_info('A', 'W', -0.16);
font.mock_kerning_info('W', 'A', 0.0);
let initial_position = Vector2::new(0.0,0.0);
let chars = "AWA".chars();
let iter = PenIterator::new(initial_position,1.0,chars,&mut font);
let result = iter.collect_vec();
let expected = vec!
[ ('A', Vector2::new(0.0, 0.0))
, ('W', Vector2::new(0.4, 0.0))
, ('A', Vector2::new(1.1, 0.0))
];
assert_eq!(expected,result);
})
let initial_position = Vector2::new(0.0,0.0);
let chars = "AWA".chars();
let iter = PenIterator::new(initial_position,1.0,chars,font);
let result = iter.collect_vec();
let expected = vec!
[ ('A', Vector2::new(0.0, 0.0))
, ('W', Vector2::new(0.4, 0.0))
, ('A', Vector2::new(1.1, 0.0))
];
assert_eq!(expected,result);
}
fn mock_a_glyph_info(font:&mut FontRenderInfo) -> &mut GlyphRenderInfo {
let a_info = font.mock_char_info('A');
a_info.advance = 0.56;
a_info.scale = Vector2::new(0.5, 0.8);
a_info.offset = Vector2::new(0.1, 0.2);
a_info
fn mock_a_glyph_info(font:FontHandle) -> GlyphRenderInfo {
let advance = 0.56;
let scale = Vector2::new(0.5, 0.8);
let offset = Vector2::new(0.1, 0.2);
font.mock_char_info('A',scale,offset,advance)
}
fn mock_w_glyph_info(font:&mut FontRenderInfo) -> &mut GlyphRenderInfo {
let a_info = font.mock_char_info('W');
a_info.advance = 0.7;
a_info.scale = Vector2::new(0.6, 0.9);
a_info.offset = Vector2::new(0.1, 0.2);
a_info
fn mock_w_glyph_info(font:FontHandle) -> GlyphRenderInfo {
let advance = 0.7;
let scale = Vector2::new(0.6, 0.9);
let offset = Vector2::new(0.1, 0.2);
font.mock_char_info('W',scale,offset,advance)
}
}

View File

@ -3,9 +3,8 @@
use crate::prelude::*;
use crate::display::layout::types::*;
use crate::display::shape::text::glyph::font::FontId;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::glyph::font::FontRegistry;
use crate::display::shape::text::glyph::font::FontHandle;
use crate::display::shape::text::glyph::font::GlyphRenderInfo;
use crate::display::shape::text::glyph::pen::PenIterator;
use crate::display::shape::text::glyph::msdf::MsdfTexture;
use crate::display::symbol::material::Material;
@ -31,7 +30,7 @@ pub struct Glyph {
context : Context,
msdf_index_attr : Attribute<f32>,
color_attr : Attribute<Vector4<f32>>,
font_id : FontId,
font : FontHandle,
msdf_uniform : Uniform<Texture<GpuOnly,Rgb,u8>>,
}
@ -42,25 +41,23 @@ impl Glyph {
}
/// Change the displayed character.
pub fn set_glyph(&mut self, ch:char, fonts:&mut FontRegistry) {
let font = fonts.get_render_info(self.font_id);
let glyph_info = font.get_glyph_info(ch);
pub fn set_glyph(&mut self, ch:char) {
let glyph_info = self.font.get_glyph_info(ch);
self.msdf_index_attr.set(glyph_info.msdf_texture_glyph_id as f32);
self.update_msdf_texture(fonts);
self.update_msdf_texture();
}
fn update_msdf_texture(&mut self, fonts:&mut FontRegistry) {
let font = fonts.get_render_info(self.font_id);
fn update_msdf_texture(&mut self) {
let texture_changed = self.msdf_uniform.with_content(|texture| {
texture.storage().height != font.msdf_texture().rows() as i32
texture.storage().height != self.font.msdf_texture_rows() as i32
});
if texture_changed {
let msdf_texture = font.msdf_texture();
let data = msdf_texture.data.as_slice();
let width = MsdfTexture::WIDTH as i32;
let height = msdf_texture.rows() as i32;
let width = MsdfTexture::WIDTH as i32;
let height = self.font.msdf_texture_rows() as i32;
let texture = Texture::<GpuOnly,Rgb,u8>::new(&self.context,(width,height));
texture.reload_with_content(data);
self.font.with_borrowed_msdf_texture_data(|data| {
texture.reload_with_content(data);
});
self.msdf_uniform.set(texture);
}
}
@ -83,30 +80,28 @@ pub struct Line {
baseline_start : Vector2<f32>,
base_color : Vector4<f32>,
height : f32,
font_id : FontId,
font : FontHandle,
}
impl Line {
/// Replace currently visible text.
///
/// The replacing strings will reuse glyphs which increases performance of rendering text.
pub fn replace_text<Chars>(&mut self, chars:Chars, fonts:&mut FontRegistry)
pub fn replace_text<Chars>(&mut self, chars:Chars)
where Chars : Iterator<Item=char> + Clone {
let font = fonts.get_render_info(self.font_id);
let chars_count = chars.clone().count();
let pen = PenIterator::new(self.baseline_start,self.height,chars.clone(),font);
let font = self.font.clone_ref();
let pen = PenIterator::new(self.baseline_start,self.height,chars,font);
for (glyph,(_,position)) in self.glyphs.iter_mut().zip(pen) {
glyph.set_position(Vector3::new(position.x,position.y,0.0));
}
for (glyph,ch) in self.glyphs.iter_mut().zip(chars) {
let font = fonts.get_render_info(self.font_id);
let glyph_info = font.get_glyph_info(ch);
for (glyph,(ch,position)) in self.glyphs.iter_mut().zip(pen) {
let glyph_info = self.font.get_glyph_info(ch);
let size = glyph_info.scale.scale(self.height);
let offset = glyph_info.offset.scale(self.height);
glyph.set_glyph(ch,fonts);
let x = position.x + offset.x;
let y = position.y + offset.y;
glyph.set_position(Vector3::new(x,y,0.0));
glyph.set_glyph(ch);
glyph.color().set(self.base_color);
glyph.mod_position(|pos| { *pos += Vector3::new(offset.x,offset.y,0.0); });
glyph.size().set(size);
}
for glyph in self.glyphs.iter_mut().skip(chars_count) {
@ -127,15 +122,26 @@ impl Line {
// === Getters ===
#[allow(missing_docs)]
impl Line {
/// The starting point of this line's baseline.
pub fn baseline_start(&self) -> &Vector2<f32> { &self.baseline_start }
pub fn baseline_start(&self) -> &Vector2<f32> {
&self.baseline_start
}
/// Line's height in pixels.
pub fn height (&self) -> f32 { self.height }
pub fn height(&self) -> f32 {
self.height
}
/// Number of glyphs, giving the maximum length of displayed line.
pub fn length (&self) -> usize { self.glyphs.len() }
pub fn font_id (&self) -> FontId { self.font_id }
pub fn length(&self) -> usize {
self.glyphs.len()
}
/// Font used for rendering this line.
pub fn font(&self) -> FontHandle {
self.font.clone_ref()
}
}
@ -149,7 +155,7 @@ impl Line {
pub struct GlyphSystem {
context : Context,
sprite_system : SpriteSystem,
font_id : FontId,
font : FontHandle,
color : Buffer<Vector4<f32>>,
glyph_msdf_index : Buffer<f32>,
msdf_uniform : Uniform<Texture<GpuOnly,Rgb,u8>>,
@ -157,7 +163,7 @@ pub struct GlyphSystem {
impl GlyphSystem {
/// Constructor.
pub fn new(world:&World, font_id:FontId) -> Self {
pub fn new(world:&World, font:FontHandle) -> Self {
let msdf_width = MsdfTexture::WIDTH as f32;
let msdf_height = MsdfTexture::ONE_GLYPH_HEIGHT as f32;
let scene = world.scene();
@ -169,9 +175,9 @@ impl GlyphSystem {
sprite_system.set_material(Self::material());
sprite_system.set_alignment(HorizontalAlignment::Left,VerticalAlignment::Bottom);
scene.variables().add("msdf_range",FontRenderInfo::MSDF_PARAMS.range as f32);
scene.variables().add("msdf_range",GlyphRenderInfo::MSDF_PARAMS.range as f32);
scene.variables().add("msdf_size",Vector2::new(msdf_width,msdf_height));
Self {context,sprite_system,font_id,
Self {context,sprite_system,font,
msdf_uniform : symbol.variables().add_or_panic("msdf_texture",texture),
color : mesh.instance_scope().add_buffer("color"),
glyph_msdf_index : mesh.instance_scope().add_buffer("glyph_msdf_index"),
@ -186,12 +192,12 @@ impl GlyphSystem {
let instance_id = sprite.instance_id();
let color_attr = self.color.at(instance_id);
let msdf_index_attr = self.glyph_msdf_index.at(instance_id);
let font_id = self.font_id;
let font = self.font.clone_ref();
let msdf_uniform = self.msdf_uniform.clone();
color_attr.set(Vector4::new(0.0,0.0,0.0,0.0));
msdf_index_attr.set(0.0);
Glyph {context,sprite,msdf_index_attr,color_attr,font_id,msdf_uniform}
Glyph {context,sprite,msdf_index_attr,color_attr,font,msdf_uniform}
}
/// Create an empty "line" structure with defined number of glyphs. In the returned `Line`
@ -199,30 +205,23 @@ impl GlyphSystem {
///
/// For details, see also `Line` structure documentation.
pub fn new_empty_line
( &mut self
, baseline_start : Vector2<f32>
, height : f32
, length : usize
, color : Vector4<f32>) -> Line {
(&mut self, baseline_start:Vector2<f32>, height:f32, length:usize, color:Vector4<f32>)
-> Line {
let glyphs = (0..length).map(|_| self.new_glyph()).collect();
let base_color = color;
let font_id = self.font_id;
Line {glyphs,baseline_start,height,base_color,font_id}
let font = self.font.clone_ref();
Line {glyphs,baseline_start,height,base_color,font}
}
/// Create a line of glyphs with proper alignment.
///
/// For details, see also `Line` structure documentation.
pub fn new_line
( &mut self
, baseline_start : Vector2<f32>
, height : f32
, text : &str
, color : Vector4<f32>
, fonts : &mut FontRegistry) -> Line {
(&mut self, baseline_start:Vector2<f32>, height:f32, text:&str, color:Vector4<f32>)
-> Line {
let length = text.chars().count();
let mut line = self.new_empty_line(baseline_start,height,length,color);
line.replace_text(text.chars(),fonts);
line.replace_text(text.chars());
line
}
@ -249,7 +248,7 @@ impl GlyphSystem {
material.add_input_def::<f32> ("glyph_msdf_index");
material.add_input("pixel_ratio", 1.0);
material.add_input("zoom" , 1.0);
material.add_input("msdf_range" , FontRenderInfo::MSDF_PARAMS.range as f32);
material.add_input("msdf_range" , GlyphRenderInfo::MSDF_PARAMS.range as f32);
material.add_input("color" , Vector4::new(0.0,0.0,0.0,1.0));
// FIXME We need to use this output, as we need to declare the same amount of shader
// FIXME outputs as the number of attachments to framebuffer. We should manage this more

View File

@ -2,6 +2,7 @@
pub mod content;
pub mod cursor;
pub mod keyboard;
pub mod location;
pub mod render;
@ -16,11 +17,13 @@ use crate::display::shape::text::text_field::cursor::Step;
use crate::display::shape::text::text_field::cursor::CursorNavigation;
use crate::display::shape::text::text_field::location::TextLocation;
use crate::display::shape::text::text_field::location::TextLocationChange;
use crate::display::shape::text::glyph::font::FontId;
use crate::display::shape::text::text_field::keyboard::TextFieldFrp;
use crate::display::shape::text::glyph::font::FontHandle;
use crate::display::shape::text::glyph::font::FontRegistry;
use crate::display::shape::text::text_field::render::TextFieldSprites;
use crate::display::shape::text::text_field::render::assignment::GlyphLinesAssignmentUpdate;
use crate::display::world::World;
use crate::system::web::text_input::KeyboardBinding;
use nalgebra::Vector2;
use nalgebra::Vector3;
@ -33,10 +36,10 @@ use nalgebra::Vector4;
// =====================
/// A display properties of TextField.
#[derive(Clone,Copy,Debug)]
#[derive(Debug)]
pub struct TextFieldProperties {
/// FontId used for rendering text.
pub font_id: FontId,
/// FontHandle used for rendering text.
pub font: FontHandle,
/// Text size being a line height in pixels.
pub text_size: f32,
/// Base color of displayed text.
@ -50,7 +53,7 @@ impl TextFieldProperties {
fn default(fonts:&mut FontRegistry) -> Self {
TextFieldProperties {
font_id : fonts.load_embedded_font(Self::DEFAULT_FONT_FACE).unwrap(),
font : fonts.get_or_load_embedded_font(Self::DEFAULT_FONT_FACE).unwrap(),
text_size : 16.0,
base_color: Vector4::new(1.0, 1.0, 1.0, 1.0),
size : Vector2::new(100.0,100.0),
@ -66,49 +69,33 @@ shared! { TextField
/// commits.
#[derive(Debug)]
pub struct TextFieldData {
properties : TextFieldProperties,
content : TextFieldContent,
cursors : Cursors,
rendered : TextFieldSprites,
display_object : DisplayObjectData,
properties : TextFieldProperties,
content : TextFieldContent,
cursors : Cursors,
rendered : TextFieldSprites,
display_object : DisplayObjectData,
frp : Option<TextFieldFrp>,
keyboard_binding : Option<KeyboardBinding>,
}
impl {
/// Create new TextField.
pub fn new
( world : &World
, initial_content : &str
, properties : TextFieldProperties
, fonts : &mut FontRegistry)
-> Self {
let logger = Logger::new("TextField");
let display_object = DisplayObjectData::new(logger);
let content = TextFieldContent::new(initial_content,&properties);
let cursors = Cursors::default();
let rendered = TextFieldSprites::new(world, &properties, fonts);
display_object.add_child(rendered.display_object.clone_ref());
let mut text_field = Self {properties,content,cursors,rendered,display_object};
text_field.initialize(fonts);
text_field
}
/// Set position of this TextField.
pub fn set_position(&mut self, position:Vector3<f32>) {
self.display_object.set_position(position);
}
/// Scroll text by given offset in pixels.
pub fn scroll(&mut self, offset:Vector2<f32>, fonts:&mut FontRegistry) {
self.rendered.display_object.mod_position(|pos| *pos -= Vector3::new(offset.x,offset.y,0.0));
let mut update = self.assignment_update(fonts);
pub fn scroll(&mut self, offset:Vector2<f32>) {
let position_change = -Vector3::new(offset.x,offset.y,0.0);
self.rendered.display_object.mod_position(|pos| *pos += position_change );
let mut update = self.assignment_update();
if offset.x != 0.0 {
update.update_after_x_scroll(offset.x);
}
if offset.y != 0.0 {
update.update_line_assignment();
}
self.rendered.update_glyphs(&mut self.content,fonts);
self.rendered.update_glyphs(&mut self.content);
}
/// Get current scroll position.
@ -117,36 +104,36 @@ shared! { TextField
}
/// Add cursor.
pub fn add_cursor(&mut self, position:TextLocation, fonts:&mut FontRegistry) {
pub fn add_cursor(&mut self, position:TextLocation) {
self.cursors.add_cursor(position);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content);
}
/// Move all cursors by given step.
pub fn navigate_cursors(&mut self, step:Step, selecting:bool, fonts:&mut FontRegistry) {
let content = self.content.full_info(fonts);
pub fn navigate_cursors(&mut self, step:Step, selecting:bool) {
let content = &mut self.content;
let mut navigation = CursorNavigation {content,selecting};
self.cursors.navigate_all_cursors(&mut navigation,step);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content);
}
/// Jump cursor to point on the screen.
pub fn jump_cursor(&mut self, point:Vector2<f32>, selecting:bool, fonts:&mut FontRegistry) {
let content = self.content.full_info(fonts);
pub fn jump_cursor(&mut self, point:Vector2<f32>, selecting:bool) {
let content = &mut self.content;
let mut navigation = CursorNavigation {content,selecting};
self.cursors.remove_additional_cursors();
self.cursors.jump_cursor(&mut navigation,point);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content);
}
/// Make change in text content.
///
/// As an opposite to `edit` function, here we don't care about cursors, just do the change
/// described in `TextChange` structure.
pub fn apply_change(&mut self, change:TextChange, fonts:&mut FontRegistry) {
pub fn apply_change(&mut self, change:TextChange) {
self.content.apply_change(change);
self.assignment_update(fonts).update_after_text_edit();
self.rendered.update_glyphs(&mut self.content,fonts);
self.assignment_update().update_after_text_edit();
self.rendered.update_glyphs(&mut self.content);
}
/// Get the selected text.
@ -160,21 +147,39 @@ shared! { TextField
///
/// All the currently selected text will be removed, and the given string will be inserted
/// by each cursor.
pub fn edit(&mut self, insertion:&str, fonts:&mut FontRegistry) {
let trimmed = insertion.trim_end_matches('\n');
pub fn write(&mut self, text:&str) {
let trimmed = text.trim_end_matches('\n');
let is_line_per_cursor_edit = trimmed.contains('\n') && self.cursors.cursors.len() > 1;
let cursor_ids = self.cursors.sorted_cursor_indices();
if is_line_per_cursor_edit {
let cursor_with_line = cursor_ids.iter().cloned().zip(trimmed.split('\n'));
self.edit_per_cursor(cursor_with_line);
self.write_per_cursor(cursor_with_line);
} else {
let cursor_with_line = cursor_ids.iter().map(|cursor_id| (*cursor_id,insertion));
self.edit_per_cursor(cursor_with_line);
let cursor_with_line = cursor_ids.iter().map(|cursor_id| (*cursor_id,text));
self.write_per_cursor(cursor_with_line);
};
self.assignment_update(fonts).update_after_text_edit();
self.rendered.update_glyphs(&mut self.content,fonts);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
self.assignment_update().update_after_text_edit();
self.rendered.update_glyphs(&mut self.content);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content);
}
/// Remove all text selected by all cursors.
pub fn remove_selection(&mut self) {
self.write("");
}
/// Do delete operation on text.
///
/// For cursors with selection it will just remove the selected text. For the rest, it will
/// remove all content covered by `step`.
pub fn do_delete_operation(&mut self, step:Step) {
let content = &mut self.content;
let selecting = true;
let mut navigation = CursorNavigation {content,selecting};
let without_selection = |c:&Cursor| !c.has_selection();
self.cursors.navigate_cursors(&mut navigation,step,without_selection);
self.remove_selection();
}
/// Update underlying Display Object.
@ -185,26 +190,62 @@ shared! { TextField
}
// === Constructor ===
impl TextField {
/// Create new empty TextField
pub fn new(world:&World, properties:TextFieldProperties) -> Self {
Self::new_with_content(world,"",properties)
}
/// Create new TextField with predefined content.
pub fn new_with_content(world:&World, initial_content:&str, properties:TextFieldProperties)
-> Self {
let data = TextFieldData::new(world,initial_content,properties);
let rc = Rc::new(RefCell::new(data));
let frp = TextFieldFrp::new(Rc::downgrade(&rc));
with(rc.borrow_mut(), move |mut data| {
data.keyboard_binding = Some(frp.bind_frp_to_js_text_input_actions());
data.frp = Some(frp);
});
Self{rc}
}
}
// === Private ===
impl TextFieldData {
fn initialize(&mut self, fonts:&mut FontRegistry) {
self.assignment_update(fonts).update_line_assignment();
self.rendered.update_glyphs(&mut self.content,fonts);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
fn new(world:&World, initial_content:&str, properties:TextFieldProperties) -> Self {
let logger = Logger::new("TextField");
let display_object = DisplayObjectData::new(logger);
let content = TextFieldContent::new(initial_content,&properties);
let cursors = Cursors::default();
let rendered = TextFieldSprites::new(world,&properties);
let frp = None;
let keyboard_binding = None;
display_object.add_child(rendered.display_object.clone_ref());
Self {properties,content,cursors,rendered,display_object,frp,keyboard_binding}.initialize()
}
fn assignment_update<'a,'b>(&'a mut self, fonts:&'b mut FontRegistry)
-> GlyphLinesAssignmentUpdate<'a,'a,'b> {
fn initialize(mut self) -> Self{
self.assignment_update().update_line_assignment();
self.rendered.update_glyphs(&mut self.content);
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content);
self
}
fn assignment_update(&mut self) -> GlyphLinesAssignmentUpdate {
GlyphLinesAssignmentUpdate {
content : self.content.full_info(fonts),
content : &mut self.content,
assignment : &mut self.rendered.assignment,
scroll_offset : -self.rendered.display_object.position().xy(),
view_size : self.properties.size,
}
}
fn edit_per_cursor<'a,It>(&mut self, cursor_id_with_text_to_insert:It)
fn write_per_cursor<'a,It>(&mut self, cursor_id_with_text_to_insert:It)
where It : Iterator<Item=(usize,&'a str)> {
let mut location_change = TextLocationChange::default();
for (cursor_id,to_insert) in cursor_id_with_text_to_insert {

View File

@ -1,374 +0,0 @@
#![allow(missing_docs)]
use crate::prelude::*;
use crate::display::shape::text::font::FontRenderInfo;
use crate::display::shape::text::msdf::MsdfTexture;
use nalgebra::Point2;
use nalgebra::Translation2;
use nalgebra::Affine2;
use nalgebra::Matrix3;
use nalgebra::Scalar;
use std::ops::RangeInclusive;
// ============================
// === Base vertices layout ===
// ============================
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)
];
}
pub fn point_to_iterable<T:Scalar>(p:Point2<T>) -> SmallVec<[T;2]> {
p.iter().cloned().collect()
}
// ===========
// === Pen ===
// ===========
/// A pen position
///
/// The pen is a font-specific term (see
/// [freetype documentation](https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1)
/// for details). The structure keeps pen position _before_ rendering the `current_char`.
#[derive(Clone,Copy,Debug)]
pub struct Pen {
pub position : Point2<f64>,
pub current_char : Option<char>,
pub next_advance : f64,
}
impl Pen {
/// Create the pen structure, where the first char will be rendered at `position`.
pub fn new(position:Point2<f64>) -> Pen {
Pen {position,
current_char : None,
next_advance : 0.0
}
}
pub fn new_with_char(position:Point2<f64>, ch:char, font:&mut FontRenderInfo) -> Self {
Pen {position,
current_char : Some(ch),
next_advance : font.get_glyph_info(ch).advance
}
}
/// Move pen to the next character
///
/// The new position will be the base for `ch` rendering with applied kerning.
pub fn next_char(&mut self, ch:char, font:&mut FontRenderInfo) -> &mut Self {
if let Some(current_ch) = self.current_char {
self.move_pen(current_ch,ch,font)
}
self.next_advance = font.get_glyph_info(ch).advance;
self.current_char = Some(ch);
self
}
fn move_pen(&mut self, current:char, next:char, font:&mut FontRenderInfo) {
let kerning = font.get_kerning(current, next);
let transform = Translation2::new(self.next_advance + kerning, 0.0);
self.position = transform * self.position;
}
pub fn is_in_x_range(&self, range:&RangeInclusive<f64>) -> bool {
let x_min = self.position.x;
let x_max = x_min + self.next_advance;
range.contains(&x_min) || range.contains(&x_max) || (x_min..x_max).contains(range.start())
}
pub fn current_char_x_range(&self) -> RangeInclusive<f64> {
self.position.x..=(self.position.x + self.next_advance)
}
}
// =========================================
// === GlyphSquareVertexAttributeBuilder ===
// =========================================
/// Builder for specific attribute of glyph square's vertices
///
/// Builder is meant to be used for producing attribute data for squares of glyph 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
///
/// `pen` field points to the position of last built glyph.
#[derive(Debug)]
pub struct GlyphVertexPositionBuilder<'a,'b> {
pub font : &'a mut FontRenderInfo,
pub pen : &'b mut Pen,
}
impl<'a,'b> GlyphVertexPositionBuilder<'a,'b> {
/// New GlyphVertexPositionBuilder
///
/// The newly created builder start to place glyph at location pointed by given pen.
pub fn new(font:&'a mut FontRenderInfo, pen:&'b mut Pen) -> Self {
GlyphVertexPositionBuilder {font,pen}
}
fn translation_by_pen_position(&self) -> Translation2<f64>{
Translation2::new(self.pen.position.x, self.pen.position.y)
}
}
impl<'a,'b> GlyphAttributeBuilder for GlyphVertexPositionBuilder<'a,'b> {
const OUTPUT_SIZE : usize = BASE_LAYOUT_SIZE * 2;
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();
let glyph_info = self.font.get_glyph_info(ch);
let glyph_specific_transform = &glyph_info.from_base_layout;
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);
moved_to_pen_position.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
#[derive(Debug)]
pub struct GlyphTextureCoordsBuilder<'a> {
pub font : &'a mut FontRenderInfo
}
impl<'a> GlyphTextureCoordsBuilder<'a> {
/// Create new builder using given font.
pub fn new(font:&'a mut FontRenderInfo) -> 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 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 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
);
Affine2::from_matrix_unchecked(matrix)
}
/// Transformation MSDF texture fragment associated with given glyph
///
/// The MSDF texture contains MSDFs for many glyph, 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_y = glyph_info.msdf_texture_glyph_id as f64 * ;
Translation2::new(0.0, offset_y)
}
}
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 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 * to_msdf * 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 {
use super::*;
use crate::display::shape::text::font::GlyphRenderInfo;
use basegl_core_msdf_sys::test_utils::TestAfterInit;
use std::future::Future;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test(async)]
fn moving_pen() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = FontRenderInfo::mock_font("Test font".to_string());
mock_a_glyph_info(&mut font);
mock_w_glyph_info(&mut font);
font.mock_kerning_info('A', 'W', -0.16);
font.mock_kerning_info('W', 'A', 0.0);
let mut pen = Pen::new(Point2::new(0.0, 0.0));
pen.next_char('A', &mut font);
assert_eq!(Some('A'), pen.current_char);
assert_eq!(0.56 , pen.next_advance);
assert_eq!(0.0 , pen.position.x);
assert_eq!(0.0 , pen.position.y);
pen.next_char('W', &mut font);
assert_eq!(Some('W'), pen.current_char);
assert_eq!(0.7 , pen.next_advance);
assert_eq!(0.4 , pen.position.x);
assert_eq!(0.0 , pen.position.y);
pen.next_char('A', &mut font);
assert_eq!(Some('A'), pen.current_char);
assert_eq!(0.56 , pen.next_advance);
assert_eq!(1.1 , pen.position.x);
assert_eq!(0.0 , pen.position.y);
})
}
#[wasm_bindgen_test(async)]
fn build_vertices_for_glyph_square() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = FontRenderInfo::mock_font("Test font".to_string());
mock_a_glyph_info(&mut font);
mock_w_glyph_info(&mut font);
font.mock_kerning_info('A', 'W', -0.16);
let mut pen = Pen::new(Point2::new(0.0,0.0));
let mut builder = GlyphVertexPositionBuilder::new(&mut font,&mut pen);
let a_vertices = builder.build_for_next_glyph('A');
let w_vertices = builder.build_for_next_glyph('W');
let expected_a_vertices = &
[ 0.1 , 0.2
, 0.1 , 1.0
, 0.6 , 0.2
, 0.6 , 0.2
, 0.1 , 1.0
, 0.6 , 1.0
];
let expected_w_vertices = &
[ 0.5 , 0.2
, 0.5 , 1.1
, 1.1 , 0.2
, 1.1 , 0.2
, 0.5 , 1.1
, 1.1 , 1.1
];
assert_eq!(expected_a_vertices, a_vertices.as_ref());
assert_eq!(expected_w_vertices, w_vertices.as_ref());
assert_eq!(Point2::new(0.4,0.0), pen.position);
assert_eq!(Some('W') , pen.current_char);
})
}
#[wasm_bindgen_test(async)]
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 = 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_coords = &
[ 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 = &
[ 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());
assert_eq!(expected_w_coords, w_texture_coords.as_ref());
})
}
}

View File

@ -1,137 +0,0 @@
#![allow(missing_docs)]
use crate::prelude::*;
use crate::display::shape::text::buffer::glyph_square::GlyphAttributeBuilder;
use crate::display::shape::text::buffer::glyph_square::GlyphVertexPositionBuilder;
use crate::display::shape::text::buffer::glyph_square::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()`).
#[derive(Debug)]
pub struct LineAttributeBuilder<'a,GlyphBuilder:GlyphAttributeBuilder> {
max_line_size : usize,
squares_produced : usize,
glyph_builder : GlyphBuilder,
chars : &'a[char],
}
pub type LineVerticesBuilder<'a,'b,'c> =
LineAttributeBuilder<'a,GlyphVertexPositionBuilder<'b,'c>>;
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(chars: &'a[char], glyph_builder: GlyphBuilder, max_line_size:usize)
-> LineAttributeBuilder<'a, GlyphBuilder> {
LineAttributeBuilder {max_line_size,glyph_builder,chars,
squares_produced : 0,
}
}
}
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;
let next_char = self.chars.get(self.squares_produced);
values_remain.and_option_from(|| {
self.squares_produced += 1;
let next_char_attrs = next_char.map(|ch| self.glyph_builder.build_for_next_glyph(*ch));
let returned_value = next_char_attrs.unwrap_or_else(GlyphBuilder::empty);
Some(returned_value)
})
}
}
#[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".chars().collect_vec();
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.as_slice(),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".chars().collect_vec();
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.as_slice(),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);
}
}

View File

@ -3,9 +3,7 @@ pub mod line;
use crate::prelude::*;
use crate::display::shape::text::glyph::font::FontId;
use crate::display::shape::text::glyph::font::FontRegistry;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::glyph::font::FontHandle;
use crate::display::shape::text::text_field::content::line::Line;
use crate::display::shape::text::text_field::content::line::LineFullInfo;
use crate::display::shape::text::text_field::location::TextLocation;
@ -170,21 +168,10 @@ impl TextChange {
pub struct TextFieldContent {
pub lines : Vec<Line>,
pub dirty_lines : DirtyLines,
pub font : FontId,
pub font : FontHandle,
pub line_height : f32,
}
/// The wrapper for TextFieldContent reference with font. That allows to get specific information
/// about lines and chars position in rendered text.
#[derive(Debug,Shrinkwrap)]
#[shrinkwrap(mutable)]
#[allow(missing_docs)]
pub struct TextFieldContentFullInfo<'a,'b> {
#[shrinkwrap(main_field)]
pub content : &'a mut TextFieldContent,
pub font : &'b mut FontRenderInfo
}
impl TextFieldContent {
/// Create a text component containing `text`
///
@ -194,7 +181,7 @@ impl TextFieldContent {
line_height : properties.text_size,
lines : Self::split_to_lines(text).map(Line::new).collect(),
dirty_lines : DirtyLines::default(),
font : properties.font_id,
font : properties.font.clone_ref(),
}
}
@ -211,16 +198,6 @@ impl TextFieldContent {
}
}
/// Get the full-info wrapper for this content.
pub fn full_info<'a,'b>(&'a mut self, fonts:&'b mut FontRegistry)
-> TextFieldContentFullInfo<'a,'b> {
let font_id = self.font;
TextFieldContentFullInfo {
content : self,
font : fonts.get_render_info(font_id),
}
}
/// Copy the fragment of text and return as String.
pub fn copy_fragment(&self, fragment:Range<TextLocation>) -> String {
let mut output = String::new();
@ -247,7 +224,41 @@ impl TextFieldContent {
let last_line = &self.lines[fragment.end.line];
output.extend(last_line.chars()[..fragment.end.column].iter().cloned());
}
}
/// Get a handy wrapper for line under index.
pub fn line(&mut self, index:usize) -> LineFullInfo {
LineFullInfo {
height : self.line_height,
line : &mut self.lines[index],
line_id : index,
font : self.font.clone_ref(),
}
}
/// Get the nearest text location from the point on the screen.
pub fn location_at_point(&mut self, point:Vector2<f32>) -> TextLocation {
let line_opt = self.line_at_y_position(point.y);
let mut line = match line_opt {
Some(line) => line,
None if point.y >= 0.0 => self.line(0),
None => self.line(self.lines.len()-1),
};
let column_opt = line.find_char_at_x_position(point.x);
let column = match column_opt {
Some(column) => column,
None if point.x <= 0.0 => 0,
None => line.len(),
};
TextLocation{line:line.line_id, column}
}
/// Get the index of line which is displayed at given y screen coordinate.
pub fn line_at_y_position(&mut self, y:f32) -> Option<LineFullInfo> {
let index = -(y / self.line_height).ceil();
let is_valid = index >= 0.0 && index < self.lines.len() as f32;
let index = is_valid.and_option_from(|| Some(index as usize));
index.map(move |i| self.line(i))
}
}
@ -319,52 +330,22 @@ impl TextFieldContent {
}
}
impl<'a,'b> TextFieldContentFullInfo<'a,'b> {
/// Get a handy wrapper for line under index.
pub fn line(&mut self, index:usize) -> LineFullInfo {
LineFullInfo {
height : self.content.line_height,
line : &mut self.content.lines[index],
line_id : index,
font : self.font,
}
}
/// Get the nearest text location from the point on the screen.
pub fn location_at_point(&mut self, point:Vector2<f32>) -> TextLocation {
let line_opt = self.line_at_y_position(point.y);
let mut line = match line_opt {
Some(line) => line,
None if point.y >= 0.0 => self.line(0),
None => self.line(self.lines.len()-1),
};
let column_opt = line.find_char_at_x_position(point.x);
let column = match column_opt {
Some(column) => column,
None if point.x <= 0.0 => 0,
None => line.len(),
};
TextLocation{line:line.line_id, column}
}
/// Get the index of line which is displayed at given y screen coordinate.
pub fn line_at_y_position(&mut self, y:f32) -> Option<LineFullInfo> {
let index = -(y / self.line_height).ceil();
let is_valid = index >= 0.0 && index < self.lines.len() as f32;
let index = is_valid.and_option_from(|| Some(index as usize));
index.map(move |i| self.line(i))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use basegl_core_msdf_sys as msdf_sys;
use nalgebra::Vector2;
use nalgebra::Vector4;
use wasm_bindgen_test::wasm_bindgen_test;
#[test]
fn mark_single_line_as_dirty() {
#[wasm_bindgen_test(async)]
async fn mark_single_line_as_dirty(){
msdf_sys::initialized().await;
let mut dirty_lines = DirtyLines::default();
dirty_lines.add_single_line(3);
dirty_lines.add_single_line(5);
@ -373,8 +354,9 @@ mod test {
assert!( dirty_lines.is_dirty(5));
}
#[test]
fn mark_line_range_as_dirty() {
#[wasm_bindgen_test(async)]
async fn mark_line_range_as_dirty() {
msdf_sys::initialized().await;
let mut dirty_lines = DirtyLines::default();
dirty_lines.add_lines_range(3..=5);
assert!(!dirty_lines.is_dirty(2));
@ -384,8 +366,9 @@ mod test {
assert!(!dirty_lines.is_dirty(6));
}
#[test]
fn mark_line_range_from_as_dirty() {
#[wasm_bindgen_test(async)]
async fn mark_line_range_from_as_dirty() {
msdf_sys::initialized().await;
let mut dirty_lines = DirtyLines::default();
dirty_lines.add_lines_range_from(3..);
dirty_lines.add_lines_range_from(5..);
@ -395,8 +378,9 @@ mod test {
assert!( dirty_lines.is_dirty(70000));
}
#[test]
fn create_content() {
#[wasm_bindgen_test(async)]
async fn create_content() {
msdf_sys::initialized().await;
let single_line = "Single line";
let mutliple_lines = "Multiple\r\nlines\n";
@ -410,8 +394,9 @@ mod test {
assert_eq!("" , multiline_content .lines[2].chars().iter().collect::<String>());
}
#[test]
fn edit_single_line() {
#[wasm_bindgen_test(async)]
async fn edit_single_line() {
msdf_sys::initialized().await;
let text = "Line a\nLine b\nLine c";
let delete_from = TextLocation {line:1, column:0};
let delete_to = TextLocation {line:1, column:4};
@ -439,8 +424,9 @@ mod test {
assert!(!content.dirty_lines.is_dirty(2));
}
#[test]
fn insert_multiple_lines() {
#[wasm_bindgen_test(async)]
async fn insert_multiple_lines() {
msdf_sys::initialized().await;
let text = "Line a\nLine b\nLine c";
let inserted = "Ins a\nIns b";
let begin_loc = TextLocation {line:0, column:0};
@ -476,8 +462,9 @@ mod test {
assert!( content.dirty_lines.is_dirty(2));
}
#[test]
fn delete_multiple_lines() {
#[wasm_bindgen_test(async)]
async fn delete_multiple_lines() {
msdf_sys::initialized().await;
let text = "Line a\nLine b\nLine c";
let delete_from = TextLocation {line:0, column:2};
let delete_to = TextLocation {line:2, column:3};
@ -491,8 +478,9 @@ mod test {
assert_eq!(expected, get_lines_as_strings(&content));
}
#[test]
fn get_line_fragment() {
#[wasm_bindgen_test(async)]
async fn get_line_fragment() {
msdf_sys::initialized().await;
let text = "Line a\nLine b\nLine c";
let single_line = TextLocation {line:1, column:1} .. TextLocation {line:1, column:4};
let line_with_eol = TextLocation {line:1, column:1} .. TextLocation {line:2, column:0};
@ -508,8 +496,9 @@ mod test {
assert_eq!(text , content.copy_fragment(whole_content));
}
#[test]
fn get_inserted_text_location_of_change() {
#[wasm_bindgen_test(async)]
async fn get_inserted_text_location_of_change() {
msdf_sys::initialized().await;
let one_line = "One line";
let two_lines = "Two\nlines";
let replaced_range = TextLocation{line:1,column:2}..TextLocation{line:2,column:2};
@ -529,7 +518,7 @@ mod test {
fn mock_properties()-> TextFieldProperties {
TextFieldProperties {
font_id : 0,
font : FontHandle::new(FontRenderInfo::mock_font("Test font".to_string())),
text_size : 0.0,
base_color : Vector4::new(1.0, 1.0, 1.0, 1.0),
size : Vector2::new(1.0, 1.0)

View File

@ -1,7 +1,7 @@
//! Structures and methods related to single line of TextField content.
use crate::prelude::*;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::glyph::font::FontHandle;
use crate::display::shape::text::glyph::pen::PenIterator;
use nalgebra::Vector2;
@ -81,15 +81,15 @@ impl Line {
#[derive(Debug,Shrinkwrap)]
#[shrinkwrap(mutable)]
#[allow(missing_docs)]
pub struct LineFullInfo<'a,'b> {
pub struct LineFullInfo<'a> {
#[shrinkwrap(main_field)]
pub line : &'a mut Line,
pub line_id : usize,
pub font : &'b mut FontRenderInfo,
pub font : FontHandle,
pub height : f32,
}
impl<'a,'b> LineFullInfo<'a,'b> {
impl<'a> LineFullInfo<'a> {
/// Get the point where a _baseline_ of current line begins (The _baseline_ is a font specific
/// term, for details see [freetype documentation]
/// (https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1)).
@ -145,7 +145,7 @@ impl<'a,'b> LineFullInfo<'a,'b> {
let line = &mut self.line;
let chars = line.chars[from_index..].iter().cloned();
let to_skip = if line.char_x_positions.is_empty() {0} else {1};
let pen = PenIterator::new(start_from,self.height,chars,self.font);
let pen = PenIterator::new(start_from,self.height,chars,self.font.clone_ref());
for (_,position) in pen.skip(to_skip).take(to_fill) {
line.char_x_positions.push(position.x);
@ -174,121 +174,112 @@ impl<'a,'b> LineFullInfo<'a,'b> {
mod test {
use super::*;
use basegl_core_msdf_sys::test_utils::TestAfterInit;
use std::future::Future;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test(async)]
fn getting_chars_x_position() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = prepare_font_with_ab();
let mut line = Line::new("ABA");
let mut line_ref = LineFullInfo {
line : &mut line,
font : &mut font,
line_id : 0,
height : 1.0,
};
async fn getting_chars_x_position() {
basegl_core_msdf_sys::initialized().await;
let mut line = Line::new("ABA");
let mut line_ref = LineFullInfo {
line : &mut line,
font : prepare_font_with_ab(),
line_id : 0,
height : 1.0,
};
assert_eq!(0, line_ref.char_x_positions.len());
let first_pos = line_ref.get_char_x_position(0);
assert_eq!(1, line_ref.char_x_positions.len());
let third_pos = line_ref.get_char_x_position(2);
assert_eq!(3, line_ref.char_x_positions.len());
assert_eq!(0, line_ref.char_x_positions.len());
let first_pos = line_ref.get_char_x_position(0);
assert_eq!(1, line_ref.char_x_positions.len());
let third_pos = line_ref.get_char_x_position(2);
assert_eq!(3, line_ref.char_x_positions.len());
assert_eq!(0.0, first_pos);
assert_eq!(2.5, third_pos);
})
assert_eq!(0.0, first_pos);
assert_eq!(2.5, third_pos);
}
#[wasm_bindgen_test(async)]
fn finding_char_by_x_position() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = prepare_font_with_ab();
let mut line = Line::new("ABBA");
let mut line_ref = LineFullInfo {
line : &mut line,
font : &mut font,
line_id : 0,
height : 1.0,
};
async fn finding_char_by_x_position() {
basegl_core_msdf_sys::initialized().await;
let mut line = Line::new("ABBA");
let mut line_ref = LineFullInfo {
line : &mut line,
font : prepare_font_with_ab(),
line_id : 0,
height : 1.0,
};
let before_first = line_ref.find_char_at_x_position(-0.1);
assert_eq!(1, line_ref.char_x_positions.len());
let first = line_ref.find_char_at_x_position(0.5);
assert_eq!(2, line_ref.char_x_positions.len());
let first_again = line_ref.find_char_at_x_position(0.5);
assert_eq!(2, line_ref.char_x_positions.len());
let third = line_ref.find_char_at_x_position(3.0);
assert_eq!(4, line_ref.char_x_positions.len());
let last = line_ref.find_char_at_x_position(4.5);
assert_eq!(4, line_ref.char_x_positions.len());
let after_last = line_ref.find_char_at_x_position(5.5);
let third_again = line_ref.find_char_at_x_position(3.0);
let before_first_again = line_ref.find_char_at_x_position(-0.5);
let before_first = line_ref.find_char_at_x_position(-0.1);
assert_eq!(1, line_ref.char_x_positions.len());
let first = line_ref.find_char_at_x_position(0.5);
assert_eq!(2, line_ref.char_x_positions.len());
let first_again = line_ref.find_char_at_x_position(0.5);
assert_eq!(2, line_ref.char_x_positions.len());
let third = line_ref.find_char_at_x_position(3.0);
assert_eq!(4, line_ref.char_x_positions.len());
let last = line_ref.find_char_at_x_position(4.5);
assert_eq!(4, line_ref.char_x_positions.len());
let after_last = line_ref.find_char_at_x_position(5.5);
let third_again = line_ref.find_char_at_x_position(3.0);
let before_first_again = line_ref.find_char_at_x_position(-0.5);
assert_eq!(None, before_first);
assert_eq!(Some(0), first);
assert_eq!(Some(0), first_again);
assert_eq!(Some(2), third);
assert_eq!(Some(3), last);
assert_eq!(None, after_last);
assert_eq!(Some(2), third_again);
assert_eq!(None, before_first_again);
})
assert_eq!(None, before_first);
assert_eq!(Some(0), first);
assert_eq!(Some(0), first_again);
assert_eq!(Some(2), third);
assert_eq!(Some(3), last);
assert_eq!(None, after_last);
assert_eq!(Some(2), third_again);
assert_eq!(None, before_first_again);
}
#[wasm_bindgen_test(async)]
fn finding_char_by_x_position_in_empty_line() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = prepare_font_with_ab();
let mut line = Line::new("");
let mut line_ref = LineFullInfo {
line : &mut line,
font : &mut font,
line_id : 0,
height : 1.0,
};
let below_0 = line_ref.find_char_at_x_position(-0.1);
let above_0 = line_ref.find_char_at_x_position( 0.1);
assert_eq!(None,below_0);
assert_eq!(None,above_0);
})
async fn finding_char_by_x_position_in_empty_line() {
basegl_core_msdf_sys::initialized().await;
let mut line = Line::new("");
let mut line_ref = LineFullInfo {
line : &mut line,
font : prepare_font_with_ab(),
line_id : 0,
height : 1.0,
};
let below_0 = line_ref.find_char_at_x_position(-0.1);
let above_0 = line_ref.find_char_at_x_position( 0.1);
assert_eq!(None,below_0);
assert_eq!(None,above_0);
}
#[wasm_bindgen_test(async)]
fn modifying_line() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = prepare_font_with_ab();
let mut line = Line::new("AB");
let mut line_ref = LineFullInfo {
line : &mut line,
font : &mut font,
line_id : 0,
height : 1.0,
};
let before_edit = line_ref.get_char_x_position(1);
assert_eq!(2, line_ref.char_x_positions.len());
line_ref.modify().insert(0, 'B');
assert_eq!(0, line_ref.char_x_positions.len());
let after_edit = line_ref.get_char_x_position(1);
async fn modifying_line() {
basegl_core_msdf_sys::initialized().await;
let mut line = Line::new("AB");
let mut line_ref = LineFullInfo {
line : &mut line,
font : prepare_font_with_ab(),
line_id : 0,
height : 1.0,
};
let before_edit = line_ref.get_char_x_position(1);
assert_eq!(2, line_ref.char_x_positions.len());
line_ref.modify().insert(0, 'B');
assert_eq!(0, line_ref.char_x_positions.len());
let after_edit = line_ref.get_char_x_position(1);
assert_eq!(1.0, before_edit);
assert_eq!(1.5, after_edit);
})
assert_eq!(1.0, before_edit);
assert_eq!(1.5, after_edit);
}
fn prepare_font_with_ab() -> FontRenderInfo {
let mut font = FontRenderInfo::mock_font("Test font".to_string());
let mut a_info = font.mock_char_info('A');
a_info.advance = 1.0;
let mut b_info = font.mock_char_info('B');
b_info.advance = 1.5;
fn prepare_font_with_ab() -> FontHandle {
let font = FontRenderInfo::mock_font("Test font".to_string());
let scale = Vector2::new(1.0, 1.0);
let offset = Vector2::new(0.0, 0.0);
font.mock_char_info('A',scale,offset,1.0);
font.mock_char_info('B',scale,offset,1.5);
font.mock_kerning_info('A', 'B', 0.0);
font.mock_kerning_info('B', 'A', 0.0);
font.mock_kerning_info('A', 'A', 0.0);
font.mock_kerning_info('B', 'B', 0.0);
font
FontHandle::new(font)
}
}

View File

@ -2,8 +2,8 @@
use crate::prelude::*;
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
use crate::display::shape::text::text_field::content::line::LineFullInfo;
use crate::display::shape::text::text_field::content::TextFieldContent;
use crate::display::shape::text::text_field::location::TextLocation;
use nalgebra::Vector2;
@ -11,7 +11,6 @@ use std::cmp::Ordering;
use std::ops::Range;
// ==============
// === Cursor ===
// ==============
@ -33,6 +32,11 @@ impl Cursor {
}
}
/// Returns true if some selection is bound to this cursor.
pub fn has_selection(&self) -> bool {
self.position != self.selected_to
}
/// Get range of selected text by this cursor.
pub fn selection_range(&self) -> Range<TextLocation> {
match self.position.cmp(&self.selected_to) {
@ -59,8 +63,8 @@ impl Cursor {
}
/// Get `LineFullInfo` object of this cursor's line.
pub fn current_line<'a>(&self, content:&'a mut TextFieldContentFullInfo)
-> LineFullInfo<'a,'a> {
pub fn current_line<'a>(&self, content:&'a mut TextFieldContent)
-> LineFullInfo<'a> {
content.line(self.position.line)
}
@ -70,10 +74,7 @@ impl Cursor {
///
/// _Baseline_ is a font specific term, for details see [freetype documentation]
/// (https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1).
pub fn render_position
( position : &TextLocation
, content : &mut TextFieldContentFullInfo
) -> Vector2<f32>{
pub fn render_position(position:&TextLocation, content:&mut TextFieldContent) -> Vector2<f32> {
let line_height = content.line_height;
let mut line = content.line(position.line);
// TODO[ao] this value should be read from font information, but msdf_sys library does
@ -108,15 +109,15 @@ pub enum Step {Left,Right,Up,Down,LineBegin,LineEnd,DocBegin,DocEnd}
/// A struct for cursor navigation process.
#[derive(Debug)]
pub struct CursorNavigation<'a,'b> {
pub struct CursorNavigation<'a> {
/// A reference to text content. This is required to obtain the x positions of chars for proper
/// moving cursors up and down.
pub content : TextFieldContentFullInfo<'a,'b>,
pub content: &'a mut TextFieldContent,
/// Selecting navigation selects/unselects all text between current and new cursor position.
pub selecting : bool
pub selecting: bool
}
impl<'a,'b> CursorNavigation<'a,'b> {
impl<'a> CursorNavigation<'a> {
/// Jump cursor directly to given position.
pub fn move_cursor_to_position(&self, cursor:&mut Cursor, to:TextLocation) {
cursor.position = to;
@ -284,8 +285,19 @@ impl Cursors {
///
/// If after this operation some of the cursors occupies the same position, or their selected
/// area overlap, they are irreversibly merged.
pub fn navigate_all_cursors(&mut self, navigaton:&mut CursorNavigation, step:Step) {
self.cursors.iter_mut().for_each(|cursor| navigaton.move_cursor(cursor,step));
pub fn navigate_all_cursors(&mut self, navigation:&mut CursorNavigation, step:Step) {
self.navigate_cursors(navigation,step,|_| true)
}
/// Do the navigation step of all cursors satisfying given predicate.
///
/// If after this operation some of the cursors occupies the same position, or their selected
/// area overlap, they are irreversibly merged.
pub fn navigate_cursors<Predicate>
(&mut self, navigation:&mut CursorNavigation, step:Step, mut predicate:Predicate)
where Predicate : FnMut(&Cursor) -> bool {
let filtered = self.cursors.iter_mut().filter(|c| predicate(c));
filtered.for_each(|cursor| navigation.move_cursor(cursor, step));
self.merge_overlapping_cursors();
}
@ -363,118 +375,112 @@ mod test {
use super::*;
use Step::*;
use basegl_core_msdf_sys::test_utils::TestAfterInit;
use std::future::Future;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
use crate::display::shape::text::text_field::cursor::Step::{LineBegin, DocBegin, LineEnd};
use crate::display::shape::text::glyph::font::FontRegistry;
use crate::display::shape::text::text_field::content::TextFieldContent;
use crate::display::shape::text::text_field::TextFieldProperties;
use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test(async)]
fn moving_cursors() -> impl Future<Output=()> {
TestAfterInit::schedule(||{
let text = "FirstLine.\nSecondLine\nThirdLine";
let initial_cursors = vec!
[ Cursor::new(TextLocation {line:0, column:0 })
, Cursor::new(TextLocation {line:1, column:0 })
, Cursor::new(TextLocation {line:1, column:6 })
, Cursor::new(TextLocation {line:1, column:10})
, Cursor::new(TextLocation {line:2, column:9 })
];
let mut expected_positions = HashMap::<Step,Vec<(usize,usize)>>::new();
expected_positions.insert(Left, vec![(0,0),(0,10),(1,5),(1,9),(2,8)]);
expected_positions.insert(Right, vec![(0,1),(1,1),(1,7),(2,0),(2,9)]);
expected_positions.insert(Up, vec![(0,0),(0,6),(0,10),(1,9)]);
expected_positions.insert(Down, vec![(1,0),(2,0),(2,6),(2,9)]);
expected_positions.insert(LineBegin, vec![(0,0),(1,0),(2,0)]);
expected_positions.insert(LineEnd, vec![(0,10),(1,10),(2,9)]);
expected_positions.insert(DocBegin, vec![(0,0)]);
expected_positions.insert(DocEnd, vec![(2,9)]);
async fn moving_cursors() {
basegl_core_msdf_sys::initialized().await;
let text = "FirstLine.\nSecondLine\nThirdLine";
let initial_cursors = vec!
[ Cursor::new(TextLocation {line:0, column:0 })
, Cursor::new(TextLocation {line:1, column:0 })
, Cursor::new(TextLocation {line:1, column:6 })
, Cursor::new(TextLocation {line:1, column:10})
, Cursor::new(TextLocation {line:2, column:9 })
];
let mut expected_positions = HashMap::<Step,Vec<(usize,usize)>>::new();
expected_positions.insert(Left, vec![(0,0),(0,10),(1,5),(1,9),(2,8)]);
expected_positions.insert(Right, vec![(0,1),(1,1),(1,7),(2,0),(2,9)]);
expected_positions.insert(Up, vec![(0,0),(0,6),(0,10),(1,9)]);
expected_positions.insert(Down, vec![(1,0),(2,0),(2,6),(2,9)]);
expected_positions.insert(LineBegin, vec![(0,0),(1,0),(2,0)]);
expected_positions.insert(LineEnd, vec![(0,10),(1,10),(2,9)]);
expected_positions.insert(DocBegin, vec![(0,0)]);
expected_positions.insert(DocEnd, vec![(2,9)]);
let mut fonts = FontRegistry::new();
let properties = TextFieldProperties::default(&mut fonts);
let mut content = TextFieldContent::new(text,&properties);
let mut navigation = CursorNavigation {
content: content.full_info(&mut fonts),
selecting: false
};
let mut fonts = FontRegistry::new();
let properties = TextFieldProperties::default(&mut fonts);
let mut content = TextFieldContent::new(text,&properties);
let mut navigation = CursorNavigation {
content: &mut content,
selecting: false
};
for step in &[/*Left,Right,Up,*/Down,/*LineBegin,LineEnd,DocBegin,DocEnd*/] {
let mut cursors = Cursors::mock(initial_cursors.clone());
cursors.navigate_all_cursors(&mut navigation,*step);
let expected = expected_positions.get(step).unwrap();
let current = cursors.cursors.iter().map(|c| (c.position.line, c.position.column));
assert_eq!(expected,&current.collect_vec(), "Error for step {:?}", step);
}
})
}
#[wasm_bindgen_test(async)]
fn moving_without_select() -> impl Future<Output=()> {
TestAfterInit::schedule(||{
let text = "FirstLine\nSecondLine";
let initial_cursor = Cursor {
position : TextLocation {line:1, column:0},
selected_to : TextLocation {line:0, column:1}
};
let initial_cursors = vec![initial_cursor];
let new_position = TextLocation {line:1,column:10};
let mut fonts = FontRegistry::new();
let properties = TextFieldProperties::default(&mut fonts);
let mut content = TextFieldContent::new(text,&properties);
let mut navigation = CursorNavigation {
content: content.full_info(&mut fonts),
selecting: false
};
let mut cursors = Cursors::mock(initial_cursors.clone());
cursors.navigate_all_cursors(&mut navigation,LineEnd);
assert_eq!(new_position, cursors.cursors.first().unwrap().position);
assert_eq!(new_position, cursors.cursors.first().unwrap().selected_to);
})
}
#[wasm_bindgen_test(async)]
fn moving_with_select() -> impl Future<Output=()> {
TestAfterInit::schedule(||{
let text = "FirstLine\nSecondLine";
let initial_loc = TextLocation {line:0,column:1};
let initial_cursors = vec![Cursor::new(initial_loc)];
let new_loc = TextLocation {line:0,column:9};
let mut fonts = FontRegistry::new();
let properties = TextFieldProperties::default(&mut fonts);
let mut content = TextFieldContent::new(text,&properties);
let mut navigation = CursorNavigation {
content: content.full_info(&mut fonts),
selecting: true
};
for step in &[/*Left,Right,Up,*/Down,/*LineBegin,LineEnd,DocBegin,DocEnd*/] {
let mut cursors = Cursors::mock(initial_cursors.clone());
cursors.navigate_all_cursors(&mut navigation,LineEnd);
assert_eq!(new_loc , cursors.cursors.first().unwrap().position);
assert_eq!(initial_loc, cursors.cursors.first().unwrap().selected_to);
})
cursors.navigate_all_cursors(&mut navigation,*step);
let expected = expected_positions.get(step).unwrap();
let current = cursors.cursors.iter().map(|c| (c.position.line, c.position.column));
assert_eq!(expected,&current.collect_vec(), "Error for step {:?}", step);
}
}
#[wasm_bindgen_test(async)]
fn merging_selection_after_moving() -> impl Future<Output=()> {
TestAfterInit::schedule(||{
let make_char_loc = |(line,column):(usize,usize)| TextLocation {line,column};
let cursor_on_left = |range:&Range<(usize,usize)>| Cursor {
position : make_char_loc(range.start),
selected_to : make_char_loc(range.end)
};
let cursor_on_right = |range:&Range<(usize,usize)>| Cursor {
position : make_char_loc(range.end),
selected_to : make_char_loc(range.start)
};
merging_selection_after_moving_case(cursor_on_left);
merging_selection_after_moving_case(cursor_on_right);
})
async fn moving_without_select() {
basegl_core_msdf_sys::initialized().await;
let text = "FirstLine\nSecondLine";
let initial_cursor = Cursor {
position : TextLocation {line:1, column:0},
selected_to : TextLocation {line:0, column:1}
};
let initial_cursors = vec![initial_cursor];
let new_position = TextLocation {line:1,column:10};
let mut fonts = FontRegistry::new();
let properties = TextFieldProperties::default(&mut fonts);
let mut content = TextFieldContent::new(text,&properties);
let mut navigation = CursorNavigation {
content: &mut content,
selecting: false
};
let mut cursors = Cursors::mock(initial_cursors.clone());
cursors.navigate_all_cursors(&mut navigation,LineEnd);
assert_eq!(new_position, cursors.cursors.first().unwrap().position);
assert_eq!(new_position, cursors.cursors.first().unwrap().selected_to);
}
#[wasm_bindgen_test(async)]
async fn moving_with_select() {
basegl_core_msdf_sys::initialized().await;
let text = "FirstLine\nSecondLine";
let initial_loc = TextLocation {line:0,column:1};
let initial_cursors = vec![Cursor::new(initial_loc)];
let new_loc = TextLocation {line:0,column:9};
let mut fonts = FontRegistry::new();
let properties = TextFieldProperties::default(&mut fonts);
let mut content = TextFieldContent::new(text,&properties);
let mut navigation = CursorNavigation {
content: &mut content,
selecting: true
};
let mut cursors = Cursors::mock(initial_cursors.clone());
cursors.navigate_all_cursors(&mut navigation,LineEnd);
assert_eq!(new_loc , cursors.cursors.first().unwrap().position);
assert_eq!(initial_loc, cursors.cursors.first().unwrap().selected_to);
}
#[wasm_bindgen_test(async)]
async fn merging_selection_after_moving() {
basegl_core_msdf_sys::initialized().await;
let make_char_loc = |(line,column):(usize,usize)| TextLocation {line,column};
let cursor_on_left = |range:&Range<(usize,usize)>| Cursor {
position : make_char_loc(range.start),
selected_to : make_char_loc(range.end)
};
let cursor_on_right = |range:&Range<(usize,usize)>| Cursor {
position : make_char_loc(range.end),
selected_to : make_char_loc(range.start)
};
merging_selection_after_moving_case(cursor_on_left);
merging_selection_after_moving_case(cursor_on_right);
}
fn merging_selection_after_moving_case<F>(convert:F)

View File

@ -0,0 +1,195 @@
//! A FRP definitions for keyboard event handling, with biding this FRP graph to js events.
use crate::prelude::*;
use crate::display::shape::text::text_field::cursor::Step;
use crate::display::shape::text::text_field::TextFieldData;
use crate::system::web::text_input::KeyboardBinding;
use enso_frp::*;
use web_sys::KeyboardEvent;
// ====================
// === TextFieldFrp ===
// ====================
/// This structure contains all nodes in FRP graph handling keyboards events of one TextField
/// component.
///
/// The most of TextField actions are covered by providing actions to KeyboardActions for specific
/// key masks. However, there are special actions which must be done in a lower level:
/// * *clipboard operations* - they are performed by reading text input js events directly from
/// text area component. See `system::web::text_input` crate.
/// * *text input operations* - here we want to handle all the keyboard mapping set by user, so
/// we connect this action directly to `key_press` node from `keyboard`.
#[derive(Debug)]
pub struct TextFieldFrp {
/// A "keyboard" part of graph derived from frp crate.
keyboard: Keyboard,
/// Keyboard actions. Here we define shortcuts for all actions except letters input, copying
/// and pasting.
actions: KeyboardActions,
/// Event sent once cut operation was requested.
on_cut: Dynamic<()>,
/// Event sent once copy operation was requested.
on_copy: Dynamic<()>,
/// Event sent once paste operation was requested.
on_paste: Dynamic<String>,
/// A lambda node performing cut operation. Returns the string which should be copied to
/// clipboard.
do_cut: Dynamic<String>,
/// A lambda node performing copy operation. Returns the string which should be copied to
/// clipboard.
do_copy: Dynamic<String>,
/// A lambda node performing paste operation.
do_paste: Dynamic<()>,
/// A lambda node performing character input operation.
do_char_input: Dynamic<()>,
}
impl TextFieldFrp {
/// Create FRP graph operating on given TextField pointer.
pub fn new(text_field_ptr:Weak<RefCell<TextFieldData>>) -> TextFieldFrp {
let keyboard = Keyboard::default();
let mut actions = KeyboardActions::new(&keyboard);
let cut = Self::copy_lambda(true, text_field_ptr.clone());
let copy = Self::copy_lambda(false, text_field_ptr.clone());
let paste = Self::paste_lambda(text_field_ptr.clone());
let insert_char = Self::char_typed_lambda(text_field_ptr.clone());
frp! {
text_field.on_cut = source();
text_field.on_copy = source();
text_field.on_paste = source();
text_field.do_copy = on_copy .map(move |()| copy());
text_field.do_cut = on_cut .map(move |()| cut());
text_field.do_paste = on_paste.map(paste);
text_field.do_char_input = keyboard.on_pressed.map2(&keyboard.key_mask,insert_char);
}
Self::initialize_actions_map(&mut actions,text_field_ptr);
TextFieldFrp {keyboard,actions,on_cut,on_copy,on_paste,do_cut,do_copy,do_paste,
do_char_input}
}
/// Bind this FRP graph to js events.
///
/// Until the returned `KeyboardBinding` structure lives, the js events will emit the proper
/// source events in this graph.
pub fn bind_frp_to_js_text_input_actions(&self) -> KeyboardBinding {
let mut binding = KeyboardBinding::create();
let frp_key_pressed = self.keyboard.on_pressed.clone_ref();
let frp_key_released = self.keyboard.on_released.clone_ref();
let frp_cut = self.on_cut.clone_ref();
let frp_copy = self.on_copy.clone_ref();
let frp_paste = self.on_paste.clone_ref();
let frp_text_to_copy = self.do_copy.clone_ref();
binding.set_key_down_handler(move |event:KeyboardEvent| {
if let Ok(key) = event.key().parse::<Key>() {
frp_key_pressed.event.emit(key);
}
});
binding.set_key_up_handler(move |event:KeyboardEvent| {
if let Ok(key) = event.key().parse::<Key>() {
frp_key_released.event.emit(key);
}
});
binding.set_copy_handler(move |is_cut| {
if is_cut {
frp_cut.event.emit(())
} else {
frp_copy.event.emit(());
}
frp_text_to_copy.behavior.current_value()
});
binding.set_paste_handler(move |text_to_paste| {
frp_paste.event.emit(text_to_paste);
});
binding
}
}
// === Private ===
impl TextFieldFrp {
fn copy_lambda(cut:bool, text_field_ptr:Weak<RefCell<TextFieldData>>)
-> impl Fn() -> String {
move || {
text_field_ptr.upgrade().map_or(default(),|text_field| {
let mut text_field_ref = text_field.borrow_mut();
let result = text_field_ref.get_selected_text();
if cut { text_field_ref.remove_selection(); }
result
})
}
}
fn paste_lambda(text_field_ptr:Weak<RefCell<TextFieldData>>) -> impl Fn(&String) {
move |text_to_paste| {
let inserted = text_to_paste.as_str();
text_field_ptr.upgrade().for_each(|tf| { tf.borrow_mut().write(inserted) })
}
}
fn char_typed_lambda(text_field_ptr:Weak<RefCell<TextFieldData>>) -> impl Fn(&Key,&KeyMask) {
move |key,mask| {
text_field_ptr.upgrade().for_each(|text_field| {
if let Key::Character(string) = key {
let modifiers = &[Key::Control,Key::Alt,Key::Meta];
if !modifiers.iter().any(|k| mask.has_key(k)) {
text_field.borrow_mut().write(string);
}
}
})
}
}
fn initialize_actions_map
(actions:&mut KeyboardActions, text_field_ptr:Weak<RefCell<TextFieldData>>) {
use Key::*;
let mut setter = TextFieldActionsSetter{actions,text_field_ptr};
setter.set_navigation_action(&[ArrowLeft], Step::Left);
setter.set_navigation_action(&[ArrowRight], Step::Right);
setter.set_navigation_action(&[ArrowUp], Step::Up);
setter.set_navigation_action(&[ArrowDown], Step::Down);
setter.set_navigation_action(&[Home], Step::LineBegin);
setter.set_navigation_action(&[End], Step::LineEnd);
setter.set_navigation_action(&[Control,Home], Step::DocBegin);
setter.set_navigation_action(&[Control,End], Step::DocEnd);
setter.set_action(&[Enter], |t| t.write("\n"));
setter.set_action(&[Delete], |t| t.do_delete_operation(Step::Right));
setter.set_action(&[Backspace], |t| t.do_delete_operation(Step::Left));
}
}
// === Private Utilities ===
/// An utility struct for setting actions in text field. See `initialize_actions_map` function
/// for its usage.
struct TextFieldActionsSetter<'a> {
text_field_ptr: Weak<RefCell<TextFieldData>>,
actions : &'a mut KeyboardActions,
}
impl<'a> TextFieldActionsSetter<'a> {
fn set_action<F>(&mut self, keys:&[Key], action:F)
where F : Fn(&mut TextFieldData) + 'static {
let ptr = self.text_field_ptr.clone();
self.actions.set_action(keys.into(), move |_| {
if let Some(ptr) = ptr.upgrade() {
let mut text_field_ref = ptr.borrow_mut();
action(&mut text_field_ref);
}
});
}
fn set_navigation_action(&mut self, base_keys:&[Key], step:Step) {
self.set_action(base_keys, move |t| t.navigate_cursors(step,false));
let base_keys_cloned = base_keys.iter().cloned();
let selecting_keys = base_keys_cloned.chain(std::iter::once(Key::Shift)).collect_vec();
self.set_action(selecting_keys.as_ref(), move |t| t.navigate_cursors(step,true));
}
}

View File

@ -6,11 +6,9 @@ pub mod selection;
use crate::prelude::*;
use crate::display::object::DisplayObjectData;
use crate::display::shape::text::glyph::font::FontRegistry;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::glyph::font::FontHandle;
use crate::display::shape::text::glyph::system::GlyphSystem;
use crate::display::shape::text::text_field::content::TextFieldContent;
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
use crate::display::shape::text::text_field::cursor::Cursor;
use crate::display::shape::text::text_field::cursor::Cursors;
use crate::display::shape::text::text_field::render::assignment::GlyphLinesAssignment;
@ -71,15 +69,15 @@ pub struct TextFieldSprites {
impl TextFieldSprites {
/// Create RenderedContent structure.
pub fn new(world:&World, properties:&TextFieldProperties, fonts:&mut FontRegistry) -> Self {
pub fn new(world:&World, properties:&TextFieldProperties) -> Self {
let font = properties.font.clone_ref();
let line_height = properties.text_size;
let window_size = properties.size;
let color = properties.base_color;
let font = fonts.get_render_info(properties.font_id);
let cursor_system = Self::create_cursor_system(world,line_height);
let selection_system = Self::create_selection_system(world);
let cursors = Vec::new();
let mut glyph_system = GlyphSystem::new(world,properties.font_id);
let mut glyph_system = GlyphSystem::new(world,font.clone_ref());
let display_object = DisplayObjectData::new(Logger::new("RenderedContent"));
display_object.add_child(&selection_system);
display_object.add_child(&glyph_system);
@ -115,7 +113,7 @@ impl TextFieldSprites {
fn create_assignment_structure
( window_size : Vector2<f32>
, line_height : f32
, font : &mut FontRenderInfo
, font : FontHandle
) -> GlyphLinesAssignment {
// Display_size.(x/y).floor() makes space for all lines/glyph that fit in space in
// their full size. But we have 2 more lines/glyph: one clipped from top or left, and one
@ -146,7 +144,7 @@ impl From<&TextFieldSprites> for DisplayObjectData {
impl TextFieldSprites {
/// Update all displayed glyphs.
pub fn update_glyphs(&mut self, content:&mut TextFieldContent, fonts:&mut FontRegistry) {
pub fn update_glyphs(&mut self, content:&mut TextFieldContent) {
let glyph_lines = self.glyph_lines.iter_mut().enumerate();
let lines_assignment = glyph_lines.zip(self.assignment.glyph_lines_fragments.iter());
let dirty_lines = std::mem::take(&mut content.dirty_lines);
@ -158,8 +156,8 @@ impl TextFieldSprites {
let is_line_dirty = assigned_line.map_or(false, |l| dirty_lines.is_dirty(l));
if is_glyph_line_dirty || is_line_dirty {
match assignment {
Some(fragment) => Self::update_glyph_line(glyph_line,fragment,content,fonts),
None => glyph_line.replace_text("".chars(), fonts),
Some(fragment) => Self::update_glyph_line(glyph_line,fragment,content),
None => glyph_line.replace_text("".chars()),
}
}
}
@ -167,8 +165,7 @@ impl TextFieldSprites {
}
/// Update all displayed cursors with their selections.
pub fn update_cursor_sprites
(&mut self, cursors:&Cursors, content:&mut TextFieldContentFullInfo) {
pub fn update_cursor_sprites(&mut self, cursors:&Cursors, content:&mut TextFieldContent) {
let cursor_system = &self.cursor_system;
self.cursors.resize_with(cursors.cursors.len(),|| Self::new_cursor_sprites(cursor_system));
for (sprites,cursor) in self.cursors.iter_mut().zip(cursors.cursors.iter()) {
@ -187,16 +184,12 @@ impl TextFieldSprites {
}
fn update_glyph_line
( glyph_line : &mut GlyphLine
, fragment : &LineFragment
, content : &mut TextFieldContent
, fonts : &mut FontRegistry) {
let mut f_content = content.full_info(fonts);
let bsl_start = Self::baseline_start_for_fragment(fragment,&mut f_content);
(glyph_line:&mut GlyphLine, fragment:&LineFragment, content:&mut TextFieldContent) {
let bsl_start = Self::baseline_start_for_fragment(fragment,content);
let line = &content.lines[fragment.line_index];
let chars = &line.chars()[fragment.chars_range.clone()];
glyph_line.set_baseline_start(bsl_start);
glyph_line.replace_text(chars.iter().cloned(),fonts);
glyph_line.replace_text(chars.iter().cloned());
}
/// The baseline start for given line's fragment.
@ -204,7 +197,7 @@ impl TextFieldSprites {
/// Because we're not rendering the whole lines, but only visible fragment of it (with some
/// margin), the baseline used for placing glyph don't start on the line begin, but at the
/// position of first char of fragment.
fn baseline_start_for_fragment(fragment:&LineFragment, content:&mut TextFieldContentFullInfo)
fn baseline_start_for_fragment(fragment:&LineFragment, content:&mut TextFieldContent)
-> Vector2<f32> {
let mut line = content.line(fragment.line_index);
if fragment.chars_range.start >= line.chars().len() {

View File

@ -5,7 +5,7 @@
use crate::prelude::*;
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
use crate::display::shape::text::text_field::content::TextFieldContent;
use nalgebra::Vector2;
use std::ops::Range;
@ -28,10 +28,7 @@ pub struct LineFragment {
impl LineFragment {
/// Tells if rendering this line's fragment will cover the x range.
pub fn covers_displayed_range
( &self
, displayed_range : &RangeInclusive<f32>
, content : &mut TextFieldContentFullInfo
) -> bool {
(&self, displayed_range:&RangeInclusive<f32>, content:&mut TextFieldContent) -> bool {
let mut line = content.line(self.line_index);
let front_rendered = self.chars_range.start == 0;
let back_rendered = self.chars_range.end == line.len();
@ -101,18 +98,18 @@ impl GlyphLinesAssignment {
/// A helper structure for making assignment updates. It takes references to GlyphLinesAssignment
/// structure and all required data to make proper reassignments.
#[derive(Debug)]
pub struct GlyphLinesAssignmentUpdate<'a,'b,'c> {
pub struct GlyphLinesAssignmentUpdate<'a,'b> {
/// A reference to assignment structure.
pub assignment: &'a mut GlyphLinesAssignment,
/// A reference to TextField content.
pub content: TextFieldContentFullInfo<'b,'c>,
pub content: &'b mut TextFieldContent,
/// Current scroll offset in pixels.
pub scroll_offset: Vector2<f32>,
/// Current view size in pixels.
pub view_size: Vector2<f32>,
}
impl<'a,'b,'c> GlyphLinesAssignmentUpdate<'a,'b,'c> {
impl<'a,'b> GlyphLinesAssignmentUpdate<'a,'b> {
/// Reassign _glyph line_ to currently displayed fragment of line.
pub fn reassign(&mut self, glyph_line_id:usize, line_id:usize) {
let fragment = self.displayed_fragment(line_id);
@ -165,17 +162,17 @@ impl<'a,'b,'c> GlyphLinesAssignmentUpdate<'a,'b,'c> {
/// Some new lines could be created after edit, and some lines can be longer, what should be
/// reflected in assigned fragments.
pub fn update_after_text_edit(&mut self) {
let dirty_lines = std::mem::take(&mut self.content.dirty_lines);
if self.content.dirty_lines.range.is_some() {
self.update_line_assignment();
}
for i in 0..self.assignment.glyph_lines_fragments.len() {
let assigned_fragment = &self.assignment.glyph_lines_fragments[i];
let assigned_line = assigned_fragment.as_ref().map(|f| f.line_index);
match assigned_line {
Some(line) if line >= self.content.lines.len() => self.unassign(i),
Some(line) if dirty_lines.is_dirty(line) => self.reassign(i,line),
_ => {},
Some(line) if line >= self.content.lines.len() => self.unassign(i),
Some(line) if self.content.dirty_lines.is_dirty(line) => self.reassign(i,line),
_ => {},
}
}
}
@ -204,7 +201,7 @@ impl GlyphLinesAssignment {
}
}
impl<'a,'b,'c> GlyphLinesAssignmentUpdate<'a,'b,'c> {
impl<'a,'b> GlyphLinesAssignmentUpdate<'a,'b> {
/// Returns LineFragment of specific line which is currently visible.
fn displayed_fragment(&mut self, line_id:usize) -> LineFragment {
let mut line = self.content.line(line_id);
@ -230,6 +227,7 @@ impl<'a,'b,'c> GlyphLinesAssignmentUpdate<'a,'b,'c> {
fn new_assignment(&mut self) -> RangeInclusive<usize> {
let visible_lines = self.visible_lines_range();
let assigned_lines = &self.assignment.assigned_lines;
let max_line_id = self.content.lines.len().saturating_sub(1);
let lines_count = |r:&RangeInclusive<usize>| r.end() + 1 - r.start();
let assigned_lines_count = lines_count(assigned_lines);
let displayed_lines_count = lines_count(&visible_lines);
@ -238,7 +236,7 @@ impl<'a,'b,'c> GlyphLinesAssignmentUpdate<'a,'b,'c> {
let new_start = visible_lines.start() - hidden_lines_to_keep;
new_start..=*visible_lines.end()
} else if assigned_lines.end() > visible_lines.end() {
let new_end = visible_lines.end() + hidden_lines_to_keep;
let new_end = (visible_lines.end() + hidden_lines_to_keep).min(max_line_id);
*visible_lines.start()..=new_end
} else {
visible_lines
@ -287,196 +285,205 @@ mod tests {
use super::*;
use crate::display::shape::text::glyph::font::FontRenderInfo;
use crate::display::shape::text::glyph::font::FontHandle;
use crate::display::shape::text::text_field::content::TextFieldContent;
use crate::display::shape::text::text_field::content::line::Line;
use crate::display::shape::text::text_field::TextFieldProperties;
use basegl_core_msdf_sys::test_utils::TestAfterInit;
use nalgebra::Vector4;
use std::future::Future;
use wasm_bindgen_test::wasm_bindgen_test;
fn mock_properties() -> TextFieldProperties {
TextFieldProperties {
font_id : 0,
font : mock_font(),
text_size : 10.0,
base_color : Vector4::new(0.0,0.0,0.0,1.0),
size : Vector2::new(20.0,35.0),
}
}
fn mock_font() -> FontRenderInfo {
let mut font = FontRenderInfo::mock_font("Test".to_string());
let mut a_info = font.mock_char_info('A');
a_info.advance = 1.0;
let mut b_info = font.mock_char_info('B');
b_info.advance = 1.5;
fn mock_font() -> FontHandle {
let font = FontRenderInfo::mock_font("Test".to_string());
let scale = Vector2::new(1.0, 1.0);
let offset = Vector2::new(0.0, 0.0);
font.mock_char_info('A',scale,offset,1.0);
font.mock_char_info('B',scale,offset,1.5);
font.mock_kerning_info('A', 'A', 0.0);
font.mock_kerning_info('B', 'B', 0.0);
font.mock_kerning_info('A', 'B', 0.0);
font.mock_kerning_info('B', 'A', 0.0);
font
FontHandle::new(font)
}
#[wasm_bindgen_test(async)]
fn initial_assignment() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = mock_font();
let properties = mock_properties();
let mut content = TextFieldContent::new("AAABBB\nBAABAB\n\nA\nA",&properties);
async fn initial_assignment() {
basegl_core_msdf_sys::initialized().await;
let properties = mock_properties();
let mut content = TextFieldContent::new("AAABBB\nBAABAB\n\nA\nA",&properties);
let mut assignment = GlyphLinesAssignment::new(4, 4, 10.0);
let mut assignment = GlyphLinesAssignment::new(4, 4, 10.0);
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : TextFieldContentFullInfo {content:&mut content, font:&mut font},
scroll_offset : Vector2::new(22.0,0.0),
view_size : properties.size
};
update.update_line_assignment();
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let expected_dirties : HashSet<usize> = [0,1,2,3].iter().cloned().collect();
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : &mut content,
scroll_offset : Vector2::new(22.0,0.0),
view_size : properties.size
};
update.update_line_assignment();
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let expected_dirties : HashSet<usize> = [0,1,2,3].iter().cloned().collect();
assert_eq!(expected_fragments, assignment.glyph_lines_fragments);
assert_eq!(expected_dirties, assignment.dirty_glyph_lines);
})
assert_eq!(expected_fragments, assignment.glyph_lines_fragments);
assert_eq!(expected_dirties, assignment.dirty_glyph_lines);
}
#[wasm_bindgen_test(async)]
fn lines_reassignment() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = mock_font();
let properties = mock_properties();
let mut content = TextFieldContent::new("AAABBB\nBAABAB\n\nA\nA\nAB",&properties);
async fn lines_reassignment() {
basegl_core_msdf_sys::initialized().await;
let properties = mock_properties();
let mut content = TextFieldContent::new("AAABBB\nBAABAB\n\nA\nA\nAB",&properties);
let mut assignment = GlyphLinesAssignment::new(4, 4, 10.0);
assignment.glyph_lines_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let assigned_lines = 0..=3;
assignment.assigned_lines = assigned_lines;
// This line casues false warning about unnecessary parentheses
// assignment.assigned_lines = 0..3;
let mut assignment = GlyphLinesAssignment::new(4, 4, 10.0);
assignment.glyph_lines_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let assigned_lines = 0..=3;
assignment.assigned_lines = assigned_lines;
// This line casues false warning about unnecessary parentheses
// assignment.assigned_lines = 0..3;
// scrolling down
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : TextFieldContentFullInfo {content:&mut content, font:&mut font},
scroll_offset : Vector2::new(22.0,-21.0),
view_size : properties.size
};
update.update_line_assignment();
let expected_fragments = vec!
[ Some(LineFragment{line_index:4, chars_range: 0..1})
, Some(LineFragment{line_index:5, chars_range: 0..2})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let expected_dirties : HashSet<usize> = [0,1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
assert_eq!(2..=5 , update.assignment.assigned_lines);
// scrolling down
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : &mut content,
scroll_offset : Vector2::new(22.0,-21.0),
view_size : properties.size
};
update.update_line_assignment();
let expected_fragments = vec!
[ Some(LineFragment{line_index:4, chars_range: 0..1})
, Some(LineFragment{line_index:5, chars_range: 0..2})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let expected_dirties : HashSet<usize> = [0,1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
assert_eq!(2..=5 , update.assignment.assigned_lines);
// scrolling up.
update.assignment.dirty_glyph_lines.clear();
update.scroll_offset = Vector2::new(22.0,-11.0);
update.update_line_assignment();
let expected_fragments = vec!
[ Some(LineFragment{line_index:4, chars_range: 0..1})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let expected_dirties : HashSet<usize> = [1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
assert_eq!(1..=4 , update.assignment.assigned_lines);
})
// scrolling up.
update.assignment.dirty_glyph_lines.clear();
update.scroll_offset = Vector2::new(22.0,-11.0);
update.update_line_assignment();
let expected_fragments = vec!
[ Some(LineFragment{line_index:4, chars_range: 0..1})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 0..0})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
let expected_dirties : HashSet<usize> = [1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
assert_eq!(1..=4 , update.assignment.assigned_lines);
}
#[wasm_bindgen_test(async)]
fn marking_dirty_after_x_scrolling() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = mock_font();
let properties = mock_properties();
let mut content = TextFieldContent::new("AAABBB\nBAABAB\nBBABAB\nA\nA",&properties);
async fn marking_dirty_after_x_scrolling() {
basegl_core_msdf_sys::initialized().await;
let properties = mock_properties();
let mut content = TextFieldContent::new("AAABBB\nBAABAB\nBBABAB\nA\nA",&properties);
let mut assignment = GlyphLinesAssignment::new(4, 4, 10.0);
assignment.glyph_lines_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 1..5})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
assignment.next_glyph_line_to_x_scroll_update = 3;
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : TextFieldContentFullInfo {content:&mut content, font:&mut font},
scroll_offset : Vector2::new(42.0,-21.0),
view_size : properties.size
};
update.update_after_x_scroll(15.0);
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 2..6})
, Some(LineFragment{line_index:1, chars_range: 2..6})
, Some(LineFragment{line_index:2, chars_range: 1..5})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(1 , update.assignment.next_glyph_line_to_x_scroll_update);
})
let mut assignment = GlyphLinesAssignment::new(4, 4, 10.0);
assignment.glyph_lines_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, Some(LineFragment{line_index:2, chars_range: 1..5})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
assignment.next_glyph_line_to_x_scroll_update = 3;
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : &mut content,
scroll_offset : Vector2::new(42.0,-21.0),
view_size : properties.size
};
update.update_after_x_scroll(15.0);
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 2..6})
, Some(LineFragment{line_index:1, chars_range: 2..6})
, Some(LineFragment{line_index:2, chars_range: 1..5})
, Some(LineFragment{line_index:3, chars_range: 0..1})
];
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(1 , update.assignment.next_glyph_line_to_x_scroll_update);
}
#[wasm_bindgen_test(async)]
fn update_after_text_edit() -> impl Future<Output=()> {
TestAfterInit::schedule(|| {
let mut font = mock_font();
let properties = mock_properties();
async fn update_after_text_edit() {
basegl_core_msdf_sys::initialized().await;
let properties = mock_properties();
let mut assignment = GlyphLinesAssignment::new(3, 4, 10.0);
assignment.glyph_lines_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, None
];
let mut content = TextFieldContent::new("AAABBB\nBA",&properties);
content.dirty_lines.add_single_line(1);
let mut assignment = GlyphLinesAssignment::new(3, 4, 10.0);
assignment.glyph_lines_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..4})
, None
];
assignment.assigned_lines = 0..=1;
let mut content = TextFieldContent::new("AAABBB\nBA",&properties);
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : &mut content,
scroll_offset : Vector2::new(22.0,0.0),
view_size : properties.size
};
let mut update = GlyphLinesAssignmentUpdate {
assignment : &mut assignment,
content : TextFieldContentFullInfo {content:&mut content, font:&mut font},
scroll_offset : Vector2::new(22.0,0.0),
view_size : properties.size
};
update.update_after_text_edit();
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..2})
, None
];
let expected_dirties : HashSet<usize> = [1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
// Editing line:
update.content.dirty_lines.add_single_line(1);
update.update_after_text_edit();
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 0..2})
, None
];
let expected_dirties:HashSet<usize> = [1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
update.assignment.dirty_glyph_lines.clear();
update.content.content.lines.pop();
update.content.content.dirty_lines.add_lines_range_from(1..);
update.update_after_text_edit();
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, None
, None
];
let expected_dirties : HashSet<usize> = [1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
})
// Removing line:
update.assignment.dirty_glyph_lines.clear();
update.content.lines.pop();
update.content.dirty_lines.add_lines_range_from(1..);
update.update_after_text_edit();
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, None
, None
];
let expected_dirties:HashSet<usize> = [1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
// Adding line:
update.assignment.dirty_glyph_lines.clear();
update.content.lines.push(Line::new("AAAAAAAAAAAA".to_string()));
update.content.dirty_lines.add_lines_range_from(1..);
update.update_after_text_edit();
let expected_fragments = vec!
[ Some(LineFragment{line_index:0, chars_range: 1..5})
, Some(LineFragment{line_index:1, chars_range: 1..5})
, None
];
let expected_dirties : HashSet<usize> = [1].iter().cloned().collect();
assert_eq!(expected_fragments, update.assignment.glyph_lines_fragments);
assert_eq!(expected_dirties , update.assignment.dirty_glyph_lines);
}
}

View File

@ -1,7 +1,7 @@
//! Drawing selection utilities.
use crate::display::shape::primitive::system::ShapeSystem;
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
use crate::display::shape::text::text_field::content::TextFieldContent;
use crate::display::shape::text::text_field::cursor::Cursor;
use crate::display::shape::text::text_field::location::TextLocation;
use crate::display::symbol::geometry::compound::sprite::Sprite;
@ -19,13 +19,13 @@ use std::ops::Range;
/// A helper structure for generating selection sprites.
#[derive(Debug)]
#[allow(missing_docs)]
pub struct SelectionSpritesGenerator<'a,'b,'c,'d> {
pub struct SelectionSpritesGenerator<'a,'b> {
pub line_height : f32,
pub system : &'a ShapeSystem,
pub content : &'b mut TextFieldContentFullInfo<'c,'d>,
pub content : &'b mut TextFieldContent,
}
impl<'a,'b,'c,'d> SelectionSpritesGenerator<'a,'b,'c,'d> {
impl<'a,'b> SelectionSpritesGenerator<'a,'b> {
/// Generate sprites for given selection.
pub fn generate(&mut self, selection : &Range<TextLocation>) -> Vec<Sprite> {
let mut return_value = Vec::new();

View File

@ -10,3 +10,7 @@ edition = "2018"
enso-prelude = { version = "0.1.0" , path = "../prelude" }
basegl-system-web = { version = "0.1.0" , path = "../system/web" }
percent-encoding = { version = "2.1.0" }
rust-dense-bitset = "0.1.1"
#TODO [ao] replace with official version once this will be merged and released:
#https://github.com/pyfisch/keyboard-types/pull/4
keyboard-types = { git = "https://github.com/farmaazon/keyboard-types" }

View File

@ -1,5 +1,7 @@
//! Root module for Input / Output FRP bindings
pub mod mouse;
pub mod keyboard;
pub use mouse::*;
pub use keyboard::*;

View File

@ -0,0 +1,259 @@
//! Keboard FRP bindings.
use crate::prelude::*;
use crate::*;
use rust_dense_bitset::BitSet;
use rust_dense_bitset::DenseBitSetExtended;
use std::collections::hash_map::Entry;
use crate::core::fmt::{Formatter, Error};
// ===========
// === Key ===
// ===========
/// A key representation.
pub use keyboard_types::Key;
// ===============
// === KeyMask ===
// ===============
/// The assumed maximum key code, used as size of KeyMask bitset.
const MAX_KEY_CODE : usize = 255;
/// The key bitmask - each bit represents one key. Used for matching key combinations.
#[derive(BitXor,Clone,Debug,Eq,Hash,PartialEq,Shrinkwrap)]
#[shrinkwrap(mutable)]
pub struct KeyMask(pub DenseBitSetExtended);
impl KeyMask {
/// Check if key bit is on.
pub fn has_key(&self, key:&Key) -> bool {
let KeyMask(bit_set) = self;
bit_set.get_bit(key.legacy_keycode() as usize)
}
}
impl Default for KeyMask {
fn default() -> Self {
let mut bitset = DenseBitSetExtended::with_capacity(MAX_KEY_CODE + 1);
// This is the only way to set bitset length.
bitset.set_bit(MAX_KEY_CODE,true);
bitset.set_bit(MAX_KEY_CODE,false);
Self(bitset)
}
}
impl<'a> FromIterator<&'a Key> for KeyMask {
fn from_iter<T: IntoIterator<Item=&'a Key>>(iter:T) -> Self {
let mut key_mask = KeyMask::default();
for key in iter {
let bit = key.legacy_keycode() as usize;
key_mask.set_bit(bit,true);
}
key_mask
}
}
impl From<&[Key]> for KeyMask {
fn from(keys: &[Key]) -> Self {
<KeyMask as FromIterator<&Key>>::from_iter(keys)
}
}
// ================
// === KeyState ===
// ================
/// A helper structure used for describing specific key state.
#[derive(Clone,Debug,Default)]
struct KeyState {
key : Key,
pressed : bool,
}
impl KeyState {
/// Create _pressed_ state for given key.
fn key_pressed(key:&Key) -> Self {
let pressed = true;
let key = key.clone();
KeyState{key,pressed}
}
/// Create _released_ state for given key.
fn key_released(key:&Key) -> Self {
let pressed = false;
let key = key.clone();
KeyState{key,pressed}
}
/// Returns copy of given KeyMask with updated key state.
fn updated_mask(&self, mask:&KeyMask) -> KeyMask {
let mut mask = mask.clone();
let bit = self.key.legacy_keycode() as usize;
mask.set_bit(bit,self.pressed);
mask
}
}
// ================
// === Keyboard ===
// ================
/// A FRP graph for basic keyboard events.
#[derive(Debug)]
pub struct Keyboard {
/// The mouse up event.
pub on_pressed: Dynamic<Key>,
/// The mouse down event.
pub on_released: Dynamic<Key>,
/// The structure holding mask of all of the currently pressed keys.
pub key_mask: Dynamic<KeyMask>,
}
impl Default for Keyboard {
fn default() -> Self {
frp! {
keyboard.on_pressed = source();
keyboard.on_released = source();
keyboard.pressed_state = on_pressed.map(KeyState::key_pressed);
keyboard.released_state = on_released.map(KeyState::key_released);
keyboard.key_state = pressed_state.merge(&released_state);
keyboard.previous_key_mask = recursive::<KeyMask>();
keyboard.key_mask = key_state.map2(&previous_key_mask,KeyState::updated_mask);
}
previous_key_mask.initialize(&key_mask);
Keyboard { on_pressed,on_released,key_mask}
}
}
// =======================
// === KeyboardActions ===
// =======================
/// An action defined for specific key combinations. For convenience, the key mask is passed as
/// argument.
pub trait Action = FnMut(&KeyMask) + 'static;
/// A mapping between key combinations and actions.
pub type ActionMap = HashMap<KeyMask,Box<dyn Action>>;
/// A structure bound to Keyboard FRP graph, which allows to define actions for specific keystrokes.
pub struct KeyboardActions {
action_map : Rc<RefCell<ActionMap>>,
_action : Dynamic<()>,
}
impl KeyboardActions {
/// Create structure without any actions defined yet. It will be listening for events from
/// passed `Keyboard` structure.
pub fn new(keyboard:&Keyboard) -> Self {
let action_map = Rc::new(RefCell::new(HashMap::new()));
frp! {
keyboard.action = keyboard.key_mask.map(Self::perform_action_lambda(action_map.clone()));
}
KeyboardActions{action_map, _action:action}
}
fn perform_action_lambda(action_map:Rc<RefCell<ActionMap>>) -> impl Fn(&KeyMask) {
move |key_mask| {
let entry_opt = with(action_map.borrow_mut(), |mut map| map.remove_entry(key_mask));
if let Some((map_mask, mut action)) = entry_opt {
action(key_mask);
if let Entry::Vacant(entry) = action_map.borrow_mut().entry(map_mask) {
entry.insert(action);
}
}
}
}
/// Set action binding for given key mask.
pub fn set_action<F:FnMut(&KeyMask) + 'static>(&mut self, key_mask:KeyMask, action:F) {
self.action_map.borrow_mut().insert(key_mask,Box::new(action));
}
/// Remove action binding for given key mask.
pub fn unset_action(&mut self, key_mask:&KeyMask) {
self.action_map.borrow_mut().remove(key_mask);
}
}
impl Debug for KeyboardActions {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
write!(f, "<KeyboardActions>")
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn key_mask() {
let keyboard = Keyboard::default();
let expected_key_mask:KeyMask = default();
assert_eq!(expected_key_mask, keyboard.key_mask.behavior.current_value());
let key1 = Key::Character("x".to_string());
let key2 = Key::Control;
keyboard.on_pressed.event.emit(key1.clone());
let expected_key_mask:KeyMask = std::iter::once(&key1).collect();
assert_eq!(expected_key_mask, keyboard.key_mask.behavior.current_value());
keyboard.on_pressed.event.emit(key2.clone());
let expected_key_mask:KeyMask = [&key1,&key2].iter().cloned().collect();
assert_eq!(expected_key_mask, keyboard.key_mask.behavior.current_value());
keyboard.on_released.event.emit(key1.clone());
let expected_key_mask:KeyMask = std::iter::once(&key2).collect();
assert_eq!(expected_key_mask, keyboard.key_mask.behavior.current_value());
}
#[test]
fn key_actions() {
use keyboard_types::Key::*;
let undone = Rc::new(RefCell::new(false));
let undone1 = undone.clone();
let redone = Rc::new(RefCell::new(false));
let redone1 = redone.clone();
let undo_keys:KeyMask = [Control, Character("z".to_string())].iter().collect();
let redo_keys:KeyMask = [Control, Character("y".to_string())].iter().collect();
let keyboard = Keyboard::default();
let mut actions = KeyboardActions::new(&keyboard);
actions.set_action(undo_keys.clone(), move |_| { *undone1.borrow_mut() = true });
actions.set_action(redo_keys.clone(), move |_| { *redone1.borrow_mut() = true });
keyboard.on_pressed.event.emit(Character("Z".to_string()));
assert!(!*undone.borrow());
assert!(!*redone.borrow());
keyboard.on_pressed.event.emit(Control);
assert!( *undone.borrow());
assert!(!*redone.borrow());
*undone.borrow_mut() = false;
keyboard.on_released.event.emit(Character("z".to_string()));
assert!(!*undone.borrow());
assert!(!*redone.borrow());
keyboard.on_pressed.event.emit(Character("y".to_string()));
assert!(!*undone.borrow());
assert!( *redone.borrow());
*redone.borrow_mut() = false;
keyboard.on_released.event.emit(Character("y".to_string()));
keyboard.on_released.event.emit(Control);
actions.unset_action(&undo_keys);
keyboard.on_pressed.event.emit(Character("Z".to_string()));
keyboard.on_pressed.event.emit(Control);
assert!(!*undone.borrow());
assert!(!*redone.borrow());
}
}

View File

@ -23,13 +23,13 @@ pub fn run_example_glyph_system() {
fn init(world: &World) {
let mut fonts = FontRegistry::new();
let font_id = fonts.load_embedded_font("DejaVuSans").unwrap();
let mut glyph_system = GlyphSystem::new(world,font_id);
let font = fonts.get_or_load_embedded_font("DejaVuSans").unwrap();
let mut glyph_system = GlyphSystem::new(world,font);
let line_position = Vector2::new(100.0, 100.0);
let height = 32.0;
let color = Vector4::new(0.0, 0.8, 0.0, 1.0);
let text = "Follow the white rabbit...";
let line = glyph_system.new_line(line_position,height,text,color,&mut fonts);
let line = glyph_system.new_line(line_position,height,text,color);
world.add_child(glyph_system.sprite_system());
world.on_frame(move |_| {

View File

@ -22,7 +22,7 @@ pub mod easing_animator;
pub mod glyph_system;
pub mod shapes;
pub mod sprite_system;
pub mod text_selecting;
pub mod text_field;
pub mod text_typing;
use enso_prelude as prelude;

View File

@ -2,25 +2,23 @@
use crate::prelude::*;
use basegl::display::world::WorldData;
use basegl::display::object::DisplayObjectOps;
use basegl::display::shape::text::glyph::font::FontRegistry;
use basegl::display::shape::text::text_field::location::TextLocation;
use basegl::display::shape::text::text_field::TextField;
use basegl::display::shape::text::text_field::TextFieldProperties;
use basegl::display::world::*;
use basegl::display::world::WorldData;
use basegl::system::web::forward_panic_hook_to_console;
use basegl::system::web::text_input::KeyboardBinding;
use basegl::system::web;
use basegl::system::web::forward_panic_hook_to_console;
use basegl_system_web::set_stdout;
use nalgebra::Vector2;
use nalgebra::Vector4;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use web_sys::MouseEvent;
const TEXT:&str =
"To be, or not to be, that is the question:
Whether 'tis nobler in the mind to suffer
@ -35,7 +33,7 @@ Devoutly to be wish'd.";
#[wasm_bindgen]
#[allow(dead_code)]
pub fn run_example_text_selecting() {
pub fn run_example_text_field() {
forward_panic_hook_to_console();
set_stdout();
basegl_core_msdf_sys::run_once_initialized(|| {
@ -44,59 +42,31 @@ pub fn run_example_text_selecting() {
let camera = scene.camera();
let screen = camera.screen();
let mut fonts = FontRegistry::new();
let font_id = fonts.load_embedded_font("DejaVuSansMono").unwrap();
let font = fonts.get_or_load_embedded_font("DejaVuSansMono").unwrap();
let properties = TextFieldProperties {
font_id,
font,
text_size : 16.0,
base_color : Vector4::new(0.0, 0.0, 0.0, 1.0),
size : Vector2::new(200.0, 200.0)
};
let text_field = TextField::new(&world,TEXT,properties,&mut fonts);
let text_field = TextField::new_with_content(&world,TEXT,properties);
text_field.set_position(Vector3::new(10.0, 600.0, 0.0));
text_field.jump_cursor(Vector2::new(50.0, -40.0),false,&mut fonts);
text_field.add_cursor(TextLocation{line:1, column:0}, &mut fonts);
text_field.jump_cursor(Vector2::new(50.0, -40.0),false);
world.add_child(&text_field);
text_field.update();
let text_field_on_click = text_field.clone_ref();
let text_field_on_copy = text_field.clone_ref();
let text_field_on_paste = text_field;
let fonts_rc = Rc::new(RefCell::new(fonts));
let fonts_on_click = fonts_rc.clone_ref();
let fonts_on_copy = fonts_rc.clone_ref();
let fonts_on_paste = fonts_rc.clone_ref();
let c: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new(move |val:JsValue| {
let mut fonts = fonts_on_click.borrow_mut();
let text_field = &text_field_on_click;
let val = val.unchecked_into::<MouseEvent>();
let x = val.x() as f32 - 10.0;
let y = (screen.height - val.y() as f32) - 600.0;
text_field.jump_cursor(Vector2::new(x,y),true,&mut fonts);
text_field.jump_cursor(Vector2::new(x,y),true);
}));
web::document().unwrap().add_event_listener_with_callback
("click",c.as_ref().unchecked_ref()).unwrap();
c.forget();
let mut keyboard = KeyboardBinding::create();
keyboard.set_copy_handler(move |cut:bool| {
let mut fonts = fonts_on_copy.borrow_mut();
let text_field = &text_field_on_copy;
let text_to_copy = text_field.get_selected_text();
if cut {
text_field.edit("", &mut fonts);
}
text_to_copy
});
keyboard.set_paste_handler(move |pasted:String| {
let mut fonts = fonts_on_paste.borrow_mut();
let text_field = &text_field_on_paste;
text_field.edit(pasted.as_str(),&mut fonts);
});
world.on_frame(move |_| { let _keep_alive = &keyboard; }).forget();
});
}

View File

@ -4,8 +4,8 @@ use wasm_bindgen::prelude::*;
use basegl::display::object::DisplayObjectOps;
use basegl::display::shape::text::glyph::font::FontRegistry;
use basegl::display::shape::text::text_field::cursor::Step::Right;
use basegl::display::shape::text::text_field::{TextField, TextFieldProperties};
use basegl::display::shape::text::text_field::TextField;
use basegl::display::shape::text::text_field::TextFieldProperties;
use basegl::display::world::*;
use basegl::system::web;
use nalgebra::Vector2;
@ -20,16 +20,16 @@ pub fn run_example_text_typing() {
basegl_core_msdf_sys::run_once_initialized(|| {
let world = &WorldData::new(&web::body());
let mut fonts = FontRegistry::new();
let font_id = fonts.load_embedded_font("DejaVuSansMono").unwrap();
let font = fonts.get_or_load_embedded_font("DejaVuSansMono").unwrap();
let properties = TextFieldProperties {
font_id,
font,
text_size : 16.0,
base_color : Vector4::new(0.0, 0.0, 0.0, 1.0),
size : Vector2::new(200.0, 200.0)
};
let mut text_field = TextField::new(&world,"",properties,&mut fonts);
let mut text_field = TextField::new(&world,properties);
text_field.set_position(Vector3::new(10.0, 600.0, 0.0));
world.add_child(&text_field);
@ -38,7 +38,7 @@ pub fn run_example_text_typing() {
let start_scrolling = animation_start + 10000.0;
let mut chars = typed_character_list(animation_start,include_str!("../../core/src/lib.rs"));
world.on_frame(move |_| {
animate_text_component(&mut fonts,&mut text_field,&mut chars,start_scrolling)
animate_text_component(&mut text_field,&mut chars,start_scrolling)
}).forget();
});
}
@ -58,19 +58,15 @@ fn typed_character_list(start_time:f64, text:&'static str) -> Vec<CharToPush> {
}
fn animate_text_component
( fonts : &mut FontRegistry
, text_field : &mut TextField
, typed_chars : &mut Vec<CharToPush>
, start_scrolling : f64) {
(text_field:&mut TextField, typed_chars:&mut Vec<CharToPush>, start_scrolling:f64) {
let now = js_sys::Date::now();
let to_type_now = typed_chars.drain_filter(|ch| ch.time <= now);
for ch in to_type_now {
let string = ch.a_char.to_string();
text_field.edit(string.as_str(),fonts);
text_field.navigate_cursors(Right,false,fonts);
text_field.write(string.as_str());
}
if start_scrolling <= js_sys::Date::now() {
text_field.scroll(Vector2::new(0.0,-0.1),fonts);
text_field.scroll(Vector2::new(0.0,-0.1));
}
text_field.update();
}