diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index 57556d61b6..d9103498c4 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -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 + } +} diff --git a/zed/src/editor/display_map/line_wrapper.rs b/zed/src/editor/display_map/line_wrapper.rs index 106e55a55f..696a2ac2dd 100644 --- a/zed/src/editor/display_map/line_wrapper.rs +++ b/zed/src/editor/display_map/line_wrapper.rs @@ -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 + '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::>(), + &[ + ShapedBoundary { + run_ix: 1, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 2, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 4, + glyph_ix: 2 + } + ], + ); + } }