pub use iced_native::text::Hit; use iced_graphics::layer::Text; use iced_native::alignment; use iced_native::{Color, Font, Rectangle, Size}; use rustc_hash::{FxHashMap, FxHashSet}; use std::cell::RefCell; use std::hash::{BuildHasher, Hash, Hasher}; use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { renderers: Vec<glyphon::TextRenderer>, atlas: glyphon::TextAtlas, cache: glyphon::SwashCache<'static>, measurement_cache: RefCell<Cache>, render_cache: Cache, layer: usize, } struct Cache { entries: FxHashMap<KeyHash, glyphon::Buffer<'static>>, recently_used: FxHashSet<KeyHash>, hasher: RandomXxHashBuilder64, } impl Cache { fn new() -> Self { Self { entries: FxHashMap::default(), recently_used: FxHashSet::default(), hasher: RandomXxHashBuilder64::default(), } } fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'static>> { self.entries.get(key) } fn allocate( &mut self, key: Key<'_>, ) -> (KeyHash, &mut glyphon::Buffer<'static>) { let hash = { let mut hasher = self.hasher.build_hasher(); key.content.hash(&mut hasher); (key.size as i32).hash(&mut hasher); key.font.hash(&mut hasher); (key.bounds.width as i32).hash(&mut hasher); (key.bounds.height as i32).hash(&mut hasher); key.color.into_rgba8().hash(&mut hasher); hasher.finish() }; if !self.entries.contains_key(&hash) { let metrics = glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); buffer.set_text( key.content, glyphon::Attrs::new().family(to_family(key.font)).color({ let [r, g, b, a] = key.color.into_linear(); glyphon::Color::rgba( (r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, (a * 255.0) as u8, ) }), ); let _ = self.entries.insert(hash, buffer); } let _ = self.recently_used.insert(hash); (hash, self.entries.get_mut(&hash).unwrap()) } fn trim(&mut self) { self.entries .retain(|key, _| self.recently_used.contains(key)); self.recently_used.clear(); } } #[derive(Debug, Clone, Copy)] struct Key<'a> { content: &'a str, size: f32, font: Font, bounds: Size, color: Color, } type KeyHash = u64; // TODO: Share with `iced_graphics` static FONT_SYSTEM: once_cell::sync::Lazy<glyphon::FontSystem> = once_cell::sync::Lazy::new(glyphon::FontSystem::new); impl Pipeline { pub fn new( device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat, _default_font: Option<&[u8]>, _multithreading: bool, ) -> Self { Pipeline { renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), cache: glyphon::SwashCache::new(&FONT_SYSTEM), measurement_cache: RefCell::new(Cache::new()), render_cache: Cache::new(), layer: 0, } } pub fn prepare( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, sections: &[Text<'_>], bounds: Rectangle, scale_factor: f32, target_size: Size<u32>, ) { if self.renderers.len() <= self.layer { self.renderers .push(glyphon::TextRenderer::new(device, queue)); } let renderer = &mut self.renderers[self.layer]; let keys: Vec<_> = sections .iter() .map(|section| { let (key, _) = self.render_cache.allocate(Key { content: section.content, size: section.size * scale_factor, font: section.font, bounds: Size { width: section.bounds.width * scale_factor, height: section.bounds.height * scale_factor, }, color: section.color, }); key }) .collect(); let buffers: Vec<_> = keys .iter() .map(|key| self.render_cache.get(key).expect("Get cached buffer")) .collect(); let bounds = glyphon::TextBounds { left: (bounds.x * scale_factor) as i32, top: (bounds.y * scale_factor) as i32, right: ((bounds.x + bounds.width) * scale_factor) as i32, bottom: ((bounds.y + bounds.height) * scale_factor) as i32, }; let text_areas: Vec<_> = sections .iter() .zip(buffers.iter()) .map(|(section, buffer)| { let x = section.bounds.x * scale_factor; let y = section.bounds.y * scale_factor; let max_width = buffer .layout_runs() .fold(0.0f32, |max, run| max.max(run.line_w)); let total_height = buffer.visible_lines() as f32 * section.size * 1.2 * scale_factor; let left = match section.horizontal_alignment { alignment::Horizontal::Left => x, alignment::Horizontal::Center => x - max_width / 2.0, alignment::Horizontal::Right => x - max_width, }; let top = match section.vertical_alignment { alignment::Vertical::Top => y, alignment::Vertical::Center => y - total_height / 2.0, alignment::Vertical::Bottom => y - total_height, }; glyphon::TextArea { buffer, left: left as i32, top: top as i32, bounds, } }) .collect(); renderer .prepare( device, queue, &mut self.atlas, glyphon::Resolution { width: target_size.width, height: target_size.height, }, &text_areas, glyphon::Color::rgb(0, 0, 0), &mut self.cache, ) .expect("Prepare text sections"); } pub fn render( &mut self, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, ) { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: target, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: true, }, })], depth_stencil_attachment: None, }); let renderer = &mut self.renderers[self.layer]; renderer .render(&self.atlas, &mut render_pass) .expect("Render text"); self.layer += 1; } pub fn end_frame(&mut self) { self.renderers.truncate(self.layer); self.layer = 0; } pub fn measure( &self, content: &str, size: f32, font: Font, bounds: Size, ) -> (f32, f32) { let mut measurement_cache = self.measurement_cache.borrow_mut(); let (_, paragraph) = measurement_cache.allocate(Key { content, size: size, font, bounds: Size { width: bounds.width, height: f32::INFINITY, }, color: Color::BLACK, }); let (total_lines, max_width) = paragraph .layout_runs() .enumerate() .fold((0, 0.0), |(_, max), (i, buffer)| { (i + 1, buffer.line_w.max(max)) }); (max_width, size * 1.2 * total_lines as f32) } pub fn hit_test( &self, _content: &str, _size: f32, _font: iced_native::Font, _bounds: iced_native::Size, _point: iced_native::Point, _nearest_only: bool, ) -> Option<Hit> { None } pub fn trim_measurement_cache(&mut self) { self.measurement_cache.borrow_mut().trim(); } } fn to_family(font: Font) -> glyphon::Family<'static> { match font { Font::Default => glyphon::Family::SansSerif, Font::External { name, .. } => glyphon::Family::Name(name), } }