Add LineWrapper::wrap_shaped_line

This allows us to perform wrapping based on
glyph positions in an already-shaped line. We
plan to use this in the new Text element,
because there we'll already need to do text
shaping as part of layout. This text isn't editable
so it won't need to be rewrapped with the same
frequency as the text editor's content.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2021-08-24 18:09:16 -07:00
parent 0187ac8fde
commit 91c2b5825e
2 changed files with 150 additions and 3 deletions

View file

@ -170,6 +170,10 @@ impl Line {
Self { layout, color_runs }
}
pub fn runs(&self) -> &[Run] {
&self.layout.runs
}
pub fn width(&self) -> f32 {
self.layout.width
}
@ -245,3 +249,9 @@ impl Line {
}
}
}
impl Run {
pub fn glyphs(&self) -> &[Glyph] {
&self.glyphs
}
}

View file

@ -1,5 +1,5 @@
use crate::Settings;
use gpui::{fonts::FontId, FontCache, FontSystem};
use gpui::{fonts::FontId, text_layout::Line, FontCache, FontSystem};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::{
@ -19,6 +19,12 @@ pub struct Boundary {
pub next_indent: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ShapedBoundary {
pub run_ix: usize,
pub glyph_ix: usize,
}
impl Boundary {
fn new(ix: usize, next_indent: u32) -> Self {
Self { ix, next_indent }
@ -135,6 +141,74 @@ impl LineWrapper {
})
}
pub fn wrap_shaped_line<'a>(
&'a mut self,
str: &'a str,
line: &'a Line,
wrap_width: f32,
) -> impl Iterator<Item = ShapedBoundary> + 'a {
let mut width = 0.0;
let mut first_non_whitespace_ix = None;
let mut last_candidate_ix = None;
let mut last_candidate_x = 0.0;
let mut last_wrap_ix = ShapedBoundary {
run_ix: 0,
glyph_ix: 0,
};
let mut last_wrap_x = 0.;
let mut prev_c = '\0';
let mut glyphs = line
.runs()
.iter()
.enumerate()
.flat_map(move |(run_ix, run)| {
run.glyphs()
.iter()
.enumerate()
.map(move |(glyph_ix, glyph)| {
let character = str[glyph.index..].chars().next().unwrap();
(
ShapedBoundary { run_ix, glyph_ix },
character,
glyph.position.x(),
)
})
});
iter::from_fn(move || {
while let Some((ix, c, x)) = glyphs.next() {
if c == '\n' {
continue;
}
if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
last_candidate_ix = Some(ix);
last_candidate_x = x;
}
if c != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
let width = x - last_wrap_x;
if width > wrap_width && ix > last_wrap_ix {
if let Some(last_candidate_ix) = last_candidate_ix.take() {
last_wrap_ix = last_candidate_ix;
last_wrap_x = last_candidate_x;
} else {
last_wrap_ix = ix;
last_wrap_x = x;
}
return Some(last_wrap_ix);
}
prev_c = c;
}
None
})
}
fn is_boundary(&self, prev: char, next: char) -> bool {
(prev == ' ') && (next != ' ')
}
@ -163,7 +237,6 @@ impl LineWrapper {
}
fn compute_width_for_char(&self, c: char) -> f32 {
log::info!("cache miss {}", c);
self.font_system
.layout_line(
&c.to_string(),
@ -200,9 +273,14 @@ impl DerefMut for LineWrapperHandle {
#[cfg(test)]
mod tests {
use super::*;
use gpui::{
color::Color,
fonts::{Properties, Weight},
TextLayoutCache,
};
#[gpui::test]
fn test_line_wrapper(cx: &mut gpui::MutableAppContext) {
fn test_wrap_line(cx: &mut gpui::MutableAppContext) {
let font_cache = cx.font_cache().clone();
let font_system = cx.platform().fonts();
let settings = Settings {
@ -263,4 +341,63 @@ mod tests {
]
);
}
#[gpui::test]
fn test_wrap_layout_line(cx: &mut gpui::MutableAppContext) {
let font_cache = cx.font_cache().clone();
let font_system = cx.platform().fonts();
let text_layout_cache = TextLayoutCache::new(font_system.clone());
let family = font_cache.load_family(&["Helvetica"]).unwrap();
let settings = Settings {
tab_size: 4,
buffer_font_family: family,
buffer_font_size: 16.0,
..Settings::new(&font_cache).unwrap()
};
let normal = font_cache.select_font(family, &Default::default()).unwrap();
let bold = font_cache
.select_font(
family,
&Properties {
weight: Weight::BOLD,
..Default::default()
},
)
.unwrap();
let text = "aa bbb cccc ddddd eeee";
let line = text_layout_cache.layout_str(
text,
16.0,
&[
(4, normal, Color::default()),
(5, bold, Color::default()),
(6, normal, Color::default()),
(1, bold, Color::default()),
(7, normal, Color::default()),
],
);
let mut wrapper = LineWrapper::new(font_system, &font_cache, settings);
assert_eq!(
wrapper
.wrap_shaped_line(&text, &line, 72.0)
.collect::<Vec<_>>(),
&[
ShapedBoundary {
run_ix: 1,
glyph_ix: 3
},
ShapedBoundary {
run_ix: 2,
glyph_ix: 3
},
ShapedBoundary {
run_ix: 4,
glyph_ix: 2
}
],
);
}
}