use crate::core::alignment; use crate::core::text::Shaping; use crate::core::{ Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; 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; #[derive(Debug)] pub struct Pipeline { glyph_cache: GlyphCache, cache: RefCell, } impl Pipeline { pub fn new() -> Self { Pipeline { glyph_cache: GlyphCache::new(), cache: RefCell::new(Cache::new()), } } // TODO: Shared engine #[allow(dead_code)] pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { font_system() .write() .expect("Write font system") .load_font(bytes); self.cache = RefCell::new(Cache::new()); } pub fn draw_paragraph( &mut self, paragraph: ¶graph::Weak, position: Point, color: Color, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { 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(), pixels, clip_mask, transformation, ); } pub fn draw_editor( &mut self, editor: &editor::Weak, position: Point, color: Color, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { 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, pixels, clip_mask, transformation, ); } pub fn draw_cached( &mut self, content: &str, bounds: Rectangle, color: Color, size: Pixels, line_height: Pixels, font: Font, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { let line_height = f32::from(line_height); 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.into(), line_height, shaping, }; let (_, entry) = self.cache.get_mut().allocate(font_system, key); 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, pixels, clip_mask, transformation, ); } pub fn draw_raw( &mut self, buffer: &cosmic_text::Buffer, position: Point, color: Color, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { let mut font_system = font_system().write().expect("Write font system"); let (width, height) = buffer.size(); draw( font_system.raw(), &mut self.glyph_cache, buffer, Rectangle::new( position, Size::new( width.unwrap_or(pixels.width() as f32), height.unwrap_or(pixels.height() as f32), ), ), color, alignment::Horizontal::Left, alignment::Vertical::Top, pixels, clip_mask, transformation, ); } pub fn trim_cache(&mut self) { self.cache.get_mut().trim(); self.glyph_cache.trim(); } } 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, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { let bounds = bounds * transformation; 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), transformation.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"); let opacity = color.a * glyph .color_opt .map(|c| c.a() as f32 / 255.0) .unwrap_or(1.0); pixels.draw_pixmap( physical_glyph.x + placement.left, physical_glyph.y - placement.top + (run.line_y * transformation.scale_factor()).round() as i32, pixmap, &tiny_skia::PixmapPaint { opacity, ..tiny_skia::PixmapPaint::default() }, tiny_skia::Transform::identity(), clip_mask, ); } } } } fn from_color(color: cosmic_text::Color) -> Color { let [r, g, b, a] = color.as_rgba(); Color::from_rgba8(r, g, b, a as f32 / 255.0) } #[derive(Debug, Clone, Default)] struct GlyphCache { entries: FxHashMap< (cosmic_text::CacheKey, [u8; 3]), (Vec, cosmic_text::Placement), >, recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>, trim_count: usize, } impl GlyphCache { const TRIM_INTERVAL: usize = 300; const CAPACITY_LIMIT: usize = 16 * 1024; fn new() -> Self { GlyphCache::default() } fn allocate( &mut self, cache_key: cosmic_text::CacheKey, color: Color, font_system: &mut cosmic_text::FontSystem, swash: &mut cosmic_text::SwashCache, ) -> Option<(&[u8], cosmic_text::Placement)> { let [r, g, b, _a] = color.into_rgba8(); let key = (cache_key, [r, g, b]); if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) { // TODO: Outline support let image = swash.get_image_uncached(font_system, cache_key)?; let glyph_size = image.placement.width as usize * image.placement.height as usize; if glyph_size == 0 { return None; } let mut buffer = vec![0u32; glyph_size]; match image.content { cosmic_text::SwashContent::Mask => { let mut i = 0; // TODO: Blend alpha for _y in 0..image.placement.height { for _x in 0..image.placement.width { buffer[i] = bytemuck::cast( tiny_skia::ColorU8::from_rgba( b, g, r, image.data[i], ) .premultiply(), ); i += 1; } } } cosmic_text::SwashContent::Color => { let mut i = 0; for _y in 0..image.placement.height { for _x in 0..image.placement.width { // TODO: Blend alpha buffer[i >> 2] = bytemuck::cast( tiny_skia::ColorU8::from_rgba( image.data[i + 2], image.data[i + 1], image.data[i], image.data[i + 3], ) .premultiply(), ); i += 4; } } } cosmic_text::SwashContent::SubpixelMask => { // TODO } } let _ = entry.insert((buffer, image.placement)); } let _ = self.recently_used.insert(key); self.entries.get(&key).map(|(buffer, placement)| { (bytemuck::cast_slice(buffer.as_slice()), *placement) }) } pub fn trim(&mut self) { if self.trim_count > Self::TRIM_INTERVAL || self.recently_used.len() >= Self::CAPACITY_LIMIT { self.entries .retain(|key, _| self.recently_used.contains(key)); self.recently_used.clear(); self.entries.shrink_to(Self::CAPACITY_LIMIT); self.recently_used.shrink_to(Self::CAPACITY_LIMIT); self.trim_count = 0; } else { self.trim_count += 1; } } }