summaryrefslogtreecommitdiffstats
path: root/wgpu/src/text.rs
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón <hector0193@gmail.com>2023-02-24 20:52:10 +0100
committerLibravatar GitHub <noreply@github.com>2023-02-24 20:52:10 +0100
commit368cadd25a8b57ee5c41e45d1abe8d1dfb194c69 (patch)
tree191cb7cc7807a5fe513b3d485b2fda21d3bf0bde /wgpu/src/text.rs
parent573d27eb52bbfacf1b06983b4282f00eb5265bdc (diff)
parent8059c40142d601588e01c152ce1bb72a1295dde8 (diff)
downloadiced-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.rs571
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(&center_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;