use crate::{FontStyle, FontWeight, TextCommand};
use glyphon::{
Attrs, Buffer as GlyphonBuffer, Cache, Color as GlyphonColor, Family, FontSystem, Metrics,
Resolution, Shaping, Style as GlyphonStyle, SwashCache, TextArea, TextAtlas, TextBounds,
TextRenderer, Viewport, Weight as GlyphonWeight,
};
use std::collections::HashMap;
struct PendingText {
buffer: GlyphonBuffer,
position: (f32, f32),
color: GlyphonColor,
bounds: TextBounds,
}
pub struct TextPipeline {
font_system: FontSystem,
swash_cache: SwashCache,
atlas: TextAtlas,
renderer: TextRenderer,
viewport: Viewport,
user_font_map: HashMap<String, String>,
default_family_name: Option<String>,
pending: Vec<PendingText>,
}
impl TextPipeline {
#[inline]
fn snap(px: f32) -> f32 {
px.round()
}
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
surface_format: wgpu::TextureFormat,
user_fonts: Vec<(String, Vec<u8>)>,
) -> Result<Self, String> {
#[cfg(target_arch = "wasm32")]
if user_fonts.is_empty() {
return Err("WASM target requires at least one font supplied.".to_string());
}
let mut font_system = FontSystem::new();
let mut user_font_map: HashMap<String, String> = HashMap::new();
for (name, data) in &user_fonts {
let before = font_system.db().faces().count();
font_system.db_mut().load_font_data(data.clone());
if font_system.db().faces().count() > before
&& let Some(face) = font_system.db().faces().last()
&& let Some((family_name, _)) = face.families.first()
{
user_font_map.insert(name.clone(), family_name.clone());
}
}
#[cfg(not(target_arch = "wasm32"))]
let default_family_name: Option<String> = None;
#[cfg(target_arch = "wasm32")]
let default_family_name: Option<String> = {
let name = user_fonts
.first()
.and_then(|(name, _)| user_font_map.get(name).cloned());
match name {
Some(n) => Some(n),
None => {
return Err("Invalid fallback font provided for WASM context.".to_string());
}
}
};
let swash_cache = SwashCache::new();
let cache = Cache::new(device);
let mut atlas = TextAtlas::new(device, queue, &cache, surface_format);
let renderer =
TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
let viewport = Viewport::new(device, &cache);
Ok(Self {
font_system,
swash_cache,
atlas,
renderer,
viewport,
user_font_map,
default_family_name,
pending: Vec::new(),
})
}
fn resolve_attrs<'a>(
user_font_map: &'a HashMap<String, String>,
default_family_name: &'a Option<String>,
font: Option<&str>,
weight: FontWeight,
style: FontStyle,
) -> Attrs<'a> {
let family = font
.and_then(|n| user_font_map.get(n))
.map(|s| Family::Name(s.as_str()))
.unwrap_or_else(|| {
default_family_name
.as_deref()
.map(Family::Name)
.unwrap_or(Family::SansSerif)
});
Attrs::new()
.family(family)
.weight(convert_weight(weight))
.style(convert_style(style))
}
#[allow(clippy::too_many_arguments)]
fn queue_run(
&mut self,
text: &str,
font: Option<&str>,
weight: FontWeight,
style: FontStyle,
scale: f32,
position: (f32, f32),
color: GlyphonColor,
) {
let attrs = Self::resolve_attrs(
&self.user_font_map,
&self.default_family_name,
font,
weight,
style,
);
let metrics = Metrics::new(scale, scale * 1.25);
let mut buffer = GlyphonBuffer::new(&mut self.font_system, metrics);
buffer.set_size(Some(f32::MAX), Some(f32::MAX));
buffer.set_text(text, &attrs, Shaping::Advanced, None);
buffer.shape_until_scroll(&mut self.font_system, false);
self.pending.push(PendingText {
buffer,
position,
color,
bounds: TextBounds {
left: position.0 as i32,
top: position.1 as i32,
right: i32::MAX,
bottom: i32::MAX,
},
});
}
fn measure_raw(
&mut self,
text: &str,
font: Option<&str>,
weight: FontWeight,
style: FontStyle,
scale: f32,
line_height: f32,
) -> (f32, f32) {
let attrs = Self::resolve_attrs(
&self.user_font_map,
&self.default_family_name,
font,
weight,
style,
);
let final_line_height = if line_height > 0.0 {
line_height
} else {
scale * 1.2
};
let metrics = Metrics::new(scale, final_line_height);
let mut buffer = GlyphonBuffer::new(&mut self.font_system, metrics);
buffer.set_size(Some(f32::MAX), Some(f32::MAX));
buffer.set_text(text, &attrs, Shaping::Advanced, None);
buffer.shape_until_scroll(&mut self.font_system, false);
let width = buffer
.layout_runs()
.map(|run| run.line_w)
.fold(0.0_f32, f32::max);
(width, metrics.line_height)
}
pub fn draw(&mut self, scale_factor: f32, theme: winit::window::Theme, command: &TextCommand) {
let color = command.style.color.unwrap_or(match theme {
winit::window::Theme::Dark => crate::Color::WHITE,
winit::window::Theme::Light => crate::Color::BLACK,
});
let scale = Self::snap(
command
.style
.font_size
.map(|s| s.to_physical(scale_factor))
.unwrap_or(20.0 * scale_factor),
);
let weight = command.style.font_weight.unwrap_or_default();
let style = command.style.font_style.unwrap_or_default();
let letter_spacing = command
.style
.letter_spacing
.map(|ls| ls.value().to_physical(scale_factor))
.unwrap_or(0.0);
let line_height = command
.style
.line_height
.map(|lh| lh.value().to_physical(scale_factor))
.unwrap_or(0.0);
let glyphon_color = GlyphonColor::rgba(
(color.r() * 255.0).round() as u8,
(color.g() * 255.0).round() as u8,
(color.b() * 255.0).round() as u8,
(color.a() * 255.0).round() as u8,
);
let snapped_position = (
Self::snap(command.position.0),
Self::snap(command.position.1),
);
if letter_spacing.abs() < f32::EPSILON {
self.queue_run(
&command.text,
command.font.as_deref(),
weight,
style,
scale,
snapped_position,
glyphon_color,
);
return;
}
let mut cursor_x = snapped_position.0;
for ch in command.text.chars() {
let mut buf = [0u8; 4];
let ch_str = ch.encode_utf8(&mut buf);
self.queue_run(
ch_str,
command.font.as_deref(),
weight,
style,
scale,
(Self::snap(cursor_x), snapped_position.1),
glyphon_color,
);
let (advance, _) = self.measure_raw(
ch_str,
command.font.as_deref(),
weight,
style,
scale,
line_height,
);
cursor_x += advance + letter_spacing;
}
}
#[allow(clippy::too_many_arguments)]
pub fn measure(
&mut self,
text: &str,
font: Option<&str>,
font_size: f32,
weight: FontWeight,
style: FontStyle,
letter_spacing: f32,
line_height: f32,
) -> (f32, f32) {
let scale = Self::snap(font_size);
let (width, height) = self.measure_raw(text, font, weight, style, scale, line_height);
let extra = if text.is_empty() {
0.0
} else {
letter_spacing * (text.chars().count() as f32 - 1.0)
};
(width + extra, height)
}
pub fn flush(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
width: u32,
height: u32,
) -> Result<(), String> {
self.viewport.update(queue, Resolution { width, height });
let text_areas: Vec<TextArea> = self
.pending
.iter()
.map(|p| TextArea {
buffer: &p.buffer,
left: p.position.0,
top: p.position.1,
scale: 1.0,
bounds: p.bounds,
default_color: p.color,
custom_glyphs: &[],
})
.collect();
self.renderer
.prepare(
device,
queue,
&mut self.font_system,
&mut self.atlas,
&self.viewport,
text_areas,
&mut self.swash_cache,
)
.map_err(|e| e.to_string())?;
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("text_pipeline_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.renderer
.render(&self.atlas, &self.viewport, &mut pass)
.map_err(|e| e.to_string())?;
}
self.atlas.trim();
self.pending.clear();
Ok(())
}
}
fn convert_weight(weight: FontWeight) -> GlyphonWeight {
match weight {
FontWeight::Thin => GlyphonWeight::THIN,
FontWeight::ExtraLight => GlyphonWeight::EXTRA_LIGHT,
FontWeight::Light => GlyphonWeight::LIGHT,
FontWeight::Regular => GlyphonWeight::NORMAL,
FontWeight::Medium => GlyphonWeight::MEDIUM,
FontWeight::SemiBold => GlyphonWeight::SEMIBOLD,
FontWeight::Bold => GlyphonWeight::BOLD,
FontWeight::ExtraBold => GlyphonWeight::EXTRA_BOLD,
FontWeight::Black => GlyphonWeight::BLACK,
}
}
fn convert_style(style: FontStyle) -> GlyphonStyle {
match style {
FontStyle::Normal => GlyphonStyle::Normal,
FontStyle::Italic => GlyphonStyle::Italic,
FontStyle::Oblique => GlyphonStyle::Oblique,
}
}