Add layout run iterator

This commit is contained in:
Jeremy Soller 2022-10-25 12:52:46 -06:00
parent 5d7dd59078
commit a9b7b4e914
No known key found for this signature in database
GPG key ID: 87F211AF2BE4C2FE
8 changed files with 135 additions and 97 deletions

View file

@ -7,7 +7,7 @@ use std::{
};
use unicode_segmentation::UnicodeSegmentation;
use crate::{FontLayoutLine, FontMatches, FontShapeLine};
use crate::{LayoutGlyph, LayoutLine, FontMatches, FontShapeLine};
/// An action to perform on a [TextBuffer]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -76,11 +76,73 @@ impl TextLayoutCursor {
}
pub struct TextLayoutRun<'a> {
line_i: TextLineIndex,
text: &'a str,
shape: &'a FontShapeLine,
layout_line: &'a FontLayoutLine,
/// The index of the original text line
pub line_i: TextLineIndex,
/// The original text line
pub text: &'a str,
/// True if the original shaped line was RTL
pub rtl: bool,
/// The array of layout glyphs to draw
pub glyphs: &'a [LayoutGlyph],
/// Y offset of line
pub line_y: i32,
}
pub struct TextLayoutRunIter<'a> {
buffer: &'a TextBuffer<'a>,
line_i: usize,
layout_i: usize,
line_y: i32,
total_layout: i32,
}
impl<'a> TextLayoutRunIter<'a> {
pub fn new(buffer: &'a TextBuffer<'a>) -> Self {
Self {
buffer,
line_i: 0,
layout_i: 0,
line_y: buffer.metrics.font_size - buffer.metrics.line_height,
total_layout: 0,
}
}
}
impl<'a> Iterator for TextLayoutRunIter<'a> {
type Item = TextLayoutRun<'a>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(line) = self.buffer.lines.get(self.line_i) {
let shape = line.shape_opt.as_ref()?;
let layout = line.layout_opt.as_ref()?;
while let Some(layout_line) = layout.get(self.layout_i) {
self.layout_i += 1;
let scrolled = self.total_layout < self.buffer.scroll;
self.total_layout += 1;
if scrolled {
continue;
}
self.line_y += self.buffer.metrics.line_height;
if self.line_y > self.buffer.height {
return None;
}
return Some(TextLayoutRun {
line_i: TextLineIndex::new(self.line_i),
text: line.text.as_str(),
rtl: shape.rtl,
glyphs: &layout_line.glyphs,
line_y: self.line_y,
});
}
self.line_i += 1;
self.layout_i = 0;
}
None
}
}
/// Index of a text line
@ -128,7 +190,7 @@ impl fmt::Display for TextMetrics {
pub struct TextBufferLine {
text: String,
shape_opt: Option<FontShapeLine>,
layout_opt: Option<Vec<FontLayoutLine>>,
layout_opt: Option<Vec<LayoutLine>>,
}
impl TextBufferLine {
@ -157,7 +219,7 @@ impl TextBufferLine {
self.shape_opt.as_ref().unwrap()
}
pub fn layout(&mut self, font_matches: &FontMatches<'_>, font_size: i32, width: i32) -> &[FontLayoutLine] {
pub fn layout(&mut self, font_matches: &FontMatches<'_>, font_size: i32, width: i32) -> &[LayoutLine] {
if self.layout_opt.is_none() {
let mut layout = Vec::new();
let shape = self.shape(font_matches);
@ -802,43 +864,8 @@ impl<'a> TextBuffer<'a> {
}
/// Get the visible layout runs for rendering and other tasks
pub fn with_layout_runs<F: FnMut(TextLayoutRun)>(&self, mut f: F) {
let mut line_y = self.metrics.font_size;
let mut total_layout = 0;
for (line_i, line) in self.lines.iter().enumerate() {
let shape = match line.shape_opt.as_ref() {
Some(some) => some,
None => break,
};
let layout = match line.layout_opt.as_ref() {
Some(some) => some,
None => break,
};
for layout_line in layout {
let scrolled = total_layout < self.scroll;
total_layout += 1;
if scrolled {
continue;
}
if line_y > self.height {
return;
}
f(TextLayoutRun {
line_i: TextLineIndex::new(line_i),
text: line.text.as_str(),
shape,
layout_line,
line_y,
});
line_y += self.metrics.line_height;
}
}
pub fn layout_runs(&self) -> TextLayoutRunIter {
TextLayoutRunIter::new(self)
}
/// Draw the buffer
@ -849,16 +876,13 @@ impl<'a> TextBuffer<'a> {
let font_size = self.metrics.font_size;
let line_height = self.metrics.line_height;
self.with_layout_runs(|run| {
for run in self.layout_runs() {
let line_i = run.line_i;
let text = run.text;
let shape = run.shape;
let layout_line = run.layout_line;
let line_y = run.line_y;
let cursor_glyph_opt = |cursor: &TextCursor| -> Option<(usize, f32)> {
if cursor.line == line_i {
for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
if cursor.index == glyph.start {
return Some((glyph_i, 0.0));
} else if cursor.index > glyph.start && cursor.index < glyph.end {
@ -866,7 +890,7 @@ impl<'a> TextBuffer<'a> {
let mut before = 0;
let mut total = 0;
let cluster = &text[glyph.start..glyph.end];
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < cursor.index {
before += 1;
@ -878,10 +902,10 @@ impl<'a> TextBuffer<'a> {
return Some((glyph_i, offset));
}
}
match layout_line.glyphs.last() {
match run.glyphs.last() {
Some(glyph) => {
if cursor.index == glyph.end {
return Some((layout_line.glyphs.len(), 0.0));
return Some((run.glyphs.len(), 0.0));
}
},
None => {
@ -910,9 +934,9 @@ impl<'a> TextBuffer<'a> {
if line_i >= start.line && line_i <= end.line {
let mut range_opt = None;
for glyph in layout_line.glyphs.iter() {
for glyph in run.glyphs.iter() {
// Guess x offset based on characters
let cluster = &text[glyph.start..glyph.end];
let cluster = &run.text[glyph.start..glyph.end];
let total = cluster.grapheme_indices(true).count();
let mut c_x = glyph.x;
let c_w = glyph.w / total as f32;
@ -947,7 +971,7 @@ impl<'a> TextBuffer<'a> {
if let Some((mut min, mut max)) = range_opt.take() {
if end.line > line_i {
// Draw to end of line
if shape.rtl {
if run.rtl {
min = 0;
} else {
max = self.width;
@ -967,7 +991,7 @@ impl<'a> TextBuffer<'a> {
// Draw cursor
//TODO: draw at end of line but not start of next line
if let Some((cursor_glyph, cursor_glyph_offset)) = cursor_glyph_opt(&self.cursor) {
let x = match layout_line.glyphs.get(cursor_glyph) {
let x = match run.glyphs.get(cursor_glyph) {
Some(glyph) => {
// Start of detected glyph
if glyph.rtl {
@ -976,7 +1000,7 @@ impl<'a> TextBuffer<'a> {
(glyph.x + cursor_glyph_offset) as i32
}
},
None => match layout_line.glyphs.last() {
None => match run.glyphs.last() {
Some(glyph) => {
// End of last glyph
if glyph.rtl {
@ -1001,12 +1025,12 @@ impl<'a> TextBuffer<'a> {
);
}
for glyph in layout_line.glyphs.iter() {
for glyph in run.glyphs.iter() {
let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
cache.with_pixels(&self.font_matches, cache_key, color, |x, y, color| {
f(x_int + x, line_y + y_int + y, 1, 1, color)
});
}
});
}
}
}

View file

@ -1,11 +1,17 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
/// Key for building a glyph cache
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct CacheKey {
/// Font ID
pub font_id: fontdb::ID,
/// Glyph ID
pub glyph_id: u16,
/// Font size in pixels
pub font_size: i32,
/// Binning of fractional X offset
pub x_bin: SubpixelBin,
/// Binning of fractional Y offset
pub y_bin: SubpixelBin,
}
@ -32,6 +38,7 @@ impl CacheKey {
}
}
/// Binning of subpixel position for cache optimization
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SubpixelBin {
Zero,

View file

@ -1,19 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::CacheKey;
pub struct FontLayoutGlyph {
pub start: usize,
pub end: usize,
pub x: f32,
pub w: f32,
pub rtl: bool,
pub cache_key: CacheKey,
pub x_int: i32,
pub y_int: i32,
}
pub struct FontLayoutLine {
pub rtl: bool,
pub glyphs: Vec<FontLayoutGlyph>,
}

View file

@ -2,15 +2,9 @@
pub(crate) mod fallback;
pub(crate) use self::cache::*;
mod cache;
pub(crate) use self::font::*;
mod font;
pub(crate) use self::layout::*;
mod layout;
pub use self::matches::*;
mod matches;

View file

@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::{CacheKey, FontLayoutGlyph, FontLayoutLine};
use crate::{CacheKey, LayoutGlyph, LayoutLine};
pub struct FontShapeGlyph {
pub start: usize,
@ -14,7 +14,7 @@ pub struct FontShapeGlyph {
}
impl FontShapeGlyph {
fn layout(&self, font_size: i32, x: f32, y: f32, rtl: bool) -> FontLayoutGlyph {
fn layout(&self, font_size: i32, x: f32, y: f32, rtl: bool) -> LayoutGlyph {
let x_offset = font_size as f32 * self.x_offset;
let y_offset = font_size as f32 * self.y_offset;
let x_advance = font_size as f32 * self.x_advance;
@ -25,7 +25,7 @@ impl FontShapeGlyph {
font_size,
(x + x_offset, y - y_offset)
);
FontLayoutGlyph {
LayoutGlyph {
start: self.start,
end: self.end,
x,
@ -58,7 +58,7 @@ impl FontShapeLine {
&self,
font_size: i32,
line_width: i32,
layout_lines: &mut Vec<FontLayoutLine>,
layout_lines: &mut Vec<LayoutLine>,
mut layout_i: usize,
) {
let mut push_line = true;
@ -171,8 +171,7 @@ impl FontShapeLine {
std::mem::swap(&mut glyphs, &mut glyphs_swap);
layout_lines.insert(
layout_i,
FontLayoutLine {
rtl: self.rtl,
LayoutLine {
glyphs: glyphs_swap,
},
);
@ -205,8 +204,7 @@ impl FontShapeLine {
std::mem::swap(&mut glyphs, &mut glyphs_swap);
layout_lines.insert(
layout_i,
FontLayoutLine {
rtl: self.rtl,
LayoutLine {
glyphs: glyphs_swap,
},
);
@ -221,8 +219,7 @@ impl FontShapeLine {
if push_line {
layout_lines.insert(
layout_i,
FontLayoutLine {
rtl: self.rtl,
LayoutLine {
glyphs,
},
);

29
src/layout.rs Normal file
View file

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::CacheKey;
/// A laid out glyph
pub struct LayoutGlyph {
/// Start index of cluster in original line
pub start: usize,
/// End index of cluster in original line
pub end: usize,
/// X offset of hitbox
pub x: f32,
/// width of hitbox
pub w: f32,
/// True if the character is from an RTL script
pub rtl: bool,
/// Cache key, see [CacheKey]
pub cache_key: CacheKey,
/// Integer component of X offset in line
pub x_int: i32,
/// Integer component of Y offset in line
pub y_int: i32,
}
/// A line of laid out glyphs
pub struct LayoutLine {
/// Glyphs in line
pub glyphs: Vec<LayoutGlyph>,
}

View file

@ -3,9 +3,15 @@
pub use self::buffer::*;
mod buffer;
pub use self::cache::*;
mod cache;
pub use self::font::*;
mod font;
pub use self::layout::*;
mod layout;
#[cfg(feature = "swash")]
pub use self::swash::*;
#[cfg(feature = "swash")]

View file

@ -9,7 +9,7 @@ pub use swash::scale::image::Image as SwashImage;
pub struct SwashCache {
context: ScaleContext,
cache: HashMap<CacheKey, Option<SwashImage>>,
pub image_cache: HashMap<CacheKey, Option<SwashImage>>,
}
impl SwashCache {
@ -17,13 +17,13 @@ impl SwashCache {
pub fn new() -> Self {
Self {
context: ScaleContext::new(),
cache: HashMap::new()
image_cache: HashMap::new()
}
}
/// Create a swash Image from a cache key, caching results
pub fn get_image(&mut self, matches: &FontMatches<'_>, cache_key: CacheKey) -> &Option<SwashImage> {
self.cache.entry(cache_key).or_insert_with(|| {
self.image_cache.entry(cache_key).or_insert_with(|| {
let font = match matches.get_font(&cache_key.font_id) {
Some(some) => some,
None => {