mirror of
https://github.com/enso-org/enso.git
synced 2024-12-28 15:13:25 +03:00
TextField keyboard input. (https://github.com/enso-org/ide/pull/170)
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:
parent
dc1ce292b0
commit
0fe5b0fe8e
@ -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
|
||||
|
@ -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,
|
||||
¶ms,
|
||||
);
|
||||
// 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,
|
||||
¶ms,
|
||||
);
|
||||
// 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]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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,¶ms);
|
||||
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,¶ms);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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());
|
||||
})
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,¤t.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,¤t.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)
|
||||
|
195
gui/lib/core/src/display/shape/text/text_field/keyboard.rs
Normal file
195
gui/lib/core/src/display/shape/text/text_field/keyboard.rs
Normal 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));
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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" }
|
||||
|
@ -1,5 +1,7 @@
|
||||
//! Root module for Input / Output FRP bindings
|
||||
|
||||
pub mod mouse;
|
||||
pub mod keyboard;
|
||||
|
||||
pub use mouse::*;
|
||||
pub use keyboard::*;
|
||||
|
259
gui/lib/frp/src/io/keyboard.rs
Normal file
259
gui/lib/frp/src/io/keyboard.rs
Normal 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());
|
||||
}
|
||||
}
|
@ -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 |_| {
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user