diff options
author | 2023-02-24 20:52:10 +0100 | |
---|---|---|
committer | 2023-02-24 20:52:10 +0100 | |
commit | 368cadd25a8b57ee5c41e45d1abe8d1dfb194c69 (patch) | |
tree | 191cb7cc7807a5fe513b3d485b2fda21d3bf0bde /wgpu/src/text.rs | |
parent | 573d27eb52bbfacf1b06983b4282f00eb5265bdc (diff) | |
parent | 8059c40142d601588e01c152ce1bb72a1295dde8 (diff) | |
download | iced-368cadd25a8b57ee5c41e45d1abe8d1dfb194c69.tar.gz iced-368cadd25a8b57ee5c41e45d1abe8d1dfb194c69.tar.bz2 iced-368cadd25a8b57ee5c41e45d1abe8d1dfb194c69.zip |
Merge pull request #1697 from iced-rs/text-glyphon
Text shaping, font fallback, and `iced_wgpu` overhaul
Diffstat (limited to 'wgpu/src/text.rs')
-rw-r--r-- | wgpu/src/text.rs | 571 |
1 files changed, 362 insertions, 209 deletions
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e17b84c1..dea6ab18 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,128 +1,267 @@ -use crate::Transformation; - -use iced_graphics::font; +pub use iced_native::text::Hit; -use std::{cell::RefCell, collections::HashMap}; -use wgpu_glyph::ab_glyph; +use iced_graphics::layer::Text; +use iced_native::alignment; +use iced_native::{Color, Font, Rectangle, Size}; -pub use iced_native::text::Hit; +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; -#[derive(Debug)] +#[allow(missing_debug_implementations)] pub struct Pipeline { - draw_brush: RefCell<wgpu_glyph::GlyphBrush<()>>, - draw_font_map: RefCell<HashMap<String, wgpu_glyph::FontId>>, - measure_brush: RefCell<glyph_brush::GlyphBrush<()>>, + system: Option<System>, + renderers: Vec<glyphon::TextRenderer>, + atlas: glyphon::TextAtlas, + prepare_layer: usize, +} + +#[ouroboros::self_referencing] +struct System { + fonts: glyphon::FontSystem, + + #[borrows(fonts)] + #[not_covariant] + measurement_cache: RefCell<Cache<'this>>, + + #[borrows(fonts)] + #[not_covariant] + render_cache: Cache<'this>, } impl Pipeline { pub fn new( device: &wgpu::Device, + queue: &wgpu::Queue, format: wgpu::TextureFormat, - default_font: Option<&[u8]>, - multithreading: bool, ) -> Self { - let default_font = default_font.map(|slice| slice.to_vec()); - - // TODO: Font customization - #[cfg(not(target_os = "ios"))] - #[cfg(feature = "default_system_font")] - let default_font = { - default_font.or_else(|| { - font::Source::new() - .load(&[font::Family::SansSerif, font::Family::Serif]) - .ok() - }) - }; + Pipeline { + system: Some( + SystemBuilder { + fonts: glyphon::FontSystem::new_with_fonts( + [glyphon::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/Iced-Icons.ttf") + .as_slice(), + ))] + .into_iter(), + ), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ), + renderers: Vec::new(), + atlas: glyphon::TextAtlas::new(device, queue, format), + prepare_layer: 0, + } + } - let default_font = - default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + let heads = self.system.take().unwrap().into_heads(); - let font = ab_glyph::FontArc::try_from_vec(default_font) - .unwrap_or_else(|_| { - log::warn!( - "System font failed to load. Falling back to \ - embedded font..." - ); + let (locale, mut db) = heads.fonts.into_locale_and_db(); - ab_glyph::FontArc::try_from_slice(font::FALLBACK) - .expect("Load fallback font") - }); + db.load_font_source(glyphon::fontdb::Source::Binary(Arc::new( + bytes.into_owned(), + ))); + + self.system = Some( + SystemBuilder { + fonts: glyphon::FontSystem::new_with_locale_and_db(locale, db), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ); + } - let draw_brush_builder = - wgpu_glyph::GlyphBrushBuilder::using_font(font.clone()) - .initial_cache_size((2048, 2048)) - .draw_cache_multithread(multithreading); + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + sections: &[Text<'_>], + bounds: Rectangle, + scale_factor: f32, + target_size: Size<u32>, + ) -> bool { + self.system.as_mut().unwrap().with_mut(|fields| { + if self.renderers.len() <= self.prepare_layer { + self.renderers + .push(glyphon::TextRenderer::new(device, queue)); + } - #[cfg(target_arch = "wasm32")] - let draw_brush_builder = draw_brush_builder.draw_cache_align_4x4(true); + let renderer = &mut self.renderers[self.prepare_layer]; + + let keys: Vec<_> = sections + .iter() + .map(|section| { + let (key, _) = fields.render_cache.allocate( + fields.fonts, + Key { + content: section.content, + size: section.size * scale_factor, + font: section.font, + bounds: Size { + width: (section.bounds.width * scale_factor) + .ceil(), + height: (section.bounds.height * scale_factor) + .ceil(), + }, + color: section.color, + }, + ); + + key + }) + .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 = + sections.iter().zip(keys.iter()).map(|(section, key)| { + let buffer = fields + .render_cache + .get(key) + .expect("Get cached buffer"); + + let x = section.bounds.x * scale_factor; + let y = section.bounds.y * scale_factor; + + let (total_lines, max_width) = buffer + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); + + let total_height = + total_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, + } + }); + + let result = 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 glyphon::SwashCache::new(fields.fonts), + ); + + match result { + Ok(()) => { + self.prepare_layer += 1; + + true + } + Err(glyphon::PrepareError::AtlasFull(content_type)) => { + self.prepare_layer = 0; + + #[allow(clippy::needless_bool)] + if self.atlas.grow(device, content_type) { + false + } else { + // If the atlas cannot grow, then all bets are off. + // Instead of panicking, we will just pray that the result + // will be somewhat readable... + true + } + } + } + }) + } - let draw_brush = draw_brush_builder.build(device, format); + pub fn render<'a>( + &'a self, + layer: usize, + bounds: Rectangle<u32>, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + let renderer = &self.renderers[layer]; - let measure_brush = - glyph_brush::GlyphBrushBuilder::using_font(font).build(); + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); - Pipeline { - draw_brush: RefCell::new(draw_brush), - draw_font_map: RefCell::new(HashMap::new()), - measure_brush: RefCell::new(measure_brush), - } + renderer + .render(&self.atlas, render_pass) + .expect("Render text"); } - pub fn queue(&mut self, section: wgpu_glyph::Section<'_>) { - self.draw_brush.borrow_mut().queue(section); - } + pub fn end_frame(&mut self) { + self.atlas.trim(); - pub fn draw_queued( - &mut self, - device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - transformation: Transformation, - region: wgpu_glyph::Region, - ) { - self.draw_brush - .borrow_mut() - .draw_queued_with_transform_and_scissoring( - device, - staging_belt, - encoder, - target, - transformation.into(), - region, - ) - .expect("Draw text"); + self.system + .as_mut() + .unwrap() + .with_render_cache_mut(|cache| cache.trim()); + + self.prepare_layer = 0; } pub fn measure( &self, content: &str, size: f32, - font: iced_native::Font, - bounds: iced_native::Size, + font: Font, + bounds: Size, ) -> (f32, f32) { - use wgpu_glyph::GlyphCruncher; - - let wgpu_glyph::FontId(font_id) = self.find_font(font); - - let section = wgpu_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![wgpu_glyph::Text { - text: content, - scale: size.into(), - font_id: wgpu_glyph::FontId(font_id), - extra: wgpu_glyph::Extra::default(), - }], - ..Default::default() - }; - - if let Some(bounds) = - self.measure_brush.borrow_mut().glyph_bounds(section) - { - (bounds.width().ceil(), bounds.height().ceil()) - } else { - (0.0, 0.0) - } + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size, + font, + bounds, + 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( @@ -132,134 +271,148 @@ impl Pipeline { font: iced_native::Font, bounds: iced_native::Size, point: iced_native::Point, - nearest_only: bool, + _nearest_only: bool, ) -> Option<Hit> { - use wgpu_glyph::GlyphCruncher; - - let wgpu_glyph::FontId(font_id) = self.find_font(font); - - let section = wgpu_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![wgpu_glyph::Text { - text: content, - scale: size.into(), - font_id: wgpu_glyph::FontId(font_id), - extra: wgpu_glyph::Extra::default(), - }], - ..Default::default() - }; + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size, + font, + bounds, + color: Color::BLACK, + }, + ); + + let cursor = paragraph.hit(point.x, point.y)?; + + Some(Hit::CharOffset(cursor.index)) + }) + } - let mut mb = self.measure_brush.borrow_mut(); - - // The underlying type is FontArc, so clones are cheap. - use wgpu_glyph::ab_glyph::{Font, ScaleFont}; - let font = mb.fonts()[font_id].clone().into_scaled(size); - - // Implements an iterator over the glyph bounding boxes. - let bounds = mb.glyphs(section).map( - |wgpu_glyph::SectionGlyph { - byte_index, glyph, .. - }| { - ( - *byte_index, - iced_native::Rectangle::new( - iced_native::Point::new( - glyph.position.x - font.h_side_bearing(glyph.id), - glyph.position.y - font.ascent(), - ), - iced_native::Size::new( - font.h_advance(glyph.id), - font.ascent() - font.descent(), - ), - ), - ) - }, - ); + pub fn trim_measurement_cache(&mut self) { + self.system + .as_mut() + .unwrap() + .with_measurement_cache_mut(|cache| cache.borrow_mut().trim()); + } +} - // Implements computation of the character index based on the byte index - // within the input string. - let char_index = |byte_index| { - let mut b_count = 0; - for (i, utf8_len) in - content.chars().map(|c| c.len_utf8()).enumerate() - { - if byte_index < (b_count + utf8_len) { - return i; - } - b_count += utf8_len; - } +fn to_family(font: Font) -> glyphon::Family<'static> { + match font { + Font::Name(name) => glyphon::Family::Name(name), + Font::SansSerif => glyphon::Family::SansSerif, + Font::Serif => glyphon::Family::Serif, + Font::Cursive => glyphon::Family::Cursive, + Font::Fantasy => glyphon::Family::Fantasy, + Font::Monospace => glyphon::Family::Monospace, + } +} - byte_index - }; +struct Cache<'a> { + entries: FxHashMap<KeyHash, glyphon::Buffer<'a>>, + recently_used: FxHashSet<KeyHash>, + hasher: HashBuilder, + trim_count: usize, +} - if !nearest_only { - for (idx, bounds) in bounds.clone() { - if bounds.contains(point) { - return Some(Hit::CharOffset(char_index(idx))); - } - } - } +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; - let nearest = bounds - .map(|(index, bounds)| (index, bounds.center())) - .min_by(|(_, center_a), (_, center_b)| { - center_a - .distance(point) - .partial_cmp(¢er_b.distance(point)) - .unwrap_or(std::cmp::Ordering::Greater) - }); - - nearest.map(|(idx, center)| { - Hit::NearestCharOffset(char_index(idx), point - center) - }) - } +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; - pub fn trim_measurement_cache(&mut self) { - // TODO: We should probably use a `GlyphCalculator` for this. However, - // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. - // This makes stuff quite inconvenient. A manual method for trimming the - // cache would make our lives easier. - loop { - let action = self - .measure_brush - .borrow_mut() - .process_queued(|_, _| {}, |_| {}); - - match action { - Ok(_) => break, - Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { - let (width, height) = suggested; - - self.measure_brush - .borrow_mut() - .resize_texture(width, height); - } - } +impl<'a> Cache<'a> { + const TRIM_INTERVAL: usize = 300; + + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: HashBuilder::default(), + trim_count: 0, } } - pub fn find_font(&self, font: iced_native::Font) -> wgpu_glyph::FontId { - match font { - iced_native::Font::Default => wgpu_glyph::FontId(0), - iced_native::Font::External { name, bytes } => { - if let Some(font_id) = self.draw_font_map.borrow().get(name) { - return *font_id; - } + fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'a>> { + self.entries.get(key) + } - let font = ab_glyph::FontArc::try_from_slice(bytes) - .expect("Load font"); + fn allocate( + &mut self, + fonts: &'a glyphon::FontSystem, + key: Key<'_>, + ) -> (KeyHash, &mut glyphon::Buffer<'a>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + key.size.to_bits().hash(&mut hasher); + key.font.hash(&mut hasher); + key.bounds.width.to_bits().hash(&mut hasher); + key.bounds.height.to_bits().hash(&mut hasher); + key.color.into_rgba8().hash(&mut hasher); + + hasher.finish() + }; - let _ = self.measure_brush.borrow_mut().add_font(font.clone()); + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { + let metrics = glyphon::Metrics::new(key.size, key.size * 1.2); + let mut buffer = glyphon::Buffer::new(fonts, metrics); + + buffer.set_size( + key.bounds.width, + key.bounds.height.max(key.size * 1.2), + ); + 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, + ) + }) + .monospaced(matches!(key.font, Font::Monospace)), + ); + + let _ = entry.insert(buffer); + } - let font_id = self.draw_brush.borrow_mut().add_font(font); + let _ = self.recently_used.insert(hash); - let _ = self - .draw_font_map - .borrow_mut() - .insert(String::from(name), font_id); + (hash, self.entries.get_mut(&hash).unwrap()) + } - font_id - } + fn trim(&mut self) { + if self.trim_count >= Self::TRIM_INTERVAL { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + 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, + font: Font, + bounds: Size, + color: Color, +} + +type KeyHash = u64; |