diff options
Diffstat (limited to 'tiny_skia/src/text.rs')
-rw-r--r-- | tiny_skia/src/text.rs | 484 |
1 files changed, 172 insertions, 312 deletions
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 15f25740..70e95d01 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,18 +1,19 @@ use crate::core::alignment; -use crate::core::font::{self, Font}; -use crate::core::text::{Hit, LineHeight, Shaping}; -use crate::core::{Color, Pixels, Point, Rectangle, Size}; +use crate::core::text::{LineHeight, Shaping}; +use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics::color; +use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::editor; +use crate::graphics::text::font_system; +use crate::graphics::text::paragraph; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; use std::cell::RefCell; use std::collections::hash_map; -use std::hash::{BuildHasher, Hash, Hasher}; -use std::sync::Arc; #[allow(missing_debug_implementations)] pub struct Pipeline { - font_system: RefCell<cosmic_text::FontSystem>, glyph_cache: GlyphCache, cache: RefCell<Cache>, } @@ -20,31 +21,88 @@ pub struct Pipeline { impl Pipeline { pub fn new() -> Self { Pipeline { - font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts( - [cosmic_text::fontdb::Source::Binary(Arc::new( - include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), - ))] - .into_iter(), - )), glyph_cache: GlyphCache::new(), cache: RefCell::new(Cache::new()), } } pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.get_mut().db_mut().load_font_source( - cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), - ); + font_system() + .write() + .expect("Write font system") + .load_font(bytes); self.cache = RefCell::new(Cache::new()); } - pub fn draw( + pub fn draw_paragraph( + &mut self, + paragraph: ¶graph::Weak, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + use crate::core::text::Paragraph as _; + + let Some(paragraph) = paragraph.upgrade() else { + return; + }; + + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + paragraph.buffer(), + Rectangle::new(position, paragraph.min_bounds()), + color, + paragraph.horizontal_alignment(), + paragraph.vertical_alignment(), + scale_factor, + pixels, + clip_mask, + ); + } + + pub fn draw_editor( + &mut self, + editor: &editor::Weak, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + use crate::core::text::Editor as _; + + let Some(editor) = editor.upgrade() else { + return; + }; + + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + editor.buffer(), + Rectangle::new(position, editor.bounds()), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + } + + pub fn draw_cached( &mut self, content: &str, bounds: Rectangle, color: Color, - size: f32, + size: Pixels, line_height: LineHeight, font: Font, horizontal_alignment: alignment::Horizontal, @@ -54,189 +112,122 @@ impl Pipeline { pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, ) { - let line_height = f32::from(line_height.to_absolute(Pixels(size))); + let line_height = f32::from(line_height.to_absolute(size)); - let font_system = self.font_system.get_mut(); - let key = Key { + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + + let key = cache::Key { bounds: bounds.size(), content, font, - size, + size: size.into(), line_height, shaping, }; let (_, entry) = self.cache.get_mut().allocate(font_system, key); - let max_width = entry.bounds.width * scale_factor; - let total_height = entry.bounds.height * scale_factor; - - let bounds = bounds * scale_factor; - - let x = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => bounds.x - max_width / 2.0, - alignment::Horizontal::Right => bounds.x - max_width, - }; - - let y = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => bounds.y - total_height / 2.0, - alignment::Vertical::Bottom => bounds.y - total_height, - }; - - let mut swash = cosmic_text::SwashCache::new(); - - for run in entry.buffer.layout_runs() { - for glyph in run.glyphs { - let physical_glyph = glyph.physical((x, y), scale_factor); - - if let Some((buffer, placement)) = self.glyph_cache.allocate( - physical_glyph.cache_key, - color, - font_system, - &mut swash, - ) { - let pixmap = tiny_skia::PixmapRef::from_bytes( - buffer, - placement.width, - placement.height, - ) - .expect("Create glyph pixel map"); - - pixels.draw_pixmap( - physical_glyph.x + placement.left, - physical_glyph.y - placement.top - + (run.line_y * scale_factor).round() as i32, - pixmap, - &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::identity(), - clip_mask, - ); - } - } - } + let width = entry.min_bounds.width; + let height = entry.min_bounds.height; + + draw( + font_system, + &mut self.glyph_cache, + &entry.buffer, + Rectangle { + width, + height, + ..bounds + }, + color, + horizontal_alignment, + vertical_alignment, + scale_factor, + pixels, + clip_mask, + ); } pub fn trim_cache(&mut self) { self.cache.get_mut().trim(); self.glyph_cache.trim(); } - - pub fn measure( - &self, - content: &str, - size: f32, - line_height: LineHeight, - font: Font, - bounds: Size, - shaping: Shaping, - ) -> Size { - let mut measurement_cache = self.cache.borrow_mut(); - - let line_height = f32::from(line_height.to_absolute(Pixels(size))); - - let (_, entry) = measurement_cache.allocate( - &mut self.font_system.borrow_mut(), - Key { - content, - size, - line_height, - font, - bounds, - shaping, - }, - ); - - entry.bounds - } - - pub fn hit_test( - &self, - content: &str, - size: f32, - line_height: LineHeight, - font: Font, - bounds: Size, - shaping: Shaping, - point: Point, - _nearest_only: bool, - ) -> Option<Hit> { - let mut measurement_cache = self.cache.borrow_mut(); - - let line_height = f32::from(line_height.to_absolute(Pixels(size))); - - let (_, entry) = measurement_cache.allocate( - &mut self.font_system.borrow_mut(), - Key { - content, - size, - line_height, - font, - bounds, - shaping, - }, - ); - - let cursor = entry.buffer.hit(point.x, point.y)?; - - Some(Hit::CharOffset(cursor.index)) - } -} - -fn measure(buffer: &cosmic_text::Buffer) -> Size { - let (width, total_lines) = buffer - .layout_runs() - .fold((0.0, 0usize), |(width, total_lines), run| { - (run.line_w.max(width), total_lines + 1) - }); - - Size::new(width, total_lines as f32 * buffer.metrics().line_height) } -fn to_family(family: font::Family) -> cosmic_text::Family<'static> { - match family { - font::Family::Name(name) => cosmic_text::Family::Name(name), - font::Family::SansSerif => cosmic_text::Family::SansSerif, - font::Family::Serif => cosmic_text::Family::Serif, - font::Family::Cursive => cosmic_text::Family::Cursive, - font::Family::Fantasy => cosmic_text::Family::Fantasy, - font::Family::Monospace => cosmic_text::Family::Monospace, - } -} - -fn to_weight(weight: font::Weight) -> cosmic_text::Weight { - match weight { - font::Weight::Thin => cosmic_text::Weight::THIN, - font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT, - font::Weight::Light => cosmic_text::Weight::LIGHT, - font::Weight::Normal => cosmic_text::Weight::NORMAL, - font::Weight::Medium => cosmic_text::Weight::MEDIUM, - font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD, - font::Weight::Bold => cosmic_text::Weight::BOLD, - font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD, - font::Weight::Black => cosmic_text::Weight::BLACK, - } -} - -fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch { - match stretch { - font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed, - font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed, - font::Stretch::Condensed => cosmic_text::Stretch::Condensed, - font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed, - font::Stretch::Normal => cosmic_text::Stretch::Normal, - font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded, - font::Stretch::Expanded => cosmic_text::Stretch::Expanded, - font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded, - font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded, +fn draw( + font_system: &mut cosmic_text::FontSystem, + glyph_cache: &mut GlyphCache, + buffer: &cosmic_text::Buffer, + bounds: Rectangle, + color: Color, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, +) { + let bounds = bounds * scale_factor; + + let x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - bounds.width / 2.0, + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - bounds.height / 2.0, + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let mut swash = cosmic_text::SwashCache::new(); + + for run in buffer.layout_runs() { + for glyph in run.glyphs { + let physical_glyph = glyph.physical((x, y), scale_factor); + + if let Some((buffer, placement)) = glyph_cache.allocate( + physical_glyph.cache_key, + glyph.color_opt.map(from_color).unwrap_or(color), + font_system, + &mut swash, + ) { + let pixmap = tiny_skia::PixmapRef::from_bytes( + buffer, + placement.width, + placement.height, + ) + .expect("Create glyph pixel map"); + + pixels.draw_pixmap( + physical_glyph.x + placement.left, + physical_glyph.y - placement.top + + (run.line_y * scale_factor).round() as i32, + pixmap, + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::identity(), + clip_mask, + ); + } + } } } -fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { - match shaping { - Shaping::Basic => cosmic_text::Shaping::Basic, - Shaping::Advanced => cosmic_text::Shaping::Advanced, +fn from_color(color: cosmic_text::Color) -> Color { + let [r, g, b, a] = color.as_rgba(); + + if color::GAMMA_CORRECTION { + // `cosmic_text::Color` is linear RGB in this case, so we + // need to convert back to sRGB + Color::from_linear_rgba( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + ) + } else { + Color::from_rgba8(r, g, b, a as f32 / 255.0) } } @@ -327,10 +318,10 @@ impl GlyphCache { } } - entry.insert((buffer, image.placement)); + let _ = entry.insert((buffer, image.placement)); } - self.recently_used.insert(key); + let _ = self.recently_used.insert(key); self.entries.get(&key).map(|(buffer, placement)| { (bytemuck::cast_slice(buffer.as_slice()), *placement) @@ -350,134 +341,3 @@ impl GlyphCache { } } } - -struct Cache { - entries: FxHashMap<KeyHash, Entry>, - measurements: FxHashMap<KeyHash, KeyHash>, - recently_used: FxHashSet<KeyHash>, - hasher: HashBuilder, - trim_count: usize, -} - -struct Entry { - buffer: cosmic_text::Buffer, - bounds: Size, -} - -#[cfg(not(target_arch = "wasm32"))] -type HashBuilder = twox_hash::RandomXxHashBuilder64; - -#[cfg(target_arch = "wasm32")] -type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; - -impl Cache { - const TRIM_INTERVAL: usize = 300; - - fn new() -> Self { - Self { - entries: FxHashMap::default(), - measurements: FxHashMap::default(), - recently_used: FxHashSet::default(), - hasher: HashBuilder::default(), - trim_count: 0, - } - } - - fn allocate( - &mut self, - font_system: &mut cosmic_text::FontSystem, - key: Key<'_>, - ) -> (KeyHash, &mut Entry) { - let hash = key.hash(self.hasher.build_hasher()); - - if let Some(hash) = self.measurements.get(&hash) { - let _ = self.recently_used.insert(*hash); - - return (*hash, self.entries.get_mut(hash).unwrap()); - } - - if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { - let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2); - let mut buffer = cosmic_text::Buffer::new(font_system, metrics); - - buffer.set_size( - font_system, - key.bounds.width, - key.bounds.height.max(key.size * 1.2), - ); - buffer.set_text( - font_system, - key.content, - cosmic_text::Attrs::new() - .family(to_family(key.font.family)) - .weight(to_weight(key.font.weight)) - .stretch(to_stretch(key.font.stretch)), - to_shaping(key.shaping), - ); - - let bounds = measure(&buffer); - - let _ = entry.insert(Entry { buffer, bounds }); - - for bounds in [ - bounds, - Size { - width: key.bounds.width, - ..bounds - }, - ] { - if key.bounds != bounds { - let _ = self.measurements.insert( - Key { bounds, ..key }.hash(self.hasher.build_hasher()), - hash, - ); - } - } - } - - let _ = self.recently_used.insert(hash); - - (hash, self.entries.get_mut(&hash).unwrap()) - } - - fn trim(&mut self) { - if self.trim_count > Self::TRIM_INTERVAL { - self.entries - .retain(|key, _| self.recently_used.contains(key)); - self.measurements - .retain(|_, value| self.recently_used.contains(value)); - - self.recently_used.clear(); - - self.trim_count = 0; - } else { - self.trim_count += 1; - } - } -} - -#[derive(Debug, Clone, Copy)] -struct Key<'a> { - content: &'a str, - size: f32, - line_height: f32, - font: Font, - bounds: Size, - shaping: Shaping, -} - -impl Key<'_> { - fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash { - self.content.hash(&mut hasher); - self.size.to_bits().hash(&mut hasher); - self.line_height.to_bits().hash(&mut hasher); - self.font.hash(&mut hasher); - self.bounds.width.to_bits().hash(&mut hasher); - self.bounds.height.to_bits().hash(&mut hasher); - self.shaping.hash(&mut hasher); - - hasher.finish() - } -} - -type KeyHash = u64; |