From 830f5336b98df5fb2d5f3462e96f9e3a82825289 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 21 Jul 2021 17:23:05 -0700 Subject: [PATCH] Start work on wrapping lines without text shaping --- zed/src/editor/display_map/wrap_map.rs | 162 ++++++++++++++++++++++--- 1 file changed, 146 insertions(+), 16 deletions(-) diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index c914227ec1..cc1adf739a 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -11,14 +11,19 @@ use crate::{ util::Bias, Settings, }; -use gpui::{AppContext, FontCache, FontSystem, Task}; +use gpui::{fonts::FontId, AppContext, FontCache, FontSystem, Task}; use parking_lot::Mutex; use postage::{ prelude::{Sink, Stream}, watch, }; use smol::channel; -use std::{collections::VecDeque, ops::Range, sync::Arc, time::Duration}; +use std::{ + collections::{HashMap, VecDeque}, + ops::Range, + sync::Arc, + time::Duration, +}; #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct OutputPoint(super::Point); @@ -440,11 +445,18 @@ impl WrapMap { } struct BackgroundWrapper { - settings: Settings, wrap_width: Option, - font_cache: Arc, - font_system: Arc, snapshot: Snapshot, + line_wrapper: LineWrapper, +} + +struct LineWrapper { + font_system: Arc, + font_cache: Arc, + font_id: FontId, + font_size: f32, + cached_ascii_char_widths: [f32; 128], + cached_other_char_widths: HashMap, } enum Change { @@ -464,11 +476,9 @@ impl BackgroundWrapper { font_system: Arc, ) -> Self { Self { - settings, wrap_width, - font_cache, - font_system, snapshot, + line_wrapper: LineWrapper::new(font_system, font_cache, settings), } } @@ -516,12 +526,6 @@ impl BackgroundWrapper { return; } - let font_id = self - .font_cache - .select_font(self.settings.buffer_font_family, &Default::default()) - .unwrap(); - let font_size = self.settings.buffer_font_size; - #[derive(Debug)] struct RowEdit { old_rows: Range, @@ -591,8 +595,8 @@ impl BackgroundWrapper { let mut prev_boundary_ix = 0; if let Some(wrap_width) = self.wrap_width { for boundary_ix in self - .font_system - .wrap_line(&line, font_id, font_size, wrap_width) + .line_wrapper + .wrap_line_without_shaping(&line, wrap_width) { let wrapped = &line[prev_boundary_ix..boundary_ix]; new_transforms @@ -654,6 +658,100 @@ impl BackgroundWrapper { } } +impl LineWrapper { + fn new( + font_system: Arc, + font_cache: Arc, + settings: Settings, + ) -> Self { + let font_id = font_cache + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(); + let font_size = settings.buffer_font_size; + Self { + font_cache, + font_system, + font_id, + font_size, + cached_ascii_char_widths: [f32::NAN; 128], + cached_other_char_widths: HashMap::new(), + } + } + + fn wrap_line_with_shaping(&mut self, line: &str, wrap_width: f32) -> Vec { + self.font_system + .wrap_line(line, self.font_id, self.font_size, wrap_width) + } + + fn wrap_line_without_shaping(&mut self, line: &str, wrap_width: f32) -> Vec { + let mut width = 0.0; + let mut result = Vec::new(); + let mut last_boundary_ix = 0; + let mut last_boundary_width = 0.0; + let mut prev_c = '\0'; + for (ix, c) in line.char_indices() { + if self.is_boundary(prev_c, c) { + last_boundary_ix = ix; + last_boundary_width = width; + } + + let char_width = self.width_for_char(c); + width += char_width; + if width > wrap_width { + if last_boundary_ix > 0 { + result.push(last_boundary_ix); + width -= last_boundary_width; + last_boundary_ix = 0; + } else { + result.push(ix); + width = char_width; + } + } + prev_c = c; + } + result + } + + fn is_boundary(&self, prev: char, next: char) -> bool { + if prev == ' ' || next == ' ' { + return true; + } + false + } + + fn width_for_char(&mut self, c: char) -> f32 { + if (c as u32) < 128 { + let mut width = self.cached_ascii_char_widths[c as usize]; + if width.is_nan() { + width = self.compute_width_for_char(c); + self.cached_ascii_char_widths[c as usize] = width; + } + width + } else { + let mut width = self + .cached_other_char_widths + .get(&c) + .copied() + .unwrap_or(f32::NAN); + if width.is_nan() { + width = self.compute_width_for_char(c); + self.cached_other_char_widths.insert(c, width); + } + width + } + } + + fn compute_width_for_char(&self, c: char) -> f32 { + self.font_system + .layout_line( + &c.to_string(), + self.font_size, + &[(1, self.font_id, Default::default())], + ) + .width + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] struct Transform { summary: TransformSummary, @@ -761,6 +859,38 @@ mod tests { use rand::prelude::*; use std::env; + #[gpui::test] + fn test_line_wrapper(cx: &mut gpui::MutableAppContext) { + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().fonts(); + let settings = Settings { + tab_size: 4, + buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(), + buffer_font_size: 16.0, + ..Settings::new(&font_cache).unwrap() + }; + + let mut wrapper = LineWrapper::new(font_system, font_cache, settings); + + assert_eq!( + wrapper.wrap_line_with_shaping("aa bbb cccc ddddd eeee", 72.0), + &[7, 12, 18], + ); + assert_eq!( + wrapper.wrap_line_without_shaping("aa bbb cccc ddddd eeee", 72.0), + &[7, 12, 18], + ); + + assert_eq!( + wrapper.wrap_line_with_shaping("aaa aaaaaaaaaaaaaaaaaa", 72.0), + &[4, 11, 18], + ); + assert_eq!( + wrapper.wrap_line_without_shaping("aaa aaaaaaaaaaaaaaaaaa", 72.0), + &[4, 11, 18], + ); + } + #[gpui::test] fn test_random_wraps(cx: &mut gpui::MutableAppContext) { let iterations = env::var("ITERATIONS")